From c45473d6c12b2db3b61af9829c2ac4c1bd3d7522 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sat, 7 Nov 2026 16:49:19 +0000 Subject: [PATCH 001/106] fix: render SQL Server table functions as inline TVFs and list warehouse Recall relations portably SQL Server's render_create_function stripped a table function's SELECT and wrapped it in BEGIN RETURN (...) END, which the engine rejects. A TABLE return now renders RETURNS TABLE AS RETURN (). sqb recall schema --backend warehouse matched state relations with LIKE '\_sqlbuild\_%' ESCAPE '\', which ClickHouse cannot parse. A prefix comparison with SUBSTRING needs no escape in any dialect. --- .../src/bindings/_helpers/recall/command.rs | 6 ++-- .../src/bindings/_helpers/recall/constants.rs | 4 +++ .../sqlserver/classes/sqlserver_adapter.py | 13 ++++++-- src/sqlbuild/adapters/sqlserver/constants.py | 1 + .../sqlbuild/cli/recall/test_recall_cli.py | 6 ++++ .../adapters/sqlserver/_test_types.py | 3 ++ .../adapters/sqlserver/test_client.py | 31 ++++++++++++++++--- 7 files changed, 53 insertions(+), 11 deletions(-) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/recall/command.rs b/crates/sqlbuild-python/src/bindings/_helpers/recall/command.rs index 5ba2193fc8..ed4516e2cb 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/recall/command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/recall/command.rs @@ -33,6 +33,7 @@ use crate::bindings::_helpers::recall::constants::{ DEFAULT_MAX_CELL_BYTES, DEFAULT_QUERY_LIMIT, JSON_FORMAT, LAST_INVOCATION, RECALL_AMBIGUOUS_CODE, RECALL_DISABLED_CODE, RECALL_NOT_FOUND_CODE, RECALL_OPTIONS, RECALL_QUERY_CODE, RECALL_SETTINGS_CODE, RECALL_STORE_CODE, WAREHOUSE_BACKEND, + WAREHOUSE_STATE_RELATIONS_SQL, }; use crate::bindings::_helpers::recall::rendering::{QueryContext, render_result, render_schema}; use crate::bindings::_helpers::records::construct::new_error; @@ -402,10 +403,7 @@ fn run_schema(py: Python<'_>, recall: &RecallRequest) -> PyResult { ORDER BY table_schema, ordinal_position", relation.replace('\'', "''") ), - None => "SELECT table_schema, table_name, table_type FROM information_schema.tables \ - WHERE table_name LIKE '\\_sqlbuild\\_%' ESCAPE '\\' \ - ORDER BY table_schema, table_name" - .to_owned(), + None => WAREHOUSE_STATE_RELATIONS_SQL.to_owned(), }; return warehouse_query(py, recall, &project_dir, (&sql, None)); } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/recall/constants.rs b/crates/sqlbuild-python/src/bindings/_helpers/recall/constants.rs index 2a1af6996c..2a10b7918f 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/recall/constants.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/recall/constants.rs @@ -26,6 +26,10 @@ pub(crate) const RECALL_AMBIGUOUS_CODE: &str = "C973"; pub(crate) const RECALL_QUERY_CODE: &str = "C974"; pub(crate) const RECALL_SETTINGS_CODE: &str = "C975"; pub(crate) const WAREHOUSE_BACKEND: &str = "warehouse"; +/// SQLBuild's state relations in the warehouse; a prefix comparison needs no `LIKE` escape, whose syntax differs by dialect. +pub(crate) const WAREHOUSE_STATE_RELATIONS_SQL: &str = "SELECT table_schema, table_name, table_type \ + FROM information_schema.tables WHERE SUBSTRING(table_name, 1, 10) = '_sqlbuild_' \ + ORDER BY table_schema, table_name"; /// Default cell width in text formats, in bytes. pub(crate) const DEFAULT_MAX_CELL_BYTES: usize = 4096; pub(crate) const JSON_FORMAT: &str = "json"; diff --git a/src/sqlbuild/adapters/sqlserver/classes/sqlserver_adapter.py b/src/sqlbuild/adapters/sqlserver/classes/sqlserver_adapter.py index 2e50a2e916..ee514dd37e 100644 --- a/src/sqlbuild/adapters/sqlserver/classes/sqlserver_adapter.py +++ b/src/sqlbuild/adapters/sqlserver/classes/sqlserver_adapter.py @@ -116,6 +116,7 @@ INFORMATION_SCHEMA_NULLABLE_VALUE, INTEGER_TYPE_TOKEN, ISO8601_CONVERT_STYLE, + TABLE_FUNCTION_RETURN_TYPE, USER_TABLE_OBJECT_TYPE, ) from sqlbuild.compiler.compile.types import FunctionLanguage @@ -831,11 +832,17 @@ def render_create_function( message="Python functions require an engine-specific implementation" ) arg_sql: str = ", ".join(f"@{arg.name} {arg.type}" for arg in arguments) - body_expression: str = body_sql.strip().removeprefix("SELECT").strip().rstrip(";") + body_query: str = body_sql.strip().rstrip(";") for arg in arguments: - body_expression = re.sub( - rf"\b{re.escape(str(arg.name))}\b", f"@{arg.name}", body_expression + body_query = re.sub(rf"\b{re.escape(str(arg.name))}\b", f"@{arg.name}", body_query) + if returns.upper() == TABLE_FUNCTION_RETURN_TYPE: + return ( + f"CREATE OR ALTER FUNCTION {destination}({arg_sql})\n" + "RETURNS TABLE\n" + "AS\n" + f"RETURN (\n{body_query}\n)", ) + body_expression: str = body_query.removeprefix("SELECT").strip() if returns.upper() == BOOLEAN_RETURN_TYPE and not body_expression.upper().startswith( ("CASE ", "CAST(") ): diff --git a/src/sqlbuild/adapters/sqlserver/constants.py b/src/sqlbuild/adapters/sqlserver/constants.py index 611c496afc..bb206e1ed9 100644 --- a/src/sqlbuild/adapters/sqlserver/constants.py +++ b/src/sqlbuild/adapters/sqlserver/constants.py @@ -1,6 +1,7 @@ """SQL Server adapter constants.""" BOOLEAN_RETURN_TYPE: str = "BIT" +TABLE_FUNCTION_RETURN_TYPE: str = "TABLE" DIFF_CHARACTER_LENGTH_TYPES: frozenset[str] = frozenset( {"binary", "char", "nchar", "nvarchar", "varbinary", "varchar"} ) diff --git a/tests/integration/src/sqlbuild/cli/recall/test_recall_cli.py b/tests/integration/src/sqlbuild/cli/recall/test_recall_cli.py index d2def05b76..de785bec9c 100644 --- a/tests/integration/src/sqlbuild/cli/recall/test_recall_cli.py +++ b/tests/integration/src/sqlbuild/cli/recall/test_recall_cli.py @@ -149,6 +149,12 @@ def test_given_a_removed_format_when_querying_then_it_is_a_usage_error_naming_th expected_fragments=("│ order_id │ amount │", "INTEGER", "DECIMAL(10,2)"), unexpected_fragments=("RECORD",), ), + QueryLayoutTestCase( + description="warehouse schema lists SQLBuild's state relations only", + args=("recall", "schema", "--backend", "warehouse"), + expected_fragments=("_sqlbuild_fingerprints", "BASE TABLE"), + unexpected_fragments=("│ orders ",), + ), QueryLayoutTestCase( description="a limited result says how to see more", args=("query", "SELECT range AS order_id FROM range(30)"), diff --git a/tests/unit/src/sqlbuild/adapters/sqlserver/_test_types.py b/tests/unit/src/sqlbuild/adapters/sqlserver/_test_types.py index d4b2389d6f..2d3cd3b2b0 100644 --- a/tests/unit/src/sqlbuild/adapters/sqlserver/_test_types.py +++ b/tests/unit/src/sqlbuild/adapters/sqlserver/_test_types.py @@ -41,6 +41,9 @@ class SqlServerRenderCreateTableAsTestCase: @dataclass(frozen=True) class SqlServerRenderCreateFunctionTestCase: description: str + arguments: tuple[tuple[str, str], ...] + returns: str + body_sql: str expected_statements: tuple[str, ...] diff --git a/tests/unit/src/sqlbuild/adapters/sqlserver/test_client.py b/tests/unit/src/sqlbuild/adapters/sqlserver/test_client.py index a192084a60..ec4b59a935 100644 --- a/tests/unit/src/sqlbuild/adapters/sqlserver/test_client.py +++ b/tests/unit/src/sqlbuild/adapters/sqlserver/test_client.py @@ -419,6 +419,9 @@ def test_given_table_target_when_rendering_create_then_sqlserver_uses_select_int [ SqlServerRenderCreateFunctionTestCase( description="renders scalar SQL function with T-SQL body", + arguments=(("order_status", "NVARCHAR(MAX)"),), + returns="BIT", + body_sql="SELECT order_status = 'completed'", expected_statements=( "CREATE OR ALTER FUNCTION dbo.is_completed_order(@order_status NVARCHAR(MAX))\n" "RETURNS BIT\n" @@ -427,7 +430,25 @@ def test_given_table_target_when_rendering_create_then_sqlserver_uses_select_int " RETURN (CASE WHEN @order_status = 'completed' THEN 1 ELSE 0 END)\n" "END", ), - ) + ), + SqlServerRenderCreateFunctionTestCase( + description="renders a table function as an inline TVF that keeps its SELECT", + arguments=(("p_floor", "FLOAT"),), + returns="table", + body_sql=( + "SELECT id, CASE WHEN amount > p_floor THEN 'high' ELSE 'low' END AS band " + "FROM dbo.orders;" + ), + expected_statements=( + "CREATE OR ALTER FUNCTION dbo.is_completed_order(@p_floor FLOAT)\n" + "RETURNS TABLE\n" + "AS\n" + "RETURN (\n" + "SELECT id, CASE WHEN amount > @p_floor THEN 'high' ELSE 'low' END AS band " + "FROM dbo.orders\n" + ")", + ), + ), ], ids=lambda case: case.description, ) @@ -438,9 +459,11 @@ def test_given_sql_function_when_rendering_create_then_sqlserver_declares_functi statements: tuple[str, ...] = adapter.render_create_function( destination="dbo.is_completed_order", - arguments=(FunctionArgument(name="order_status", type="NVARCHAR(MAX)"),), - returns="BIT", - body_sql="SELECT order_status = 'completed'", + arguments=tuple( + FunctionArgument(name=name, type=type_name) for name, type_name in test_case.arguments + ), + returns=test_case.returns, + body_sql=test_case.body_sql, ) assert statements == test_case.expected_statements From bc86f064ed2c012bdf0b95f3b54b43447777cc40 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sat, 7 Nov 2026 16:16:43 +0000 Subject: [PATCH 002/106] feat(lint): warn when SQL lint is unavailable for a dialect A native lint response can report that the engine does not parse the project's dialect yet. Each such file now gets one L007 warning carrying the engine's reason, instead of looking lint-clean. A non-string reason is rejected as an invalid response. --- .../src/lint_text/_helpers/sql_diagnostics.rs | 79 +++++++++++++++++-- .../sqlbuild-rules/src/lint_text/constants.rs | 6 ++ .../src/lint_text/main/lint_sql_bodies.rs | 35 +++----- .../src/lint_text/tests/test_lint_text.rs | 72 ++++++++++++++++- .../src/lint_text/tests/test_types.rs | 10 +++ 5 files changed, 171 insertions(+), 31 deletions(-) diff --git a/crates/sqlbuild-rules/src/lint_text/_helpers/sql_diagnostics.rs b/crates/sqlbuild-rules/src/lint_text/_helpers/sql_diagnostics.rs index d7ddf43c3c..a2d51c33d3 100644 --- a/crates/sqlbuild-rules/src/lint_text/_helpers/sql_diagnostics.rs +++ b/crates/sqlbuild-rules/src/lint_text/_helpers/sql_diagnostics.rs @@ -1,6 +1,7 @@ //! Native SQL lint responses mapped back onto authored source through expansion passes. use std::borrow::Cow; +use std::collections::HashSet; use std::sync::LazyLock; use regex::Regex; @@ -10,7 +11,8 @@ use crate::lint_text::_helpers::sites::backtick_identifiers; use crate::lint_text::_helpers::sites::{byte_offset, compiled, lint_policy, site_end}; use crate::lint_text::constants::{ GENERATED_FIX, GENERATED_SQL_SUFFIX, HARNESS_CTE_PREFIXES, INVALID_DIAGNOSTIC, - INVALID_FIX_EDIT, INVALID_FIX_OBJECT, INVALID_FIX_REASON, LINT_API_VERSION, LONG_LITERAL_CODE, + INVALID_FIX_EDIT, INVALID_FIX_OBJECT, INVALID_FIX_REASON, INVALID_UNAVAILABLE, + LINT_API_VERSION, LINT_UNAVAILABLE_CODE, LINT_UNAVAILABLE_REMEDIATION, LONG_LITERAL_CODE, MACRO_MIXED_QUALIFICATION_REMEDIATION, MISSING_DIAGNOSTICS, MIXED_QUALIFICATION_CODE, NATIVE_LINT_ERROR_CODE, NON_CONTIGUOUS_FIX, NON_OBJECT_RESPONSE, NOT_AN_OBJECT, PARSE_ERROR_POSITION_PATTERN, PARSE_FAILURE_PREFIX, PARSE_FAILURE_REMEDIATION, @@ -109,8 +111,15 @@ pub(crate) fn map_offset(offset: i64, passes: &[Vec]) -> (i64, bo (current, generated) } -/// The diagnostics list of one engine response, or the error it reports. -pub(crate) fn response_diagnostics(response: &Value) -> Result, String> { +/// The diagnostics list of one engine response, or why its dialect could not be linted. +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum LintResult { + Diagnostics(Vec), + Unavailable(String), +} + +/// The result of one engine response, or the error it reports. +pub(crate) fn response_diagnostics(response: &Value) -> Result { let Some(payload) = response.as_object() else { return Err(NON_OBJECT_RESPONSE.to_string()); }; @@ -123,9 +132,67 @@ pub(crate) fn response_diagnostics(response: &Value) -> Result, Strin if integer(result.get("version")) != Some(LINT_API_VERSION) { return Err(UNSUPPORTED_VERSION.to_string()); } - match result.get("diagnostics") { - Some(Value::Array(diagnostics)) => Ok(diagnostics.clone()), - _ => Err(MISSING_DIAGNOSTICS.to_string()), + match (result.get("unavailable"), result.get("diagnostics")) { + (Some(Value::String(reason)), _) => Ok(LintResult::Unavailable(reason.clone())), + (Some(_), _) => Err(INVALID_UNAVAILABLE.to_string()), + (None, Some(Value::Array(diagnostics))) => Ok(LintResult::Diagnostics(diagnostics.clone())), + (None, _) => Err(MISSING_DIAGNOSTICS.to_string()), + } +} + +/// One body's authored diagnostics from its engine response, or the warning that it was not linted. +pub(crate) fn body_diagnostics( + response: &Result, + (body, omitted): (&LintSqlBody, Option), + file: &AuthoredFile<'_>, +) -> Vec { + let raw: Vec = match response { + Ok(LintResult::Diagnostics(raw)) => raw.clone(), + Ok(LintResult::Unavailable(reason)) => { + return vec![unavailable_warning(reason, body, file)]; + } + Err(message) => return vec![parse_failure(message, body, file)], + }; + without_generated_frame(raw, body, omitted) + .iter() + .filter_map(|item| match authored_diagnostic(item, body, file) { + Ok(diagnostic) => diagnostic, + Err(message) => Some(parse_failure(&message, body, file)), + }) + .collect() +} + +/// The diagnostics with only each file's first lint-unavailable warning. +pub(crate) fn unavailable_once_per_file( + diagnostics: Vec, +) -> Vec { + let mut warned: HashSet = HashSet::new(); + diagnostics + .into_iter() + .filter(|diagnostic| { + diagnostic.code != LINT_UNAVAILABLE_CODE || warned.insert(diagnostic.file) + }) + .collect() +} + +/// The warning that a body's SQL was not linted because the engine cannot parse its dialect. +fn unavailable_warning( + reason: &str, + body: &LintSqlBody, + file: &AuthoredFile<'_>, +) -> AuthoredDiagnostic { + let (line, column): (i64, i64) = file.position(body.body_start); + AuthoredDiagnostic { + file: body.file, + line, + column, + code: LINT_UNAVAILABLE_CODE.to_string(), + message: reason.to_string(), + fault: false, + end: None, + remediation: LINT_UNAVAILABLE_REMEDIATION.to_string(), + fix: None, + fix_unavailable_reason: None, } } diff --git a/crates/sqlbuild-rules/src/lint_text/constants.rs b/crates/sqlbuild-rules/src/lint_text/constants.rs index ea4e21a0bf..adc92a97cb 100644 --- a/crates/sqlbuild-rules/src/lint_text/constants.rs +++ b/crates/sqlbuild-rules/src/lint_text/constants.rs @@ -106,6 +106,10 @@ pub(crate) const CTE_ARGUMENT: &str = "__cte"; /// The help for mixed qualification in a SQL macro body, where slot reads are qualified through an alias. pub(crate) const MACRO_MIXED_QUALIFICATION_REMEDIATION: &str = "Use one consistent qualification style in this query scope. A plain slot read such as `@rel.column` counts as unqualified; to qualify it, read through an alias: `FROM @rel AS s`, then `s.@rel.column`."; pub(crate) const NATIVE_LINT_ERROR_CODE: &str = "L003"; +/// The warning code for SQL the native lint engine cannot parse in the project's dialect. +pub(crate) const LINT_UNAVAILABLE_CODE: &str = "L007"; +pub(crate) const LINT_UNAVAILABLE_REMEDIATION: &str = + "This SQL was not linted. Review it by hand until SQL lint supports this dialect."; pub(crate) const PARSE_ERROR_POSITION_PATTERN: &str = r"^Parse error at line (?P\d+), column (?P\d+):"; pub(crate) const HARNESS_CTE_PREFIXES: [&str; 13] = [ @@ -152,6 +156,8 @@ pub(crate) const NOT_AN_OBJECT: &str = "native lint diagnostic must be an object pub(crate) const NON_OBJECT_RESPONSE: &str = "native lint engine returned a non-object response"; pub(crate) const UNSUPPORTED_VERSION: &str = "native lint engine returned an unsupported response version"; +pub(crate) const INVALID_UNAVAILABLE: &str = + "native lint response has a non-string unavailable reason"; pub(crate) const MISSING_DIAGNOSTICS: &str = "native lint response is missing a diagnostics list"; pub(crate) const INVALID_BATCH: &str = "native lint engine returned an invalid batch response"; pub(crate) const NON_CONTIGUOUS_FIX: &str = diff --git a/crates/sqlbuild-rules/src/lint_text/main/lint_sql_bodies.rs b/crates/sqlbuild-rules/src/lint_text/main/lint_sql_bodies.rs index be43f45778..5eefd1e8f0 100644 --- a/crates/sqlbuild-rules/src/lint_text/main/lint_sql_bodies.rs +++ b/crates/sqlbuild-rules/src/lint_text/main/lint_sql_bodies.rs @@ -5,12 +5,12 @@ use std::collections::HashMap; use serde_json::Value; use crate::lint_text::_helpers::sql_diagnostics::{ - AuthoredFile, authored_diagnostic, parse_failure, response_diagnostics, without_generated_frame, + AuthoredFile, LintResult, body_diagnostics, response_diagnostics, unavailable_once_per_file, }; use crate::lint_text::_helpers::sql_requests::{lint_payload, omitted_select, run_batch}; use crate::lint_text::models::{AuthoredDiagnostic, LintSqlBody, LintSqlOptions}; -/// Lint every body once per distinct request and map each diagnostic onto its authored file. +/// Lint each distinct request once and map its diagnostics, or one unavailable warning per file, onto source. pub fn lint_sql_bodies( bodies: &[LintSqlBody], files: &[&str], @@ -30,31 +30,20 @@ pub fn lint_sql_bodies( } request_of_body.push(index); } - let responses: Vec, String>> = run_batch(&payloads) + let responses: Vec> = run_batch(&payloads) .map(|responses| responses.iter().map(response_diagnostics).collect()) .unwrap_or_else(|error| vec![Err(error); payloads.len()]); let authored: Vec> = files .iter() .map(|text| AuthoredFile::new(text, &options.dialect)) .collect(); - let mut diagnostics: Vec = Vec::new(); - for ((body, offset), request) in bodies.iter().zip(omitted).zip(request_of_body) { - let file: &AuthoredFile<'_> = &authored[body.file]; - let raw: Vec = match &responses[request] { - Ok(raw) => raw.clone(), - Err(message) => { - diagnostics.push(parse_failure(message, body, file)); - continue; - } - }; - let raw: Vec = without_generated_frame(raw, body, offset); - for item in &raw { - match authored_diagnostic(item, body, file) { - Ok(Some(diagnostic)) => diagnostics.push(diagnostic), - Ok(None) => {} - Err(message) => diagnostics.push(parse_failure(&message, body, file)), - } - } - } - diagnostics + let diagnostics: Vec = bodies + .iter() + .zip(omitted) + .zip(request_of_body) + .flat_map(|((body, offset), request)| { + body_diagnostics(&responses[request], (body, offset), &authored[body.file]) + }) + .collect(); + unavailable_once_per_file(diagnostics) } diff --git a/crates/sqlbuild-rules/src/lint_text/tests/test_lint_text.rs b/crates/sqlbuild-rules/src/lint_text/tests/test_lint_text.rs index 454ce07874..e6793bebb5 100644 --- a/crates/sqlbuild-rules/src/lint_text/tests/test_lint_text.rs +++ b/crates/sqlbuild-rules/src/lint_text/tests/test_lint_text.rs @@ -1,6 +1,9 @@ use crate::lint_text::_helpers::format_bodies::unwrapped_expression; use crate::lint_text::_helpers::sites::site_pattern; -use crate::lint_text::_helpers::sql_diagnostics::interpolation_text_at; +use crate::lint_text::_helpers::sql_diagnostics::{ + AuthoredFile, body_diagnostics, interpolation_text_at, response_diagnostics, + unavailable_once_per_file, +}; use crate::lint_text::constants::SITE_START_BYTES; use crate::lint_text::errors::LintTextError; use crate::lint_text::main::format_sql_bodies::format_sql_bodies; @@ -18,7 +21,7 @@ use crate::lint_text::tests::helpers::directive_finding; use crate::lint_text::tests::test_types::{ Finding, FormatBodiesTestCase, LintBodiesTestCase, NeutralizeTestCase, ParameterTestCase, RestoreTestCase, SitePatternTestCase, SlotReadTestCase, SuppressionTestCase, TokenAtTestCase, - UnwrappedExpressionTestCase, + UnavailableFinding, UnavailableLintTestCase, UnwrappedExpressionTestCase, }; use sqlbuild_analysis::sql_macros::main::parse_sql_macro::parse_sql_macro; @@ -666,3 +669,68 @@ fn given_sql_macro_bodies_when_linting_then_frames_and_generated_comments_are_no ); } } + +#[test] +fn given_engine_response_when_dialect_is_unavailable_then_each_file_gets_one_warning() { + const REASON: &str = + "SQL lint is unavailable for the tsql dialect: SQLQuack does not parse it yet"; + let test_cases = [ + UnavailableLintTestCase { + description: "an unavailable reason warns once at the file's first body", + response: serde_json::json!({"Ok": {"version": 1, "diagnostics": [], "unavailable": REASON}}), + bodies_in_file: 2, + expected_findings: vec![("L007", 2, 1, false, REASON)], + }, + UnavailableLintTestCase { + description: "a linted response without the key keeps its empty diagnostics", + response: serde_json::json!({"Ok": {"version": 1, "diagnostics": []}}), + bodies_in_file: 2, + expected_findings: Vec::new(), + }, + UnavailableLintTestCase { + description: "a non-string reason is an invalid response, not a clean one", + response: serde_json::json!({"Ok": {"version": 1, "diagnostics": [], "unavailable": null}}), + bodies_in_file: 1, + expected_findings: vec![( + "L003", + 2, + 1, + true, + "Native SQL parser could not analyze this body: native lint response has a non-string unavailable reason", + )], + }, + ]; + let file = AuthoredFile::new("MODEL ();\nselect a from t\n", "tsql"); + let body = LintSqlBody { + body_start: 10, + lint_text: "select a from t".to_string(), + ..LintSqlBody::default() + }; + + for test_case in test_cases { + let response = response_diagnostics(&test_case.response); + let diagnostics = unavailable_once_per_file( + (0..test_case.bodies_in_file) + .flat_map(|_| body_diagnostics(&response, (&body, None), &file)) + .collect(), + ); + let findings: Vec> = diagnostics + .iter() + .map(|finding| { + ( + finding.code.as_str(), + finding.line, + finding.column, + finding.fault, + finding.message.as_str(), + ) + }) + .collect(); + + assert_eq!( + findings, test_case.expected_findings, + "{}", + test_case.description + ); + } +} diff --git a/crates/sqlbuild-rules/src/lint_text/tests/test_types.rs b/crates/sqlbuild-rules/src/lint_text/tests/test_types.rs index 60c28b778c..da78f48bc7 100644 --- a/crates/sqlbuild-rules/src/lint_text/tests/test_types.rs +++ b/crates/sqlbuild-rules/src/lint_text/tests/test_types.rs @@ -56,6 +56,16 @@ pub(crate) struct LintBodiesTestCase { pub(crate) expected_findings: Vec>, } +pub(crate) struct UnavailableLintTestCase { + pub(crate) description: &'static str, + pub(crate) response: serde_json::Value, + pub(crate) bodies_in_file: usize, + pub(crate) expected_findings: Vec>, +} + +/// One authored finding: code, line, column, whether it is a fault, and its message. +pub(crate) type UnavailableFinding<'a> = (&'a str, i64, i64, bool, &'a str); + /// One mapped finding: code, line, column and optional end position. pub(crate) type Finding<'a> = (&'a str, i64, i64, Option<(i64, i64)>); From 15b863e92ce686daa2aca7c3a51c94f9886a9707 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sat, 7 Nov 2026 17:15:35 +0000 Subject: [PATCH 003/106] fix(sqlserver): quote schema names in EXEC, end MERGE with a semicolon and refuse WITH source expressions render_create_schema doubled quotes only in the sys.schemas lookup, so a schema name with a quote broke the EXEC string. render_merge now ends with the semicolon SQL Server requires. A source expression starting with WITH cannot sit in a derived table on SQL Server, so it now fails with A715 and a rewrite hint instead of a warehouse syntax error. --- .../sqlserver/classes/sqlserver_adapter.py | 15 ++++- .../adapters/sqlserver/_test_types.py | 8 +++ .../adapters/sqlserver/test_client.py | 55 ++++++++++++++++++- 3 files changed, 73 insertions(+), 5 deletions(-) diff --git a/src/sqlbuild/adapters/sqlserver/classes/sqlserver_adapter.py b/src/sqlbuild/adapters/sqlserver/classes/sqlserver_adapter.py index ee514dd37e..36a8d42f3f 100644 --- a/src/sqlbuild/adapters/sqlserver/classes/sqlserver_adapter.py +++ b/src/sqlbuild/adapters/sqlserver/classes/sqlserver_adapter.py @@ -320,9 +320,10 @@ def query(self, *, connection: Any, sql: str, limit: int | None) -> QueryResult: def render_create_schema(self, *, database: str | None, schema: str) -> tuple[str, ...]: del database escaped_schema: str = schema.replace("'", "''") + escaped_identifier: str = self.render_identifier(schema).replace("'", "''") return ( "IF NOT EXISTS (SELECT 1 FROM sys.schemas WHERE name = " - f"'{escaped_schema}') EXEC('CREATE SCHEMA {self.render_identifier(schema)}')", + f"'{escaped_schema}') EXEC('CREATE SCHEMA {escaped_identifier}')", ) def render_drop_schema(self, *, database: str | None, schema: str) -> tuple[str, ...]: @@ -2406,7 +2407,7 @@ def render_merge( ) if update_assignments: merge_sql += f"WHEN MATCHED THEN UPDATE SET {update_assignments} " - merge_sql += f"WHEN NOT MATCHED THEN INSERT ({insert_columns}) VALUES ({insert_values})" + merge_sql += f"WHEN NOT MATCHED THEN INSERT ({insert_columns}) VALUES ({insert_values});" return (merge_sql,) def render_qualified_name( @@ -2456,7 +2457,15 @@ def render_source_expression_relation(self, *, expression: str) -> str: if stripped_expression.startswith("("): return stripped_expression lowered: str = stripped_expression.lower() - if lowered.startswith(("select", "with", "values")): + if re.match(r"with\b", lowered): + raise AdapterUserError( + message="SQL Server cannot read a source expression that starts with WITH: " + "a common table expression cannot sit inside a derived table", + code="A715", + help="rewrite the source expression as one SELECT, using subqueries instead of " + "WITH, or define it as a view or model and read that", + ) + if lowered.startswith(("select", "values")): return f"({stripped_expression})" return stripped_expression diff --git a/tests/unit/src/sqlbuild/adapters/sqlserver/_test_types.py b/tests/unit/src/sqlbuild/adapters/sqlserver/_test_types.py index 2d3cd3b2b0..3992e76652 100644 --- a/tests/unit/src/sqlbuild/adapters/sqlserver/_test_types.py +++ b/tests/unit/src/sqlbuild/adapters/sqlserver/_test_types.py @@ -167,3 +167,11 @@ class SqlServerMigrationEventReadTestCase: description: str stored_created_at: object expected_read_suffix: str + + +@dataclass(frozen=True) +class SqlServerSourceExpressionRelationTestCase: + description: str + expression: str + expected_relation: str | None + expected_error_code: str | None diff --git a/tests/unit/src/sqlbuild/adapters/sqlserver/test_client.py b/tests/unit/src/sqlbuild/adapters/sqlserver/test_client.py index ec4b59a935..42ba596d2d 100644 --- a/tests/unit/src/sqlbuild/adapters/sqlserver/test_client.py +++ b/tests/unit/src/sqlbuild/adapters/sqlserver/test_client.py @@ -7,6 +7,7 @@ from sqlbuild._native import FunctionArgument from sqlbuild.adapter.contract.classes.statement_recorder import StatementRecorder +from sqlbuild.adapter.contract.exceptions import AdapterUserError from sqlbuild.adapter.contract.models import ( ColumnInfo, CursorValue, @@ -33,6 +34,7 @@ SqlServerRenderRenameTestCase, SqlServerRollbackTestCase, SqlServerRowDiffEqualitySqlTestCase, + SqlServerSourceExpressionRelationTestCase, ) from tests.unit.src.sqlbuild.adapters.sqlserver.helpers import FakeSqlServerConnection @@ -185,7 +187,7 @@ def test_given_cursor_type_when_rendering_delete_insert_then_sqlserver_uses_type expected_insert_clause=( "([tenant_id], [order_id], [updated_at], [status]) VALUES " "(__source.[tenant_id], __source.[order_id], __source.[updated_at], " - "__source.[status])" + "__source.[status]);" ), ) ], @@ -479,7 +481,15 @@ def test_given_sql_function_when_rendering_create_then_sqlserver_declares_functi "IF NOT EXISTS (SELECT 1 FROM sys.schemas WHERE name = 'analytics') " "EXEC('CREATE SCHEMA [analytics]')" ), - ) + ), + SqlServerRenderCreateSchemaTestCase( + description="doubles quotes in both the lookup and the EXEC string", + schema="sqb'meta]x", + expected_statement=( + "IF NOT EXISTS (SELECT 1 FROM sys.schemas WHERE name = 'sqb''meta]x') " + "EXEC('CREATE SCHEMA [sqb''meta]]x]')" + ), + ), ], ids=lambda case: case.description, ) @@ -876,3 +886,44 @@ def test_given_cross_schema_table_move_when_moving_then_sqlserver_uses_native_tr assert tuple(event.content for event in statement_recorder.snapshot()) == ( test_case.expected_statements ) + + +@pytest.mark.parametrize( + "test_case", + [ + SqlServerSourceExpressionRelationTestCase( + description="a SELECT becomes a derived table", + expression="SELECT id FROM raw.orders;", + expected_relation="(SELECT id FROM raw.orders)", + expected_error_code=None, + ), + SqlServerSourceExpressionRelationTestCase( + description="a relation whose name starts with with stays a relation", + expression="raw.with_orders", + expected_relation="raw.with_orders", + expected_error_code=None, + ), + SqlServerSourceExpressionRelationTestCase( + description="a WITH query is refused because SQL Server cannot nest it", + expression="WITH x AS (SELECT 1 AS id) SELECT id FROM x", + expected_relation=None, + expected_error_code="A715", + ), + ], + ids=lambda case: case.description, +) +def test_given_source_expression_when_rendering_relation_then_sqlserver_nests_only_what_it_can( + test_case: SqlServerSourceExpressionRelationTestCase, +) -> None: + adapter: SqlServerAdapter = SqlServerAdapter() + + try: + relation: str | None = adapter.render_source_expression_relation( + expression=test_case.expression + ) + error_code: str | None = None + except AdapterUserError as error: + relation = None + error_code = error.code + + assert (relation, error_code) == (test_case.expected_relation, test_case.expected_error_code) From 57b0c206fa1e38dd3ae159bcaeddfe8e82aea372 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sat, 7 Nov 2026 17:22:35 +0000 Subject: [PATCH 004/106] refactor(analysis): read B101 spellings from a recorded table instead of SQL library probes The unknown-function check (B101) asked the SQL library's parser which built-in node each unaccepted call spelling becomes, and took DuckDB's accepted names from the library's function catalogue. The check now reads a table of the 41 rejected spellings, each with its argument count and the spelling the dialect accepts. Every DuckDB row was confirmed unknown to DuckDB 2.0.0. The call scan takes the analysis dialect name, so analysis off the library can run it unchanged. Findings are identical on every DuckDB and Snowflake query of the compiler differential corpus and on every candidate spelling at 0 to 9 arguments. The workspace no longer depends on the library's function-catalogue crate directly; resolved features are unchanged. --- Cargo.lock | 1 - Cargo.toml | 3 - crates/sqlbuild-analysis/Cargo.toml | 1 - .../src/query_analysis/_helpers/engine.rs | 6 +- .../semantic_validation/_helpers/catalog.rs | 13 +- .../_helpers/diagnostics.rs | 7 +- .../_helpers/function_names.rs | 237 +---- .../_helpers/function_spellings.txt | 48 + .../_helpers/snowflake_functions.txt | 993 ------------------ .../main/map_diagnostics.rs | 4 +- .../src/semantic_validation/models.rs | 9 +- .../tests/test_function_names.rs | 48 +- .../tests/test_truth_tests.rs | 3 - .../semantic_validation/tests/test_types.rs | 2 +- .../src/semantic_validation/types.rs | 1 - 15 files changed, 121 insertions(+), 1255 deletions(-) create mode 100644 crates/sqlbuild-analysis/src/semantic_validation/_helpers/function_spellings.txt delete mode 100644 crates/sqlbuild-analysis/src/semantic_validation/_helpers/snowflake_functions.txt diff --git a/Cargo.lock b/Cargo.lock index 7306a408d1..74da5a92fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4126,7 +4126,6 @@ version = "0.0.0" dependencies = [ "memchr", "polyglot-sql", - "polyglot-sql-function-catalogs", "rayon", "regex", "rustc-hash", diff --git a/Cargo.toml b/Cargo.toml index 9ef850601d..af40942aa5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,9 +93,6 @@ polyglot-sql = { git = "https://github.com/sqlstack-io/polyglot.git", rev = "876 "semantic", "stacker", ] } -polyglot-sql-function-catalogs = { git = "https://github.com/sqlstack-io/polyglot.git", rev = "8767d688c5b504ad609645e27ab77a260d91c5a1", default-features = false, features = [ - "dialect-duckdb", -] } blake3 = { version = "1", features = ["rayon"] } # Only sqlbuild-build-features uses these: each enables in the sqb build exactly the features the # extension build resolves for that crate, so both builds share one compile of every common crate. diff --git a/crates/sqlbuild-analysis/Cargo.toml b/crates/sqlbuild-analysis/Cargo.toml index 864c22d7e7..6b0d2063c2 100644 --- a/crates/sqlbuild-analysis/Cargo.toml +++ b/crates/sqlbuild-analysis/Cargo.toml @@ -10,7 +10,6 @@ rust-version.workspace = true [dependencies] memchr.workspace = true polyglot-sql.workspace = true -polyglot-sql-function-catalogs.workspace = true rayon.workspace = true regex.workspace = true rustc-hash.workspace = true diff --git a/crates/sqlbuild-analysis/src/query_analysis/_helpers/engine.rs b/crates/sqlbuild-analysis/src/query_analysis/_helpers/engine.rs index ba731740dd..cde00841a5 100644 --- a/crates/sqlbuild-analysis/src/query_analysis/_helpers/engine.rs +++ b/crates/sqlbuild-analysis/src/query_analysis/_helpers/engine.rs @@ -550,12 +550,8 @@ fn analyze_compact_query_work( if let (Some(sql), Ok(dialect)) = (diagnostic_sql, dialect) && let Some(validation) = result.validation.take() { - let local_probes = crate::semantic_validation::models::FunctionProbes::default(); - let probes = catalog.map_or(&local_probes, |catalog| catalog.function_probes.as_ref()); result.validation = Some(validation.and_then(|value| { - crate::semantic_validation::main::map_diagnostics::map_diagnostics( - &sql, dialect, value, probes, - ) + crate::semantic_validation::main::map_diagnostics::map_diagnostics(&sql, dialect, value) })); } result diff --git a/crates/sqlbuild-analysis/src/semantic_validation/_helpers/catalog.rs b/crates/sqlbuild-analysis/src/semantic_validation/_helpers/catalog.rs index 32fd8e8198..9dae17ad2e 100644 --- a/crates/sqlbuild-analysis/src/semantic_validation/_helpers/catalog.rs +++ b/crates/sqlbuild-analysis/src/semantic_validation/_helpers/catalog.rs @@ -6,7 +6,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, PoisonError}; use crate::semantic_validation::models::{ - CatalogInput, Columns, CompactAnalysisJob, DiagnosticRow, FunctionProbes, ProjectCatalog, + CatalogInput, Columns, CompactAnalysisJob, DiagnosticRow, ProjectCatalog, }; use crate::semantic_validation::types::{ BindingRequest, NormalizationRequest, PreparedCompactAnalysis, Relations, @@ -160,7 +160,6 @@ impl ProjectCatalog { tables: HashMap::new(), overrides: Vec::new(), analysis_tables: HashMap::new(), - function_probes: Arc::new(FunctionProbes::default()), analysis_pool: Arc::default(), running_analyses: Arc::default(), }; @@ -182,7 +181,6 @@ impl ProjectCatalog { tables: self.tables.clone(), overrides: Vec::new(), analysis_tables: self.analysis_tables.clone(), - function_probes: Arc::new(FunctionProbes::default()), analysis_pool: Arc::clone(&self.analysis_pool), running_analyses: Arc::clone(&self.running_analyses), }; @@ -285,12 +283,7 @@ impl ProjectCatalog { .map(|(sql, references, overrides)| { let schema = self.schema(&references, overrides)?; let result = self.validate(&sql, &schema)?; - let result = diagnostics::map_diagnostics( - &sql, - self.dialect, - result, - &self.function_probes, - )?; + let result = diagnostics::map_diagnostics(&sql, self.dialect, result)?; Ok(result.errors.into_iter().map(diagnostic_row).collect()) }) .collect::, String>>() @@ -356,7 +349,6 @@ impl ProjectCatalog { tables: HashMap::new(), overrides: Vec::new(), analysis_tables: HashMap::new(), - function_probes: Arc::clone(&self.function_probes), analysis_pool: Arc::clone(&self.analysis_pool), running_analyses: Arc::clone(&self.running_analyses), } @@ -388,7 +380,6 @@ impl ProjectCatalog { tables: HashMap::new(), overrides: Vec::new(), analysis_tables: HashMap::new(), - function_probes: Arc::new(FunctionProbes::default()), analysis_pool: Arc::default(), running_analyses: Arc::default(), } diff --git a/crates/sqlbuild-analysis/src/semantic_validation/_helpers/diagnostics.rs b/crates/sqlbuild-analysis/src/semantic_validation/_helpers/diagnostics.rs index b198ae8a54..2b98a1f64e 100644 --- a/crates/sqlbuild-analysis/src/semantic_validation/_helpers/diagnostics.rs +++ b/crates/sqlbuild-analysis/src/semantic_validation/_helpers/diagnostics.rs @@ -6,7 +6,6 @@ use crate::semantic_validation::_helpers::sqlite_temporal_casts; use crate::semantic_validation::_helpers::truth_tests; use crate::semantic_validation::_helpers::type_locations::locate_type_findings; use crate::semantic_validation::models::DiagnosticRow; -use crate::semantic_validation::models::FunctionProbes; use polyglot_sql::expressions::Select; use polyglot_sql::{ Dialect, DialectType, Expression, ExpressionWalk, ValidationError, ValidationResult, @@ -149,7 +148,6 @@ pub(crate) fn map_diagnostics( sql: &str, dialect: DialectType, result: ValidationResult, - probes: &FunctionProbes, ) -> Result { let mut context = DiagnosticContext { sql, @@ -164,9 +162,7 @@ pub(crate) fn map_diagnostics( errors.push(error); } } - errors.extend(function_names::unsupported_function_errors( - sql, dialect, probes, - )); + errors.extend(function_names::unsupported_function_errors(sql, dialect)); errors.extend(truth_tests::unsupported_truth_test_errors(sql, dialect)); errors.extend(sqlite_temporal_casts::sqlite_temporal_cast_warnings( sql, dialect, @@ -418,7 +414,6 @@ pub(crate) fn binding_diagnostics( valid: false, errors, }, - &FunctionProbes::default(), )?; Ok(result.errors.into_iter().map(diagnostic_row).collect()) } diff --git a/crates/sqlbuild-analysis/src/semantic_validation/_helpers/function_names.rs b/crates/sqlbuild-analysis/src/semantic_validation/_helpers/function_names.rs index ca0337945e..fe9a00ead3 100644 --- a/crates/sqlbuild-analysis/src/semantic_validation/_helpers/function_names.rs +++ b/crates/sqlbuild-analysis/src/semantic_validation/_helpers/function_names.rs @@ -1,28 +1,17 @@ //! Built-in function spellings checked against the spellings the target dialect accepts. -use crate::semantic_validation::models::FunctionProbes; -use crate::semantic_validation::types::ProbeKey; -use polyglot_sql::{Dialect, DialectType, Expression, ValidationError}; -use polyglot_sql_function_catalogs::{CatalogSink, FunctionNameCase, FunctionSignature}; +use polyglot_sql::{DialectType, ValidationError}; use rustc_hash::{FxHashMap, FxHashSet}; use sqlbuild_sqltext::sql_scan::main::comment_end::comment_end; use sqlbuild_sqltext::sql_scan::main::non_code_end::non_code_end; use sqlbuild_sqltext::sql_scan::models::{QuotePolicy, Unclosed}; -use std::mem::Discriminant; -use std::sync::{LazyLock, OnceLock, PoisonError}; +use std::sync::LazyLock; const UNKNOWN_FUNCTION: &str = "B101"; -const SNOWFLAKE_FUNCTIONS: &str = include_str!("snowflake_functions.txt"); -const DUCKDB_CATALOG: &str = "duckdb"; -const PROBE_PREFIX: &str = "SELECT "; +const FUNCTION_SPELLINGS: &str = include_str!("function_spellings.txt"); +const SNOWFLAKE_DIALECT: &str = "snowflake"; +const DUCKDB_DIALECT: &str = "duckdb"; const TYPE_POSITION_KEYWORD: &str = "AS"; -const PROBE_ARITIES: usize = 9; -/// Keywords that may precede a parenthesis without being a function call. -const NON_CALL_KEYWORDS: &str = "ALL AND ANY AS ASOF AT BEFORE BETWEEN BY CASE CHANGES CONNECT CUBE \ - DEFINE DISTINCT DISTINCTROW ELSE END EXCEPT EXCLUDE EXISTS FILTER FROM HAVING ILIKE IN \ - INTERSECT INTERVAL IS JOIN LATERAL LIKE MATCH_CONDITION MATCH_RECOGNIZE MEASURES MINUS NOT ON \ - OR OVER PARTITION PATTERN PERCENT PIVOT PRIOR QUALIFY RANGE RENAME ROLLUP ROW ROWS SAMPLE \ - SELECT SETS SOME TABLE TABLESAMPLE THEN UNION UNPIVOT USING VALUES WHEN WHERE WITH WITHIN"; /// Words after which an expression, and so a function call, may start. const EXPRESSION_KEYWORDS: &str = "ALL AND ANY BETWEEN BY CASE DISTINCT ELSE ELSEIF ESCAPE HAVING \ @@ -37,10 +26,9 @@ const NOT_KEYWORD: &str = "NOT"; const MATERIALIZED_KEYWORD: &str = "MATERIALIZED"; const DISTINCT_KEYWORD: &str = "DISTINCT"; -type SpellingIndex = FxHashMap>; +/// Where one rejected spelling applies: the dialect, the argument count and the accepted spelling. +type SpellingRow = (&'static str, usize, &'static str); -static KEYWORDS: LazyLock> = - LazyLock::new(|| NON_CALL_KEYWORDS.split_whitespace().collect()); static EXPRESSION_STARTS: LazyLock> = LazyLock::new(|| EXPRESSION_KEYWORDS.split_whitespace().collect()); static CLAUSE_KEYWORDS: LazyLock> = @@ -49,17 +37,8 @@ static ROOT_FRAME: Frame = Frame { select_seen: false, clause: Clause::Other, }; -static SNOWFLAKE: LazyLock> = - LazyLock::new(|| accepted_names(SNOWFLAKE_FUNCTIONS)); -static DUCKDB: LazyLock> = LazyLock::new(|| { - let mut sink: CatalogNames = CatalogNames::default(); - polyglot_sql_function_catalogs::register_enabled_catalogs(&mut sink); - sink.names -}); -static SNOWFLAKE_INDEX: [OnceLock; PROBE_ARITIES] = - [const { OnceLock::new() }; PROBE_ARITIES]; -static DUCKDB_INDEX: [OnceLock; PROBE_ARITIES] = - [const { OnceLock::new() }; PROBE_ARITIES]; +static SPELLINGS: LazyLock>> = + LazyLock::new(|| spellings(FUNCTION_SPELLINGS)); /// One call site whose authored built-in spelling the dialect does not accept. #[derive(Debug, PartialEq, Eq)] @@ -95,21 +74,6 @@ struct Scan { path_colon: bool, } -/// How the parser represents one probe call. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -enum Signature { - /// A generic function node with this upper-case name. - Named(String), - /// A typed built-in node, identified by its expression variant. - Typed(Discriminant), -} - -/// Upper-case DuckDB names from the SQL library's introspected function catalogue. -#[derive(Default)] -struct CatalogNames { - names: FxHashSet, -} - /// Incremental byte-to-character position, so locating every call stays linear in the SQL. pub(crate) struct Cursor { byte: usize, @@ -118,29 +82,6 @@ pub(crate) struct Cursor { pub(crate) column: usize, } -impl CatalogSink for CatalogNames { - fn set_dialect_name_case(&mut self, _dialect: &'static str, _name_case: FunctionNameCase) {} - - fn set_function_name_case( - &mut self, - _dialect: &'static str, - _function_name: &str, - _name_case: FunctionNameCase, - ) { - } - - fn register( - &mut self, - dialect: &'static str, - function_name: &str, - _signatures: Vec, - ) { - if dialect == DUCKDB_CATALOG && is_identifier(function_name) { - self.names.insert(function_name.to_ascii_uppercase()); - } - } -} - impl Cursor { pub(crate) fn new() -> Self { Self { @@ -165,52 +106,50 @@ impl Cursor { } } -impl FunctionProbes { - /// Return the accepted spelling for an unaccepted call-site name the parser treats as built-in. - fn suggestion(&self, dialect: DialectType, upper: &str, arity: usize) -> Option { - let key: ProbeKey = (dialect, upper.to_owned(), arity.min(PROBE_ARITIES - 1)); - if let Some(cached) = self - .suggestions - .lock() - .unwrap_or_else(PoisonError::into_inner) - .get(&key) +/// Read `dialect SPELLING arguments ACCEPTED` rows; `#` starts a comment. +fn spellings(source: &'static str) -> FxHashMap<&'static str, Vec> { + let mut rows: FxHashMap<&'static str, Vec> = FxHashMap::default(); + for line in source.lines().map(str::trim) { + let mut fields = line.split_whitespace(); + if let (Some(dialect), Some(spelling), Some(arity), Some(accepted)) = + (fields.next(), fields.next(), fields.next(), fields.next()) + && !dialect.starts_with('#') + && let Ok(arity) = arity.parse::() { - return cached.clone(); + rows.entry(spelling) + .or_default() + .push((dialect, arity, accepted)); } - let found: Option = closest_spelling(dialect, upper, key.2); - self.suggestions - .lock() - .unwrap_or_else(PoisonError::into_inner) - .insert(key, found.clone()); - found } + rows } -fn accepted_names(source: &str) -> FxHashSet { - source - .lines() - .map(str::trim) - .filter(|line| !line.is_empty() && !line.starts_with('#')) - .map(str::to_ascii_uppercase) - .collect() +/// Return the accepted spelling for `upper` called with `arity` arguments in `dialect`. +fn accepted_spelling(dialect: &str, upper: &str, arity: usize) -> Option<&'static str> { + SPELLINGS + .get(upper)? + .iter() + .find_map(|(row_dialect, row_arity, accepted)| { + (*row_dialect == dialect && *row_arity == arity).then_some(*accepted) + }) } -fn accepted(dialect: DialectType) -> Option<&'static FxHashSet> { +/// The analysis dialect name the spelling table is keyed on, for the legacy validation path. +fn dialect_name(dialect: DialectType) -> Option<&'static str> { match dialect { - DialectType::Snowflake => Some(&SNOWFLAKE), - DialectType::DuckDB => Some(&DUCKDB), + DialectType::Snowflake => Some(SNOWFLAKE_DIALECT), + DialectType::DuckDB => Some(DUCKDB_DIALECT), _ => None, } } /// Return located B101 diagnostics, with character spans, for unsupported built-in spellings. -pub(crate) fn unsupported_function_errors( - sql: &str, - dialect: DialectType, - probes: &FunctionProbes, -) -> Vec { +pub(crate) fn unsupported_function_errors(sql: &str, dialect: DialectType) -> Vec { let mut cursor: Cursor = Cursor::new(); - unsupported_calls(sql, dialect, probes) + let Some(name) = dialect_name(dialect) else { + return Vec::new(); + }; + unsupported_calls(sql, name) .into_iter() .map(|call| { let message: String = format!( @@ -227,15 +166,8 @@ pub(crate) fn unsupported_function_errors( .collect() } -/// Return call sites in `sql` whose built-in spelling `dialect` does not accept. -pub(crate) fn unsupported_calls( - sql: &str, - dialect: DialectType, - probes: &FunctionProbes, -) -> Vec { - let Some(names) = accepted(dialect) else { - return Vec::new(); - }; +/// Return call sites in `sql` whose built-in spelling the analysis `dialect` does not accept. +pub(crate) fn unsupported_calls(sql: &str, dialect: &str) -> Vec { let bytes: &[u8] = sql.as_bytes(); let mut calls: Vec = Vec::new(); let mut scan: Scan = Scan::new(); @@ -277,18 +209,15 @@ pub(crate) fn unsupported_calls( let open: usize = skip_trivia(bytes, index); if bytes.get(open) == Some(&b'(') && scan.call_position() - && !name.starts_with("__") - && !names.contains(&upper) - && !KEYWORDS.contains(upper.as_str()) && let Some((arity, close)) = arguments(bytes, open) && !opens_cte_body(bytes, close + 1) - && let Some(suggestion) = probes.suggestion(dialect, &upper, arity) + && let Some(suggestion) = accepted_spelling(dialect, &upper, arity) { calls.push(UnsupportedCall { name: name.to_owned(), start, end: index, - suggestion, + suggestion: suggestion.to_owned(), }); } scan.word(upper); @@ -416,13 +345,6 @@ pub(crate) fn is_word_byte(byte: u8) -> bool { byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'$' } -fn is_identifier(name: &str) -> bool { - name.bytes() - .next() - .is_some_and(|byte| byte.is_ascii_alphabetic() || byte == b'_') - && name.bytes().all(is_word_byte) -} - /// Return the top-level argument count and closing index of the call opening at `open`. fn arguments(sql: &[u8], open: usize) -> Option<(usize, usize)> { let mut depth: usize = 0; @@ -463,76 +385,3 @@ fn arguments(sql: &[u8], open: usize) -> Option<(usize, usize)> { } None } - -/// Return the closest accepted spelling the parser maps to the same built-in node as `upper`. -fn closest_spelling(dialect: DialectType, upper: &str, arity: usize) -> Option { - let signature: Signature = parsed_signature(dialect, upper, arity)?; - if signature == Signature::Named(upper.to_owned()) { - return None; - } - spelling_index(dialect, arity)? - .get(&signature)? - .iter() - .min_by_key(|accepted| (edit_distance(upper, accepted), (*accepted).clone())) - .cloned() -} - -fn spelling_index(dialect: DialectType, arity: usize) -> Option<&'static SpellingIndex> { - let (names, indexes) = match dialect { - DialectType::Snowflake => (&*SNOWFLAKE, &SNOWFLAKE_INDEX), - DialectType::DuckDB => (&*DUCKDB, &DUCKDB_INDEX), - _ => return None, - }; - Some(indexes.get(arity)?.get_or_init(|| { - let mut index: SpellingIndex = FxHashMap::default(); - for name in names { - if let Some(signature) = parsed_signature(dialect, name, arity) { - index.entry(signature).or_default().push(name.clone()); - } - } - index - })) -} - -fn parsed_signature(dialect: DialectType, upper: &str, arity: usize) -> Option { - let arguments: Vec = (1..=arity).map(|index| format!("a{index}")).collect(); - let sql: String = format!("{PROBE_PREFIX}{upper}({})", arguments.join(", ")); - let Ok(statements) = Dialect::get(dialect).parse(&sql) else { - return None; - }; - let [Expression::Select(select)] = statements.as_slice() else { - return None; - }; - let [expression] = select.expressions.as_slice() else { - return None; - }; - match expression { - Expression::Function(function) => { - Some(Signature::Named(function.name.to_ascii_uppercase())) - } - Expression::AggregateFunction(function) => { - Some(Signature::Named(function.name.to_ascii_uppercase())) - } - Expression::Column(_) - | Expression::Identifier(_) - | Expression::Tuple(_) - | Expression::Paren(_) => None, - other => Some(Signature::Typed(std::mem::discriminant(other))), - } -} - -fn edit_distance(left: &str, right: &str) -> usize { - let right: Vec = right.chars().collect(); - let mut previous: Vec = (0..=right.len()).collect(); - for (row, left_char) in left.chars().enumerate() { - let mut current: Vec = vec![row + 1; right.len() + 1]; - for (column, right_char) in right.iter().enumerate() { - let substitution: usize = previous[column] + usize::from(left_char != *right_char); - current[column + 1] = substitution - .min(previous[column + 1] + 1) - .min(current[column] + 1); - } - previous = current; - } - previous[right.len()] -} diff --git a/crates/sqlbuild-analysis/src/semantic_validation/_helpers/function_spellings.txt b/crates/sqlbuild-analysis/src/semantic_validation/_helpers/function_spellings.txt new file mode 100644 index 0000000000..06b81043cd --- /dev/null +++ b/crates/sqlbuild-analysis/src/semantic_validation/_helpers/function_spellings.txt @@ -0,0 +1,48 @@ +# Built-in spellings SQLBuild's compile-time name check (B101) rejects, with the spelling the +# dialect accepts for the same function. A call is rejected only when its spelling and argument +# count appear here, so user-defined and unknown functions are never rejected. +# +# Columns: analysis dialect, upper-case spelling, argument count, accepted spelling. +# Every Snowflake spelling is absent from Snowflake's function reference; every DuckDB spelling +# is unknown to DuckDB 2.0.0 with its core extensions. Add a row only with that evidence. +snowflake CEILING 1 CEIL +snowflake CEILING 2 CEIL +snowflake CHARACTER_LENGTH 1 LENGTH +snowflake CHAR_LENGTH 1 LENGTH +snowflake COUNTIF 1 COUNT_IF +snowflake DATEPART 2 DATE_PART +snowflake DAY_OF_MONTH 1 DAYOFMONTH +snowflake DAY_OF_WEEK 1 DAYOFWEEK +snowflake DAY_OF_YEAR 1 DAYOFYEAR +snowflake ENDS_WITH 2 ENDSWITH +snowflake IF 2 IFF +snowflake IF 3 IFF +snowflake IIF 2 IFF +snowflake IIF 3 IFF +snowflake INSTR 2 POSITION +snowflake INSTR 3 POSITION +snowflake LCASE 1 LOWER +snowflake LOCATE 2 POSITION +snowflake LOCATE 3 POSITION +snowflake RAND 1 RANDOM +snowflake STARTS_WITH 2 STARTSWITH +snowflake STRPOS 2 POSITION +snowflake STRPOS 3 POSITION +snowflake TEXTLEN 1 LEN +snowflake TOTYPENAME 1 TYPEOF +snowflake UCASE 1 UPPER +snowflake WEEK_OF_YEAR 1 WEEKOFYEAR +duckdb DAY_OF_MONTH 1 DAYOFMONTH +duckdb DAY_OF_WEEK 1 DAYOFWEEK +duckdb DAY_OF_YEAR 1 DAYOFYEAR +duckdb ENDSWITH 2 ENDS_WITH +duckdb EVERY 1 BOOL_AND +duckdb LOCATE 2 INSTR +duckdb LOCATE 3 INSTR +duckdb LOGICAL_AND 1 BOOL_AND +duckdb LOGICAL_OR 1 BOOL_OR +duckdb RAND 1 RANDOM +duckdb STARTSWITH 2 STARTS_WITH +duckdb TEXTLEN 1 LEN +duckdb TOTYPENAME 1 TYPEOF +duckdb WEEK_OF_YEAR 1 WEEKOFYEAR diff --git a/crates/sqlbuild-analysis/src/semantic_validation/_helpers/snowflake_functions.txt b/crates/sqlbuild-analysis/src/semantic_validation/_helpers/snowflake_functions.txt deleted file mode 100644 index 6147565f61..0000000000 --- a/crates/sqlbuild-analysis/src/semantic_validation/_helpers/snowflake_functions.txt +++ /dev/null @@ -1,993 +0,0 @@ -# Snowflake built-in function spellings accepted by SQLBuild's compile-time name check (B101). -# -# Source: Snowflake's public "All functions (alphabetical)" reference, including every listed -# alias (for example SUBSTR and SUBSTRING, DATEDIFF, TIMEDIFF and TIMESTAMPDIFF, TRY_TO_DECIMAL, -# TRY_TO_NUMBER and TRY_TO_NUMERIC, STARTSWITH, ENDSWITH, CONTAINS, IFF), with wildcard families -# such as TO_TIMESTAMP_* and YEAR*/DAY*/WEEK* expanded, plus SYS_CONNECT_BY_PATH from the -# CONNECT BY reference. Names are case-insensitive; one name per line; `#` starts a comment. -# -# A spelling absent from this list is rejected only when the SQL parser maps it to the same -# built-in node as a listed spelling, so user-defined and unknown functions are never rejected. -# Add a missing Snowflake built-in here rather than suppressing the diagnostic. -ABS -ACCEPTED_VALUES -ACOS -ACOSH -ADD_MONTHS -AGENT_RUN -AGG -AI_AGG -AI_CLASSIFY -AI_COMPLETE -AI_COUNT_TOKENS -AI_EMBED -AI_EXTRACT -AI_FILTER -AI_MULTI_EMBED -AI_PARSE_DOCUMENT -AI_REDACT -AI_SENTIMENT -AI_SIMILARITY -AI_SUMMARIZE -AI_SUMMARIZE_AGG -AI_TRANSCRIBE -AI_TRANSLATE -ALERT_HISTORY -ALL_USER_NAMES -ANY_VALUE -APPLICATION_CALLBACK_HISTORY -APPLICATION_CONFIGURATION_VALUE_HISTORY -APPLICATION_JSON -APPLICATION_SPECIFICATION_STATUS_HISTORY -APPROXIMATE_JACCARD_INDEX -APPROXIMATE_SIMILARITY -APPROX_COUNT_DISTINCT -APPROX_PERCENTILE -APPROX_PERCENTILE_ACCUMULATE -APPROX_PERCENTILE_COMBINE -APPROX_PERCENTILE_ESTIMATE -APPROX_TOP_K -APPROX_TOP_K_ACCUMULATE -APPROX_TOP_K_COMBINE -APPROX_TOP_K_ESTIMATE -ARRAYS_OVERLAP -ARRAYS_TO_OBJECT -ARRAYS_ZIP -ARRAY_AGG -ARRAY_APPEND -ARRAY_CAT -ARRAY_COMPACT -ARRAY_CONSTRUCT -ARRAY_CONSTRUCT_COMPACT -ARRAY_CONSTRUCT_STRUCTURED -ARRAY_CONTAINS -ARRAY_DISTINCT -ARRAY_EXCEPT -ARRAY_FLATTEN -ARRAY_GENERATE_RANGE -ARRAY_INSERT -ARRAY_INTERSECTION -ARRAY_MAX -ARRAY_MIN -ARRAY_POSITION -ARRAY_PREPEND -ARRAY_REMOVE -ARRAY_REMOVE_AT -ARRAY_REVERSE -ARRAY_SIZE -ARRAY_SLICE -ARRAY_SORT -ARRAY_TO_STRING -ARRAY_UNION_AGG -ARRAY_UNIQUE_AGG -ASCII -ASIN -ASINH -AS_ARRAY -AS_BINARY -AS_BOOLEAN -AS_CHAR -AS_DATE -AS_DECIMAL -AS_DOUBLE -AS_INTEGER -AS_NUMBER -AS_OBJECT -AS_REAL -AS_TIME -AS_TIMESTAMP_LTZ -AS_TIMESTAMP_NTZ -AS_TIMESTAMP_TZ -AS_VARCHAR -ATAN -ATAN2 -ATANH -AUTOMATIC_CLUSTERING_HISTORY -AUTO_REFRESH_REGISTRATION_HISTORY -AVAILABLE_LISTINGS -AVAILABLE_LISTING_REFRESH_HISTORY -AVG -BASE64_DECODE_BINARY -BASE64_DECODE_STRING -BASE64_ENCODE -BETWEEN -BIND_VALUES -BITAND -BITAND_AGG -BITMAP_ABSOLUTE_POSITION -BITMAP_AND -BITMAP_AND_AGG -BITMAP_BIT_POSITION -BITMAP_BUCKET_NUMBER -BITMAP_CONSTRUCT_AGG -BITMAP_COUNT -BITMAP_OR -BITMAP_OR_AGG -BITMAP_TO_ARRAY -BITNOT -BITOR -BITOR_AGG -BITSHIFTLEFT -BITSHIFTRIGHT -BITXOR -BITXOR_AGG -BIT_LENGTH -BLANK_COUNT -BLANK_PERCENT -BOOLAND -BOOLAND_AGG -BOOLNOT -BOOLOR -BOOLOR_AGG -BOOLXOR -BOOLXOR_AGG -BUILD_SCOPED_FILE_URL -BUILD_STAGE_FILE_URL -CASE -CAST -CBRT -CEIL -CHAR -CHARINDEX -CHECK_JSON -CHECK_XML -CHR -CLASSIFY_TEXT -COALESCE -COLLATE -COLLATION -COMPLETE -COMPLETE_TASK_GRAPHS -COMPRESS -CONCAT -CONCAT_WS -CONDITIONAL_CHANGE_EVENT -CONDITIONAL_TRUE_EVENT -CONTAINS -CONVERT_TIMEZONE -COPY_HISTORY -CORR -CORTEX_SEARCH_BATCH -CORTEX_SEARCH_DATA_SCAN -CORTEX_SEARCH_REFRESH_HISTORY -COS -COSH -COT -COUNT -COUNT_IF -COUNT_TOKENS -COVAR_POP -COVAR_SAMP -CUME_DIST -CUMULATIVE_PRIVACY_LOSSES -CURRENT_ACCOUNT -CURRENT_ACCOUNT_NAME -CURRENT_AVAILABLE_ROLES -CURRENT_CLIENT -CURRENT_DATABASE -CURRENT_DATE -CURRENT_IP_ADDRESS -CURRENT_ORGANIZATION_NAME -CURRENT_ORGANIZATION_USER -CURRENT_REGION -CURRENT_ROLE -CURRENT_ROLE_TYPE -CURRENT_SCHEMA -CURRENT_SCHEMAS -CURRENT_SECONDARY_ROLES -CURRENT_SESSION -CURRENT_STATEMENT -CURRENT_TASK_GRAPHS -CURRENT_TIME -CURRENT_TIMESTAMP -CURRENT_TRANSACTION -CURRENT_USER -CURRENT_VERSION -CURRENT_WAREHOUSE -DATABASE_REFRESH_HISTORY -DATABASE_REFRESH_PROGRESS -DATABASE_REFRESH_PROGRESS_BY_JOB -DATABASE_REPLICATION_USAGE_HISTORY -DATABASE_STORAGE_USAGE_HISTORY -DATASKETCHES_HLL -DATASKETCHES_HLL_ACCUMULATE -DATASKETCHES_HLL_COMBINE -DATASKETCHES_HLL_ESTIMATE -DATA_AGENT_RUN -DATA_METRIC_FUNCTION_EXPECTATIONS -DATA_METRIC_FUNCTION_REFERENCES -DATA_METRIC_SCHEDULED_TIME -DATA_QUALITY_MONITORING_EXPECTATION_STATUS -DATA_QUALITY_MONITORING_RESULTS -DATA_TRANSFER_HISTORY -DATE -DATEADD -DATEDIFF -DATE_FROM_PARTS -DATE_PART -DATE_TRUNC -DAY -DAYNAME -DAYOFMONTH -DAYOFWEEK -DAYOFWEEKISO -DAYOFYEAR -DBT_PROJECT_EXECUTION_HISTORY -DECODE -DECOMPRESS_BINARY -DECOMPRESS_STRING -DECRYPT -DECRYPT_RAW -DEGREES -DENSE_RANK -DIV0 -DIV0NULL -DP_INTERVAL_HIGH -DP_INTERVAL_LOW -DUPLICATE_COUNT -DYNAMIC_TABLES -DYNAMIC_TABLE_GRAPH_HISTORY -DYNAMIC_TABLE_REFRESH_HISTORY -EDITDISTANCE -EMAIL_INTEGRATION_CONFIG -EMBED_TEXT_1024 -EMBED_TEXT_768 -ENCRYPT -ENCRYPT_RAW -ENDSWITH -ENTITY_SENTIMENT -EQUAL_NULL -ESTIMATE_REMAINING_DP_AGGREGATES -EXECUTE_AI_EVALUATION -EXP -EXPLAIN_JSON -EXTERNAL_FUNCTIONS_HISTORY -EXTERNAL_TABLE_FILES -EXTERNAL_TABLE_FILE_REGISTRATION_HISTORY -EXTRACT -EXTRACT_ANSWER -EXTRACT_SEMANTIC_CATEGORIES -EXTREME_OUTLIER_COUNT -EXTREME_OUTLIER_IQR_COUNT -EXTREME_OUTLIER_IQR_PERCENT -EXTREME_OUTLIER_PERCENT -EXTREME_OUTLIER_ZSCORE_COUNT -EXTREME_OUTLIER_ZSCORE_PERCENT -FACTORIAL -FILTER -FINETUNE -FIRST_VALUE -FLATTEN -FLOOR -FL_GET_CONTENT_TYPE -FL_GET_ETAG -FL_GET_FILE_TYPE -FL_GET_LAST_MODIFIED -FL_GET_RELATIVE_PATH -FL_GET_SCOPED_FILE_URL -FL_GET_SIZE -FL_GET_STAGE -FL_GET_STAGE_FILE_URL -FL_IS_AUDIO -FL_IS_COMPRESSED -FL_IS_DOCUMENT -FL_IS_IMAGE -FL_IS_VIDEO -FRESHNESS -GENERATE_COLUMN_DESCRIPTION -GENERATE_POSTGRES_ACCESS_TOKEN_FOR_USER -GENERATOR -GET -GETBIT -GETDATE -GETVARIABLE -GET_ABSOLUTE_PATH -GET_AI_EVALUATION_DATA -GET_AI_OBSERVABILITY_LOGS -GET_AI_RECORD_TRACE -GET_ANACONDA_PACKAGES_REPODATA -GET_CONDITION_QUERY_UUID -GET_CONFIGURATION_VALUE -GET_CONTACTS -GET_DDL -GET_IGNORE_CASE -GET_JOB_HISTORY -GET_LINEAGE -GET_OBJECT_REFERENCES -GET_PATH -GET_PRESIGNED_URL -GET_PYTHON_PROFILER_OUTPUT -GET_QUERY_OPERATOR_STATS -GET_RELATIVE_PATH -GET_STAGE_LOCATION -GREATEST -GREATEST_IGNORE_NULLS -GROUPING -GROUPING_ID -H3_CELL_TO_BOUNDARY -H3_CELL_TO_CHILDREN -H3_CELL_TO_CHILDREN_STRING -H3_CELL_TO_PARENT -H3_CELL_TO_POINT -H3_COMPACT_CELLS -H3_COMPACT_CELLS_STRINGS -H3_COVERAGE -H3_COVERAGE_STRINGS -H3_GET_RESOLUTION -H3_GRID_DISK -H3_GRID_DISTANCE -H3_GRID_PATH -H3_INT_TO_STRING -H3_IS_PENTAGON -H3_IS_VALID_CELL -H3_LATLNG_TO_CELL -H3_LATLNG_TO_CELL_STRING -H3_POINT_TO_CELL -H3_POINT_TO_CELL_STRING -H3_POLYGON_TO_CELLS -H3_POLYGON_TO_CELLS_STRINGS -H3_STRING_TO_INT -H3_TRY_COVERAGE -H3_TRY_COVERAGE_STRINGS -H3_TRY_GRID_DISTANCE -H3_TRY_GRID_PATH -H3_TRY_POLYGON_TO_CELLS -H3_TRY_POLYGON_TO_CELLS_STRINGS -H3_UNCOMPACT_CELLS -H3_UNCOMPACT_CELLS_STRINGS -HASH -HASH_AGG -HAVERSINE -HEX_DECODE_BINARY -HEX_DECODE_STRING -HEX_ENCODE -HLL -HLL_ACCUMULATE -HLL_COMBINE -HLL_ESTIMATE -HLL_EXPORT -HLL_IMPORT -HOUR -ICEBERG_TABLE_FILES -ICEBERG_TABLE_SNAPSHOT_REFRESH_HISTORY -IFF -IFNULL -ILIKE -IN -INFER_SCHEMA -INITCAP -INSERT -INTEGRATION -INTERPOLATE_BFILL -INTERPOLATE_FFILL -INTERPOLATE_LINEAR -INVOKER_ROLE -INVOKER_SHARE -IS_APPLICATION_ROLE_ACTIVATED -IS_APPLICATION_ROLE_IN_SESSION -IS_ARRAY -IS_BINARY -IS_BOOLEAN -IS_CHAR -IS_CONFIGURATION_SET -IS_DATABASE_ROLE_ACTIVATED -IS_DATABASE_ROLE_IN_SESSION -IS_DATE -IS_DATE_VALUE -IS_DECIMAL -IS_DOUBLE -IS_GRANTED_TO_INVOKER_ROLE -IS_GROUP_ACTIVATED -IS_GROUP_IMPORTED -IS_INSTANCE_ROLE_IN_SESSION -IS_INTEGER -IS_NULL_VALUE -IS_OBJECT -IS_ORGANIZATION_USER -IS_ORGANIZATION_USER_GROUP -IS_ORGANIZATION_USER_GROUP_IN_SESSION -IS_REAL -IS_ROLE_ACTIVATED -IS_ROLE_IN_SESSION -IS_TIME -IS_TIMESTAMP_LTZ -IS_TIMESTAMP_NTZ -IS_TIMESTAMP_TZ -IS_USER_IMPORTED -IS_VARCHAR -JAROWINKLER_SIMILARITY -JSON_EXTRACT_PATH_TEXT -KURTOSIS -LAG -LAST_DAY -LAST_QUERY_ID -LAST_SUCCESSFUL_SCHEDULED_TIME -LAST_TRANSACTION -LAST_VALUE -LEAD -LEAST -LEAST_IGNORE_NULLS -LEFT -LEN -LENGTH -LIKE -LISTAGG -LISTING_REFRESH_HISTORY -LN -LOCALTIME -LOCALTIMESTAMP -LOG -LOGIN_HISTORY -LOGIN_HISTORY_BY_USER -LOWER -LPAD -LTRIM -MAP_CAT -MAP_CONSTRUCT -MAP_CONTAINS_KEY -MAP_DELETE -MAP_ENTRIES -MAP_INSERT -MAP_KEYS -MAP_PICK -MAP_SIZE -MATERIALIZED_VIEW_REFRESH_HISTORY -MAX -MAX_BY -MD5 -MD5_BINARY -MD5_HEX -MD5_NUMBER_LOWER64 -MD5_NUMBER_UPPER64 -MEDIAN -MIN -MINHASH -MINHASH_COMBINE -MINUTE -MIN_BY -MOD -MODE -MODEL_MONITOR_DRIFT_METRIC -MODEL_MONITOR_PERFORMANCE_METRIC -MODEL_MONITOR_STAT_METRIC -MONTH -MONTHNAME -MONTHS_BETWEEN -NETWORK_RULE_REFERENCES -NEXT_DAY -NORMAL -NOTIFICATION_HISTORY -NTH_VALUE -NTILE -NULLIF -NULLIFZERO -NULL_COUNT -NULL_PERCENT -NVL -NVL2 -OBJECT_AGG -OBJECT_CONSTRUCT -OBJECT_CONSTRUCT_KEEP_NULL -OBJECT_CONSTRUCT_KEEP_NULL_STRUCTURED -OBJECT_DELETE -OBJECT_INSERT -OBJECT_KEYS -OBJECT_PICK -OCTET_LENGTH -ONLINE_FEATURE_TABLE_REFRESH_HISTORY -OUTLIER_COUNT -OUTLIER_IQR_COUNT -OUTLIER_IQR_PERCENT -OUTLIER_PERCENT -OUTLIER_ZSCORE_COUNT -OUTLIER_ZSCORE_PERCENT -PARSE_DOCUMENT -PARSE_IP -PARSE_JSON -PARSE_URL -PARSE_XML -PERCENTILE_CONT -PERCENTILE_DISC -PERCENT_RANK -PI -PIPE_USAGE_HISTORY -POLICY_CONTEXT -POLICY_REFERENCES -POSITION -POW -POWER -PREVIOUS_DAY -PROMPT -QUARTER -QUERY_ACCELERATION_HISTORY -QUERY_HISTORY -RADIANS -RANDOM -RANDSTR -RANK -RATIO_TO_REPORT -REDUCE -REGEXP -REGEXP_COUNT -REGEXP_INSTR -REGEXP_LIKE -REGEXP_REPLACE -REGEXP_SUBSTR -REGEXP_SUBSTR_ALL -REGR_AVGX -REGR_AVGY -REGR_COUNT -REGR_INTERCEPT -REGR_R2 -REGR_SLOPE -REGR_SXX -REGR_SXY -REGR_SYY -REGR_VALX -REGR_VALY -REPEAT -REPLACE -REPLICATION_GROUP_DANGLING_REFERENCES -REPLICATION_GROUP_REFRESH_HISTORY -REPLICATION_GROUP_REFRESH_HISTORY_ALL -REPLICATION_GROUP_REFRESH_PROGRESS -REPLICATION_GROUP_REFRESH_PROGRESS_ALL -REPLICATION_GROUP_REFRESH_PROGRESS_BY_JOB -REPLICATION_GROUP_USAGE_HISTORY -REPLICATION_USAGE_HISTORY -REST_EVENT_HISTORY -RESULT_SCAN -REVERSE -RIGHT -RLIKE -ROUND -ROW_COUNT -ROW_NUMBER -RPAD -RTRIM -RTRIMMED_LENGTH -SANITIZE_WEBHOOK_CONTENT -SCHEDULED_TIME -SEARCH -SEARCH_IP -SEARCH_OPTIMIZATION_HISTORY -SEARCH_PREVIEW -SECOND -SENTIMENT -SEQ1 -SEQ2 -SEQ4 -SEQ8 -SERVERLESS_ALERT_HISTORY -SERVERLESS_TASK_HISTORY -SET_SYS_CONTEXT -SHA1 -SHA1_BINARY -SHA1_HEX -SHA2 -SHA2_BINARY -SHA2_HEX -SHOW_PYTHON_PACKAGES_DEPENDENCIES -SIGN -SIN -SINH -SKEW -SOUNDEX -SOUNDEX_P123 -SPACE -SPLIT -SPLIT_PART -SPLIT_TEXT_MARKDOWN_HEADER -SPLIT_TEXT_RECURSIVE_CHARACTER -SPLIT_TO_TABLE -SQRT -SQUARE -STAGE_DIRECTORY_FILE_REGISTRATION_HISTORY -STAGE_STORAGE_USAGE_HISTORY -STARTSWITH -STDDEV -STDDEV_POP -STDDEV_SAMP -STORAGE_LIFECYCLE_POLICY_HISTORY -STRIP_NULL_VALUE -STRTOK -STRTOK_SPLIT_TO_TABLE -STRTOK_TO_ARRAY -ST_AREA -ST_ASBINARY -ST_ASEWKB -ST_ASEWKT -ST_ASGEOJSON -ST_ASTEXT -ST_ASWKB -ST_ASWKT -ST_AZIMUTH -ST_BUFFER -ST_CENTROID -ST_COLLECT -ST_CONTAINS -ST_COVEREDBY -ST_COVERS -ST_DIFFERENCE -ST_DIMENSION -ST_DISJOINT -ST_DISTANCE -ST_DWITHIN -ST_ENDPOINT -ST_ENVELOPE -ST_GEOGFROMGEOHASH -ST_GEOGPOINTFROMGEOHASH -ST_GEOGRAPHYFROMWKB -ST_GEOGRAPHYFROMWKT -ST_GEOHASH -ST_GEOMETRYFROMWKB -ST_GEOMETRYFROMWKT -ST_GEOMFROMGEOHASH -ST_GEOMPOINT -ST_GEOMPOINTFROMGEOHASH -ST_HAUSDORFFDISTANCE -ST_INTERPOLATE -ST_INTERSECTION -ST_INTERSECTION_AGG -ST_INTERSECTS -ST_ISVALID -ST_LENGTH -ST_MAKEGEOMPOINT -ST_MAKELINE -ST_MAKEPOINT -ST_MAKEPOLYGON -ST_MAKEPOLYGONORIENTED -ST_NPOINTS -ST_NUMPOINTS -ST_PERIMETER -ST_POINT -ST_POINTN -ST_POLYGON -ST_SETSRID -ST_SIMPLIFY -ST_SRID -ST_STARTPOINT -ST_SYMDIFFERENCE -ST_TRANSFORM -ST_UNION -ST_UNION_AGG -ST_WITHIN -ST_X -ST_XMAX -ST_XMIN -ST_Y -ST_YMAX -ST_YMIN -SUBSTR -SUBSTRING -SUM -SYSDATE -SYSTEM$ABORT_SESSION -SYSTEM$ABORT_TRANSACTION -SYSTEM$ACTIVATE_CMK_INFO -SYSTEM$ACTIVATE_CMK_INFO_POSTGRES -SYSTEM$ADD_EVENT -SYSTEM$ADD_REFERENCE -SYSTEM$ALLOWLIST -SYSTEM$ALLOWLIST_PRIVATELINK -SYSTEM$APPLICATION_GET_LOG_LEVEL -SYSTEM$APPLICATION_GET_METRIC_LEVEL -SYSTEM$APPLICATION_GET_TRACE_LEVEL -SYSTEM$APP_COMPATIBILITY_CHECK -SYSTEM$AUTHORIZE_PRIVATELINK -SYSTEM$AUTHORIZE_STAGE_PRIVATELINK_ACCESS -SYSTEM$AUTO_REFRESH_STATUS -SYSTEM$BEGIN_DEBUG_APPLICATION -SYSTEM$BEHAVIOR_CHANGE_BUNDLE_STATUS -SYSTEM$BLOCK_INTERNAL_STAGES_PUBLIC_ACCESS -SYSTEM$BLOCK_INTERNAL_STAGES_PUBLIC_ACCESS_WITH_EXCEPTION -SYSTEM$CANCEL_ALL_QUERIES -SYSTEM$CANCEL_QUERY -SYSTEM$CATALOG_LINK_STATUS -SYSTEM$CKE_HASH_FUNCTION -SYSTEM$CLEANUP_DATABASE_ROLE_GRANTS -SYSTEM$CLIENT_VERSION_INFO -SYSTEM$CLIENT_VULNERABILITY_INFO -SYSTEM$CLUSTERING_DEPTH -SYSTEM$CLUSTERING_INFORMATION -SYSTEM$COMMIT_MOVE_ORGANIZATION_ACCOUNT -SYSTEM$CONVERT_PIPES_SQS_TO_SNS -SYSTEM$CREATE_BILLING_EVENT -SYSTEM$CREATE_BILLING_EVENTS -SYSTEM$CURRENT_USER_TASK_NAME -SYSTEM$DATABASE_REFRESH_PROGRESS -SYSTEM$DATA_METRIC_SCAN -SYSTEM$DEACTIVATE_CMK_INFO -SYSTEM$DECODE_PAT -SYSTEM$DEPROVISION_PRIVATELINK_ENDPOINT -SYSTEM$DEPROVISION_PRIVATELINK_ENDPOINT_TSS -SYSTEM$DEREGISTER_CMK_INFO -SYSTEM$DEREGISTER_CMK_INFO_POSTGRES -SYSTEM$DESC_ICEBERG_ACCESS_IDENTITY -SYSTEM$DISABLE_BEHAVIOR_CHANGE_BUNDLE -SYSTEM$DISABLE_DATABASE_REPLICATION -SYSTEM$DISABLE_GLOBAL_DATA_SHARING_FOR_ACCOUNT -SYSTEM$DISABLE_PREVIEW_ACCESS -SYSTEM$DISABLE_PRIVATELINK_ACCESS_ONLY -SYSTEM$ENABLE_BEHAVIOR_CHANGE_BUNDLE -SYSTEM$ENABLE_GLOBAL_DATA_SHARING_FOR_ACCOUNT -SYSTEM$ENABLE_PREVIEW_ACCESS -SYSTEM$ENCODE_CKE_PRIMARY_KEY -SYSTEM$END_DEBUG_APPLICATION -SYSTEM$ENFORCE_PRIVATELINK_ACCESS_ONLY -SYSTEM$ESTIMATE_AUTOMATIC_CLUSTERING_COSTS -SYSTEM$ESTIMATE_QUERY_ACCELERATION -SYSTEM$ESTIMATE_SEARCH_OPTIMIZATION_COSTS -SYSTEM$EVALUATE_DATA_QUALITY_EXPECTATIONS -SYSTEM$EVALUATE_DATA_QUALITY_EXPECTATIONS_PERSIST_RESULT -SYSTEM$EXPLAIN_JSON_TO_TEXT -SYSTEM$EXPLAIN_PLAN_JSON -SYSTEM$EXPORT_TDS_FROM_SEMANTIC_VIEW -SYSTEM$EXTERNAL_TABLE_PIPE_STATUS -SYSTEM$FINISH_OAUTH_FLOW -SYSTEM$GENERATE_SAML_CSR -SYSTEM$GENERATE_SCIM_ACCESS_TOKEN -SYSTEM$GET_ALL_DEFAULT_COLUMNS_OVERRIDES -SYSTEM$GET_ALL_REFERENCES -SYSTEM$GET_AWS_SNS_IAM_POLICY -SYSTEM$GET_CATALOG_LINKED_DATABASE_CONFIG -SYSTEM$GET_CLASSIFICATION_RESULT -SYSTEM$GET_CMK_AKV_CONSENT_URL -SYSTEM$GET_CMK_CONFIG -SYSTEM$GET_CMK_CONFIG_POSTGRES -SYSTEM$GET_CMK_INFO -SYSTEM$GET_CMK_INFO_POSTGRES -SYSTEM$GET_CMK_KMS_KEY_POLICY -SYSTEM$GET_COMPUTE_POOL_PENDING_MAINTENANCE -SYSTEM$GET_DBT_LOG -SYSTEM$GET_DEBUG_STATUS -SYSTEM$GET_DEFAULT_COLUMNS_OVERRIDE_FOR_SHOW_COMMAND -SYSTEM$GET_DEFAULT_COLUMNS_OVERRIDE_FOR_SYSTEM_OBJECT -SYSTEM$GET_DIRECTORY_TABLE_STATUS -SYSTEM$GET_GCP_KMS_CMK_GRANT_ACCESS_CMD -SYSTEM$GET_HASH_FOR_APPLICATION -SYSTEM$GET_ICEBERG_TABLE_INFORMATION -SYSTEM$GET_INSTANCE_FAMILY_PLACEMENT_GROUPS -SYSTEM$GET_LOGIN_FAILURE_DETAILS -SYSTEM$GET_PREDECESSOR_RETURN_VALUE -SYSTEM$GET_PREVIEW_ACCESS_STATUS -SYSTEM$GET_PRIVATELINK -SYSTEM$GET_PRIVATELINK_AUTHORIZED_ENDPOINTS -SYSTEM$GET_PRIVATELINK_CONFIG -SYSTEM$GET_PRIVATELINK_ENDPOINTS_INFO -SYSTEM$GET_PRIVATELINK_ENDPOINT_REGISTRATIONS -SYSTEM$GET_PURCHASE_ATTRIBUTES -SYSTEM$GET_REFERENCED_OBJECT_ID_HASH -SYSTEM$GET_RESULTSET_STATUS -SYSTEM$GET_SERVICE_DNS_DOMAIN -SYSTEM$GET_SERVICE_LOGS -SYSTEM$GET_SNOWFLAKE_EGRESS_IP_RANGES -SYSTEM$GET_SNOWFLAKE_PLATFORM_INFO -SYSTEM$GET_STAGE_PRIVATELINK_AUTHORIZED_ENDPOINTS -SYSTEM$GET_TAG -SYSTEM$GET_TAG_ALLOWED_VALUES -SYSTEM$GET_TAG_ON_CURRENT_COLUMN -SYSTEM$GET_TAG_ON_CURRENT_TABLE -SYSTEM$GET_TASK_GRAPH_CONFIG -SYSTEM$GLOBAL_ACCOUNT_SET_PARAMETER -SYSTEM$HOLD_PRIVILEGE_ON_ACCOUNT -SYSTEM$INITIATE_MOVE_ORGANIZATION_ACCOUNT -SYSTEM$INTERNAL_STAGES_PUBLIC_ACCESS_STATUS -SYSTEM$IS_APPLICATION_ALL_MANDATORY_TELEMETRY_EVENT_DEFINITIONS_ENABLED -SYSTEM$IS_APPLICATION_AUTHORIZED_FOR_TELEMETRY_EVENT_SHARING -SYSTEM$IS_APPLICATION_INSTALLED_FROM_SAME_ACCOUNT -SYSTEM$IS_APPLICATION_SHARING_EVENTS_WITH_PROVIDER -SYSTEM$IS_GLOBAL_DATA_SHARING_ENABLED_FOR_ACCOUNT -SYSTEM$IS_LISTING_PURCHASED -SYSTEM$IS_LISTING_TRIAL -SYSTEM$LAST_CHANGE_COMMIT_TIME -SYSTEM$LINK_ACCOUNT_OBJECTS_BY_NAME -SYSTEM$LINK_ORGANIZATION_USER -SYSTEM$LINK_ORGANIZATION_USER_GROUP -SYSTEM$LIST_APPLICATION_RESTRICTED_FEATURES -SYSTEM$LIST_ICEBERG_TABLES_FROM_CATALOG -SYSTEM$LIST_NAMESPACES_FROM_CATALOG -SYSTEM$LOCATE_DBT_ARCHIVE -SYSTEM$LOCATE_DBT_ARTIFACTS -SYSTEM$LOG -SYSTEM$MIGRATE_SAML_IDP_REGISTRATION -SYSTEM$OPT_IN_INTERNAL_STAGE_NETWORK_LOGS -SYSTEM$OPT_OUT_INTERNAL_STAGE_NETWORK_LOGS -SYSTEM$OPT_OUT_MALICIOUS_IP_PROTECTION_BY_CATEGORY -SYSTEM$PIPE_FORCE_RESUME -SYSTEM$PIPE_REBINDING_WITH_NOTIFICATION_CHANNEL -SYSTEM$PIPE_STATUS -SYSTEM$PROVISION_PRIVATELINK_ENDPOINT -SYSTEM$PROVISION_PRIVATELINK_ENDPOINT_TSS -SYSTEM$QUERY_REFERENCE -SYSTEM$READ_OSI_YAML_FROM_SEMANTIC_VIEW -SYSTEM$READ_YAML_FROM_SEMANTIC_VIEW -SYSTEM$REFERENCE -SYSTEM$REGISTER_CMK_INFO -SYSTEM$REGISTER_CMK_INFO_POSTGRES -SYSTEM$REGISTER_PRIVATELINK_ENDPOINT -SYSTEM$REMOVE_ALL_REFERENCES -SYSTEM$REMOVE_REFERENCE -SYSTEM$REPORT_HEALTH_STATUS -SYSTEM$RESOLVE_PYTHON_PACKAGES -SYSTEM$RESTORE_PRIVATELINK_ENDPOINT -SYSTEM$RESTORE_PRIVATELINK_ENDPOINT_TSS -SYSTEM$REVOKE_PRIVATELINK -SYSTEM$REVOKE_STAGE_PRIVATELINK_ACCESS -SYSTEM$SCHEDULE_ASYNC_REPLICATION_GROUP_REFRESH -SYSTEM$SEND_NOTIFICATIONS_TO_CATALOG -SYSTEM$SET_APPLICATION_RESTRICTED_FEATURE_ACCESS -SYSTEM$SET_CATALOG_INTEGRATION -SYSTEM$SET_DEFAULT_COLUMNS_OVERRIDE_FOR_SHOW_COMMAND -SYSTEM$SET_DEFAULT_COLUMNS_OVERRIDE_FOR_SYSTEM_OBJECT -SYSTEM$SET_EVENT_SHARING_ACCOUNT_FOR_REGION -SYSTEM$SET_PRIVATELINK_ENDPOINT_HOSTNAME -SYSTEM$SET_REFERENCE -SYSTEM$SET_RETURN_VALUE -SYSTEM$SET_ROW_TIMESTAMP_ON_ALL_SUPPORTED_TABLES -SYSTEM$SET_SPAN_ATTRIBUTES -SYSTEM$SHOW_ACTIVE_BEHAVIOR_CHANGE_BUNDLES -SYSTEM$SHOW_BUDGETS_FOR_RESOURCE -SYSTEM$SHOW_BUDGETS_IN_ACCOUNT -SYSTEM$SHOW_EVENT_SHARING_ACCOUNTS -SYSTEM$SHOW_MOVE_ORGANIZATION_ACCOUNT_STATUS -SYSTEM$SHOW_OAUTH_CLIENT_SECRETS -SYSTEM$SHOW_SENSITIVE_DATA_MONITORED_ENTITIES -SYSTEM$SNOWPIPE_STREAMING_UPDATE_CHANNEL_OFFSET_TOKEN -SYSTEM$START_OAUTH_FLOW -SYSTEM$START_USER_EMAIL_VERIFICATION -SYSTEM$STREAM_BACKLOG -SYSTEM$STREAM_GET_TABLE_TIMESTAMP -SYSTEM$STREAM_HAS_DATA -SYSTEM$SUPPORTED_DBT_VERSIONS -SYSTEM$TASK_DEPENDENTS_ENABLE -SYSTEM$TASK_RUNTIME_INFO -SYSTEM$TRIGGER_LISTING_REFRESH -SYSTEM$TYPEOF -SYSTEM$UNBLOCK_INTERNAL_STAGES_PUBLIC_ACCESS -SYSTEM$UNLINK_ORGANIZATION_USER -SYSTEM$UNLINK_ORGANIZATION_USER_GROUP -SYSTEM$UNREGISTER_PRIVATELINK_ENDPOINT -SYSTEM$UNSET_DEFAULT_COLUMNS_OVERRIDE_FOR_SHOW_COMMAND -SYSTEM$UNSET_DEFAULT_COLUMNS_OVERRIDE_FOR_SYSTEM_OBJECT -SYSTEM$UNSET_EVENT_SHARING_ACCOUNT_FOR_REGION -SYSTEM$USER_TASK_CANCEL_ONGOING_EXECUTIONS -SYSTEM$VALIDATE_STORAGE_INTEGRATION -SYSTEM$VERIFY_CATALOG_INTEGRATION -SYSTEM$VERIFY_CMK_INFO -SYSTEM$VERIFY_CMK_INFO_POSTGRES -SYSTEM$VERIFY_EXTERNAL_OAUTH_TOKEN -SYSTEM$VERIFY_EXTERNAL_VOLUME -SYSTEM$WAIT -SYSTEM$WAIT_FOR_SERVICES -SYSTIMESTAMP -SYS_CONNECT_BY_PATH -SYS_CONTEXT -TAG_REFERENCES -TAG_REFERENCES_ALL_COLUMNS -TAG_REFERENCES_WITH_LINEAGE -TAN -TANH -TASK_DEPENDENTS -TASK_HISTORY -TEXT_HTML -TEXT_PLAIN -TIME -TIMEADD -TIMEDIFF -TIMESTAMPADD -TIMESTAMPDIFF -TIMESTAMP_FROM_PARTS -TIME_FROM_PARTS -TIME_SLICE -TO_ARRAY -TO_BINARY -TO_BOOLEAN -TO_CHAR -TO_DATE -TO_DECFLOAT -TO_DECIMAL -TO_DOUBLE -TO_FILE -TO_GEOGRAPHY -TO_GEOMETRY -TO_JSON -TO_NUMBER -TO_NUMERIC -TO_OBJECT -TO_QUERY -TO_TIME -TO_TIMESTAMP -TO_TIMESTAMP_LTZ -TO_TIMESTAMP_NTZ -TO_TIMESTAMP_TZ -TO_UUID -TO_VARCHAR -TO_VARIANT -TO_XML -TRANSFORM -TRANSLATE -TRIM -TRUNC -TRUNCATE -TRY_BASE64_DECODE_BINARY -TRY_BASE64_DECODE_STRING -TRY_CAST -TRY_COMPLETE -TRY_DECRYPT -TRY_DECRYPT_RAW -TRY_HEX_DECODE_BINARY -TRY_HEX_DECODE_STRING -TRY_PARSE_JSON -TRY_TO_BINARY -TRY_TO_BOOLEAN -TRY_TO_DATE -TRY_TO_DECFLOAT -TRY_TO_DECIMAL -TRY_TO_DOUBLE -TRY_TO_FILE -TRY_TO_GEOGRAPHY -TRY_TO_GEOMETRY -TRY_TO_NUMBER -TRY_TO_NUMERIC -TRY_TO_TIME -TRY_TO_TIMESTAMP -TRY_TO_TIMESTAMP_LTZ -TRY_TO_TIMESTAMP_NTZ -TRY_TO_TIMESTAMP_TZ -TRY_TO_UUID -TYPEOF -UNICODE -UNIFORM -UNIQUE_COUNT -UPPER -UUID_STRING -VALIDATE -VALIDATE_PIPE_LOAD -VARIANCE -VARIANCE_POP -VARIANCE_SAMP -VAR_POP -VAR_SAMP -VECTOR_AVG -VECTOR_COSINE_SIMILARITY -VECTOR_INNER_PRODUCT -VECTOR_L1_DISTANCE -VECTOR_L2_DISTANCE -VECTOR_MAX -VECTOR_MIN -VECTOR_NORMALIZE -VECTOR_SUM -VECTOR_TRUNCATE -WAREHOUSE_LOAD_HISTORY -WAREHOUSE_METERING_HISTORY -WEEK -WEEKISO -WEEKOFYEAR -WIDTH_BUCKET -XMLGET -YEAR -YEAROFWEEK -YEAROFWEEKISO -ZEROIFNULL -ZIPF diff --git a/crates/sqlbuild-analysis/src/semantic_validation/main/map_diagnostics.rs b/crates/sqlbuild-analysis/src/semantic_validation/main/map_diagnostics.rs index 73e179774e..75bfd83774 100644 --- a/crates/sqlbuild-analysis/src/semantic_validation/main/map_diagnostics.rs +++ b/crates/sqlbuild-analysis/src/semantic_validation/main/map_diagnostics.rs @@ -1,11 +1,9 @@ -use crate::semantic_validation::models::FunctionProbes; use polyglot_sql::{DialectType, ValidationResult}; pub(crate) fn map_diagnostics( sql: &str, dialect: DialectType, result: ValidationResult, - probes: &FunctionProbes, ) -> Result { - crate::semantic_validation::_helpers::diagnostics::map_diagnostics(sql, dialect, result, probes) + crate::semantic_validation::_helpers::diagnostics::map_diagnostics(sql, dialect, result) } diff --git a/crates/sqlbuild-analysis/src/semantic_validation/models.rs b/crates/sqlbuild-analysis/src/semantic_validation/models.rs index a24345c77a..a6f184a32c 100644 --- a/crates/sqlbuild-analysis/src/semantic_validation/models.rs +++ b/crates/sqlbuild-analysis/src/semantic_validation/models.rs @@ -1,6 +1,6 @@ //! Compile-owned native catalog, mapping state and boundary requests. -use crate::semantic_validation::types::{PreparedCompactAnalysis, ProbeKey, Relations}; +use crate::semantic_validation::types::{PreparedCompactAnalysis, Relations}; use polyglot_sql::validation::SchemaTable; use polyglot_sql::{DialectType, SchemaValidationOptions}; use rayon::ThreadPool; @@ -65,12 +65,6 @@ pub struct PositionInput { pub passes: Vec>, } -/// Memoized call-site function-name probes, owned by the catalog that validates a compile. -#[derive(Debug, Default)] -pub(crate) struct FunctionProbes { - pub(crate) suggestions: Mutex>>, -} - pub struct ProjectCatalog { pub(crate) dialect: DialectType, pub(crate) options: SchemaValidationOptions, @@ -78,7 +72,6 @@ pub struct ProjectCatalog { pub(super) tables: HashMap, pub(super) overrides: Vec>, pub(super) analysis_tables: HashMap, - pub(crate) function_probes: Arc, /// One analysis pool per compile catalog, built on first use and shared by its views. pub(super) analysis_pool: Arc>>>, /// Analysis batches running on the pool; preparation only borrows the pool while none run. diff --git a/crates/sqlbuild-analysis/src/semantic_validation/tests/test_function_names.rs b/crates/sqlbuild-analysis/src/semantic_validation/tests/test_function_names.rs index 11698e22b1..9003dcfa2a 100644 --- a/crates/sqlbuild-analysis/src/semantic_validation/tests/test_function_names.rs +++ b/crates/sqlbuild-analysis/src/semantic_validation/tests/test_function_names.rs @@ -1,6 +1,5 @@ use crate::semantic_validation::_helpers::diagnostics::map_diagnostics; use crate::semantic_validation::_helpers::function_names::unsupported_calls; -use crate::semantic_validation::models::FunctionProbes; use crate::semantic_validation::tests::test_types::{ LocatedFunctionDiagnosticTestCase, UnsupportedFunctionTestCase, }; @@ -11,25 +10,25 @@ fn given_unsupported_builtin_spelling_when_scanning_then_reports_dialect_spellin let test_cases = [ UnsupportedFunctionTestCase { description: "Snowflake STARTS_WITH suggests STARTSWITH", - dialect: DialectType::Snowflake, + dialect: "snowflake", sql: "SELECT STARTS_WITH(product_name, 'a') AS flagged FROM products", expected_calls: &[("STARTS_WITH", "STARTSWITH")], }, UnsupportedFunctionTestCase { description: "matching is case-insensitive and keeps the authored spelling", - dialect: DialectType::Snowflake, + dialect: "snowflake", sql: "select ends_with(product_name, 'a'), lcase(product_name) from products", expected_calls: &[("ends_with", "ENDSWITH"), ("lcase", "LOWER")], }, UnsupportedFunctionTestCase { description: "conditional and length aliases suggest the Snowflake spelling", - dialect: DialectType::Snowflake, + dialect: "snowflake", sql: "SELECT IF(quantity > 0, 1, 0), CHAR_LENGTH(product_name) FROM products", expected_calls: &[("IF", "IFF"), ("CHAR_LENGTH", "LENGTH")], }, UnsupportedFunctionTestCase { description: "calls inside windows, lambdas and table functions are checked", - dialect: DialectType::Snowflake, + dialect: "snowflake", sql: "SELECT SUM(quantity) OVER (PARTITION BY UCASE(category)), \ FILTER(tags, tag -> STARTS_WITH(tag, 'a')) \ FROM products, TABLE(FLATTEN(input => ARRAY_CONSTRUCT(LCASE(sku))))", @@ -41,26 +40,26 @@ fn given_unsupported_builtin_spelling_when_scanning_then_reports_dialect_spellin }, UnsupportedFunctionTestCase { description: "an object literal value after a string key is checked", - dialect: DialectType::Snowflake, + dialect: "snowflake", sql: "SELECT {'featured': STARTS_WITH(product_name, 'a')} FROM products", expected_calls: &[("STARTS_WITH", "STARTSWITH")], }, UnsupportedFunctionTestCase { description: "a MATCH_RECOGNIZE DEFINE condition after AS is checked", - dialect: DialectType::Snowflake, + dialect: "snowflake", sql: "SELECT * FROM products MATCH_RECOGNIZE (ORDER BY sku PATTERN (featured) \ DEFINE featured AS STARTS_WITH(product_name, 'a'))", expected_calls: &[("STARTS_WITH", "STARTSWITH")], }, UnsupportedFunctionTestCase { description: "comments between the name and its parenthesis are skipped", - dialect: DialectType::Snowflake, + dialect: "snowflake", sql: "SELECT LCASE /* lower */ (sku), UCASE -- upper\n (sku) FROM products", expected_calls: &[("LCASE", "LOWER"), ("UCASE", "UPPER")], }, UnsupportedFunctionTestCase { description: "operand FROM and select lists inside a FROM subquery are checked", - dialect: DialectType::Snowflake, + dialect: "snowflake", sql: "SELECT TRIM(BOTH 'x' FROM LCASE(sku)), sku IS DISTINCT FROM UCASE(sku) \ FROM (SELECT sku, LCASE(sku) AS lowered FROM products) AS listed, catalog \ WHERE sku IN (1, 2) AND LCASE(sku) = 'a'", @@ -73,19 +72,19 @@ fn given_unsupported_builtin_spelling_when_scanning_then_reports_dialect_spellin }, UnsupportedFunctionTestCase { description: "a call followed by AS NOT without MATERIALIZED is still checked", - dialect: DialectType::DuckDB, + dialect: "duckdb", sql: "SELECT STARTSWITH(product_name, 'a') AS not_featured FROM products", expected_calls: &[("STARTSWITH", "STARTS_WITH")], }, UnsupportedFunctionTestCase { description: "DuckDB STARTSWITH suggests the catalogue spelling", - dialect: DialectType::DuckDB, + dialect: "duckdb", sql: "SELECT STARTSWITH(product_name, 'a') FROM products", expected_calls: &[("STARTSWITH", "STARTS_WITH")], }, ]; for test_case in &test_cases { - let calls = unsupported_calls(test_case.sql, test_case.dialect, &FunctionProbes::default()); + let calls = unsupported_calls(test_case.sql, test_case.dialect); let observed: Vec<(&str, &str)> = calls .iter() .map(|call| { @@ -109,7 +108,7 @@ fn given_supported_or_non_builtin_call_when_scanning_then_reports_nothing() { let test_cases = [ UnsupportedFunctionTestCase { description: "listed Snowflake synonyms are accepted", - dialect: DialectType::Snowflake, + dialect: "snowflake", sql: "SELECT STARTSWITH(a, 'x'), ENDSWITH(a, 'x'), CONTAINS(a, 'x'), SUBSTR(a, 1, 2), \ SUBSTRING(a, 1, 2), TIMESTAMPDIFF(day, b, c), DATEDIFF(day, b, c), \ TRY_TO_DECIMAL(a, 10, 2), TRY_TO_NUMBER(a), IFF(d, 1, 0), LEN(a), \ @@ -119,14 +118,14 @@ fn given_supported_or_non_builtin_call_when_scanning_then_reports_nothing() { }, UnsupportedFunctionTestCase { description: "quoted identifiers, literals and comments are not call sites", - dialect: DialectType::Snowflake, + dialect: "snowflake", sql: "SELECT \"STARTS_WITH\"(a, 'x'), 'STARTS_WITH(a, b)', $$LCASE(a)$$ \ -- STARTS_WITH(a, 'x')\n /* LCASE(a) */ FROM products", expected_calls: &[], }, UnsupportedFunctionTestCase { description: "methods, qualified functions, sentinels and macros are not checked", - dialect: DialectType::Snowflake, + dialect: "snowflake", sql: "SELECT tools.starts_with(a, 'x'), analytics.helpers.lcase(a), \ __sqlbuild_udf_starts_with(a, 'x'), @py.starts_with(a), payload:lcase(a) \ FROM __ref(\"products\")", @@ -134,13 +133,13 @@ fn given_supported_or_non_builtin_call_when_scanning_then_reports_nothing() { }, UnsupportedFunctionTestCase { description: "unknown and user-defined function names are never flagged", - dialect: DialectType::Snowflake, + dialect: "snowflake", sql: "SELECT normalize_sku(a), DATE_ADD(day, 1, b), my_udf() FROM products", expected_calls: &[], }, UnsupportedFunctionTestCase { description: "type positions and keywords before parentheses are not calls", - dialect: DialectType::Snowflake, + dialect: "snowflake", sql: "WITH ranked (sku, position_rank) AS (SELECT DISTINCT (a), \ ROW_NUMBER() OVER (ORDER BY b) FROM products WHERE NOT (a IN (1, 2)) \ AND EXISTS (SELECT 1)) \ @@ -149,14 +148,14 @@ fn given_supported_or_non_builtin_call_when_scanning_then_reports_nothing() { }, UnsupportedFunctionTestCase { description: "CTE column lists are not calls", - dialect: DialectType::Snowflake, + dialect: "snowflake", sql: "WITH listed AS (SELECT 1 AS dow), day_of_week (dow) AS (SELECT 1), \ lcase(x) AS (SELECT 2) SELECT dow FROM day_of_week", expected_calls: &[], }, UnsupportedFunctionTestCase { description: "materialized CTE column lists are not calls", - dialect: DialectType::DuckDB, + dialect: "duckdb", sql: "WITH day_of_week (d) AS MATERIALIZED (SELECT 1), \ day_of_month (d) AS /* hint */ NOT -- keep\n MATERIALIZED (SELECT 2) \ SELECT * FROM day_of_week", @@ -164,7 +163,7 @@ fn given_supported_or_non_builtin_call_when_scanning_then_reports_nothing() { }, UnsupportedFunctionTestCase { description: "derived-table, table-function and table aliases are not calls", - dialect: DialectType::Snowflake, + dialect: "snowflake", sql: "SELECT x FROM (VALUES (1)) day_of_week (x), products lcase (y), \ TABLE(FLATTEN(input => tags)) ucase (z), day_of_month (w) \ JOIN char_length (v) ON TRUE", @@ -172,27 +171,27 @@ fn given_supported_or_non_builtin_call_when_scanning_then_reports_nothing() { }, UnsupportedFunctionTestCase { description: "INSERT and CREATE column lists are not calls", - dialect: DialectType::Snowflake, + dialect: "snowflake", sql: "CREATE TABLE day_of_week (dow INTEGER); CREATE VIEW lcase (x) AS SELECT 1; \ INSERT INTO ucase (x) SELECT 1; CREATE TABLE IF NOT EXISTS char_length (x INT)", expected_calls: &[], }, UnsupportedFunctionTestCase { description: "dialects without a spelling table are not checked", - dialect: DialectType::PostgreSQL, + dialect: "postgres", sql: "SELECT STARTS_WITH(a, 'x'), LCASE(a) FROM products", expected_calls: &[], }, UnsupportedFunctionTestCase { description: "DuckDB catalogue spellings are accepted", - dialect: DialectType::DuckDB, + dialect: "duckdb", sql: "SELECT starts_with(a, 'x'), prefix(a, 'x'), lcase(a), substr(a, 1, 2) \ FROM products", expected_calls: &[], }, ]; for test_case in &test_cases { - let calls = unsupported_calls(test_case.sql, test_case.dialect, &FunctionProbes::default()); + let calls = unsupported_calls(test_case.sql, test_case.dialect); let observed: Vec<(&str, &str)> = calls .iter() .map(|call| (call.name.as_str(), call.suggestion.as_str())) @@ -225,7 +224,6 @@ fn given_unsupported_spelling_when_mapping_diagnostics_then_returns_located_b101 valid: true, errors: Vec::new(), }, - &FunctionProbes::default(), )?; let [error] = result.errors.as_slice() else { return Err(format!("{}: {:?}", test_case.description, result.errors)); diff --git a/crates/sqlbuild-analysis/src/semantic_validation/tests/test_truth_tests.rs b/crates/sqlbuild-analysis/src/semantic_validation/tests/test_truth_tests.rs index f9ba73f68f..6b9865dd9b 100644 --- a/crates/sqlbuild-analysis/src/semantic_validation/tests/test_truth_tests.rs +++ b/crates/sqlbuild-analysis/src/semantic_validation/tests/test_truth_tests.rs @@ -1,6 +1,5 @@ use crate::semantic_validation::_helpers::diagnostics::map_diagnostics; use crate::semantic_validation::_helpers::truth_tests::{rejects_truth_tests, truth_tests}; -use crate::semantic_validation::models::FunctionProbes; use crate::semantic_validation::tests::test_types::{ LocatedTruthTestDiagnosticTestCase, TruthTestDialectTestCase, TruthTestScanTestCase, }; @@ -91,7 +90,6 @@ fn given_dialect_when_checking_truth_tests_then_only_rejecting_warehouses_report valid: true, errors: Vec::new(), }, - &FunctionProbes::default(), ) .expect(test_case.description); assert_eq!( @@ -162,7 +160,6 @@ fn given_rejected_truth_test_when_mapping_diagnostics_then_returns_located_b103( valid: true, errors: Vec::new(), }, - &FunctionProbes::default(), )?; let [error] = result.errors.as_slice() else { return Err(format!("{}: {:?}", test_case.description, result.errors)); diff --git a/crates/sqlbuild-analysis/src/semantic_validation/tests/test_types.rs b/crates/sqlbuild-analysis/src/semantic_validation/tests/test_types.rs index 201a5dc6fc..7fcc899e20 100644 --- a/crates/sqlbuild-analysis/src/semantic_validation/tests/test_types.rs +++ b/crates/sqlbuild-analysis/src/semantic_validation/tests/test_types.rs @@ -12,7 +12,7 @@ pub(crate) struct SemanticValidationBatchTestCase { pub(crate) struct UnsupportedFunctionTestCase { pub(crate) description: &'static str, - pub(crate) dialect: polyglot_sql::DialectType, + pub(crate) dialect: &'static str, pub(crate) sql: &'static str, pub(crate) expected_calls: &'static [(&'static str, &'static str)], } diff --git a/crates/sqlbuild-analysis/src/semantic_validation/types.rs b/crates/sqlbuild-analysis/src/semantic_validation/types.rs index 8ab94dd7a8..02480b2f00 100644 --- a/crates/sqlbuild-analysis/src/semantic_validation/types.rs +++ b/crates/sqlbuild-analysis/src/semantic_validation/types.rs @@ -5,7 +5,6 @@ pub type Relations = HashMap; pub type BindingRequest = (String, Vec<(String, bool)>, Relations); /// One analysis SQL with its relation stubs and placeholder defaults. pub type NormalizationRequest = (String, HashMap, HashMap); -pub(crate) type ProbeKey = (polyglot_sql::DialectType, String, usize); /// Compact analysis whose catalog schemas are resolved; it runs with an options-only catalog. pub(crate) type PreparedCompactAnalysis = Box Result + Send>; From 7edf70df8b270b874363bb3ef9635e193ccd7303 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sat, 7 Nov 2026 18:34:13 +0000 Subject: [PATCH 005/106] feat(lint): keep L007 out of every exit decision and show it in compile and rules run compile and rules run kept only selected Rule codes, so an unavailable dialect looked clean there. L007 now passes through as a warning-severity Rule diagnostic in compile and is left out of rules run's failing-finding count. counts_toward_exit owns the exemption. --- .../_helpers/compile_driver/analysis.rs | 12 +++++-- .../bindings/_helpers/plan_command/rules.rs | 8 ++++- .../src/bindings/_helpers/rules/sql_rules.rs | 5 ++- .../src/lint_text/main/counts_toward_exit.rs | 8 +++++ .../sqlbuild-rules/src/lint_text/main/mod.rs | 1 + .../src/lint_text/tests/test_lint_text.rs | 36 +++++++++++++++++-- .../src/lint_text/tests/test_types.rs | 6 ++++ .../references/docs/concepts/rules.md | 5 +++ .../src/content/docs/docs/concepts/rules.mdx | 5 +++ 9 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 crates/sqlbuild-rules/src/lint_text/main/counts_toward_exit.rs diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/analysis.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/analysis.rs index eaabced465..e7fd6c7603 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/analysis.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/analysis.rs @@ -6,6 +6,7 @@ use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyRes use pyo3::types::{PyBool, PyDict, PyDictMethods, PyString, PyTuple, PyTupleMethods}; use pyo3::{IntoPyObject, pyfunction, wrap_pyfunction}; use sqlbuild_host::entries::models::ExecutorHost; +use sqlbuild_rules::lint_text::main::counts_toward_exit::counts_toward_exit; use crate::bindings::_helpers::boundary::python_calls::{ Status, call_with, imported, keywords, within, @@ -546,7 +547,7 @@ impl<'py> Driver<'_, 'py> { } } -/// The core diagnostics followed by one rule-phase error per rule finding. +/// The core diagnostics followed by one rule-phase error per rule finding; lint-unavailable ones only warn. fn with_rule_diagnostics<'py>( py: Python<'py>, core: &Bound<'py, PyTuple>, @@ -554,6 +555,7 @@ fn with_rule_diagnostics<'py>( ) -> PyResult> { let phase: Bound<'py, PyAny> = enum_member(py, RecordId::DiagnosticPhase, "RULE")?; let severity: Bound<'py, PyAny> = enum_member(py, RecordId::DiagnosticSeverity, "ERROR")?; + let unavailable: Bound<'py, PyAny> = enum_member(py, RecordId::DiagnosticSeverity, "WARNING")?; let fix_note: Bound<'py, PyAny> = PyString::new(py, RULE_FIX_AVAILABLE_NOTE).into_any(); let mut diagnostics: Vec> = core.iter().collect(); for fault in native_field(rules_result, "findings")?.try_iter()? { @@ -568,7 +570,13 @@ fn with_rule_diagnostics<'py>( RecordId::CompilerDiagnostic, &[ ("phase", phase.clone()), - ("severity", severity.clone()), + ( + "severity", + match counts_toward_exit(&native_field(&fault, "code")?.extract::()?) { + true => severity.clone(), + false => unavailable.clone(), + }, + ), ("code", native_field(&fault, "code")?), ("message", native_field(&fault, "message")?), ("path", native_field(&fault, "path")?), diff --git a/crates/sqlbuild-python/src/bindings/_helpers/plan_command/rules.rs b/crates/sqlbuild-python/src/bindings/_helpers/plan_command/rules.rs index 19fc6051ec..36081f97c2 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/plan_command/rules.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/plan_command/rules.rs @@ -5,6 +5,7 @@ use pyo3::prelude::{ }; use pyo3::types::{PyDict, PyDictMethods, PyTuple}; use pyo3::wrap_pyfunction; +use sqlbuild_rules::lint_text::main::counts_toward_exit::counts_toward_exit; use sqlbuild_rules::rule_catalogue::main::render_rule_list::render_rule_list; use sqlbuild_rules::rule_catalogue::models::RuleListing; @@ -246,7 +247,12 @@ fn run_rule_selection( native_field(request, "json_output")?.is_truthy()?, )?; print_line(py, &text, false)?; - let findings: usize = native_field(&result, "findings")?.len()?; + let mut findings: usize = 0; + for finding in native_field(&result, "findings")?.try_iter()? { + if counts_toward_exit(&native_field(&finding?, "code")?.extract::()?) { + findings += 1; + } + } if findings > 0 { print_line( py, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/rules/sql_rules.rs b/crates/sqlbuild-python/src/bindings/_helpers/rules/sql_rules.rs index e211ebcefa..a404881bf2 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/rules/sql_rules.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/rules/sql_rules.rs @@ -13,6 +13,7 @@ use pyo3::prelude::{ use pyo3::types::{PyBytes, PyDict, PyDictMethods, PyString, PyTuple}; use pyo3::{IntoPyObjectExt, pyfunction, wrap_pyfunction}; use sqlbuild_core::text::main::python_splitlines::python_splitlines; +use sqlbuild_rules::lint_text::main::counts_toward_exit::counts_toward_exit; use sqlbuild_rules::rule_cache::main::file_rule_key::file_rule_key; use sqlbuild_rules::rule_results::main::duplicate_literal_hints::duplicate_literal_hints; use sqlbuild_rules::rule_results::models::RuleFinding; @@ -670,7 +671,7 @@ fn lint_expansion_context( }) } -/// The findings of the selected SQL rules' violations; an engine failure leaves each unevaluated. +/// The selected SQL rules' findings and lint-unavailable warnings; an engine failure leaves each rule unevaluated. fn violation_findings( run: &SqlRun<'_, '_>, violations: &[(PathBuf, LintViolationRow)], @@ -686,6 +687,8 @@ fn violation_findings( let code: &str = &violation.code; if code == SQL_RULE_SUPPRESSION_CODE || run.codes.iter().any(|known| known == code) { evaluated.push(lint_finding((path, violation), &resolved_dir, model_paths)); + } else if !counts_toward_exit(code) { + evaluated.push(lint_finding((path, violation), &resolved_dir, &no_paths)); } else if code == engine_error { let finding: RuleFinding = lint_finding((path, violation), &resolved_dir, &no_paths); for rule_code in run.codes { diff --git a/crates/sqlbuild-rules/src/lint_text/main/counts_toward_exit.rs b/crates/sqlbuild-rules/src/lint_text/main/counts_toward_exit.rs new file mode 100644 index 0000000000..6fcb69d909 --- /dev/null +++ b/crates/sqlbuild-rules/src/lint_text/main/counts_toward_exit.rs @@ -0,0 +1,8 @@ +//! `counts_toward_exit`: whether a lint finding can make a command fail. + +use crate::lint_text::constants::LINT_UNAVAILABLE_CODE; + +/// Whether a finding with `code` counts toward a command's exit status; SQL lint being unavailable for a dialect never does. +pub fn counts_toward_exit(code: &str) -> bool { + code != LINT_UNAVAILABLE_CODE +} diff --git a/crates/sqlbuild-rules/src/lint_text/main/mod.rs b/crates/sqlbuild-rules/src/lint_text/main/mod.rs index 3343ef6e90..b3640027a2 100644 --- a/crates/sqlbuild-rules/src/lint_text/main/mod.rs +++ b/crates/sqlbuild-rules/src/lint_text/main/mod.rs @@ -1,3 +1,4 @@ +pub mod counts_toward_exit; pub mod defined_cte_names; pub mod dependency_sites; pub mod first_header_only; diff --git a/crates/sqlbuild-rules/src/lint_text/tests/test_lint_text.rs b/crates/sqlbuild-rules/src/lint_text/tests/test_lint_text.rs index e6793bebb5..0142637b30 100644 --- a/crates/sqlbuild-rules/src/lint_text/tests/test_lint_text.rs +++ b/crates/sqlbuild-rules/src/lint_text/tests/test_lint_text.rs @@ -6,6 +6,7 @@ use crate::lint_text::_helpers::sql_diagnostics::{ }; use crate::lint_text::constants::SITE_START_BYTES; use crate::lint_text::errors::LintTextError; +use crate::lint_text::main::counts_toward_exit::counts_toward_exit; use crate::lint_text::main::format_sql_bodies::format_sql_bodies; use crate::lint_text::main::frame_sql_macro_expression::frame_sql_macro_expression; use crate::lint_text::main::lint_sql_bodies::lint_sql_bodies; @@ -19,9 +20,9 @@ use crate::lint_text::main::restore_interpolation::restore_interpolation; use crate::lint_text::models::{ExpansionSpan, FormatFile, LintSqlBody, LintSqlOptions}; use crate::lint_text::tests::helpers::directive_finding; use crate::lint_text::tests::test_types::{ - Finding, FormatBodiesTestCase, LintBodiesTestCase, NeutralizeTestCase, ParameterTestCase, - RestoreTestCase, SitePatternTestCase, SlotReadTestCase, SuppressionTestCase, TokenAtTestCase, - UnavailableFinding, UnavailableLintTestCase, UnwrappedExpressionTestCase, + ExitCountTestCase, Finding, FormatBodiesTestCase, LintBodiesTestCase, NeutralizeTestCase, + ParameterTestCase, RestoreTestCase, SitePatternTestCase, SlotReadTestCase, SuppressionTestCase, + TokenAtTestCase, UnavailableFinding, UnavailableLintTestCase, UnwrappedExpressionTestCase, }; use sqlbuild_analysis::sql_macros::main::parse_sql_macro::parse_sql_macro; @@ -734,3 +735,32 @@ fn given_engine_response_when_dialect_is_unavailable_then_each_file_gets_one_war ); } } + +#[test] +fn given_findings_when_deciding_exit_then_only_lint_unavailable_never_fails_a_command() { + let test_cases = [ + ExitCountTestCase { + description: "an unavailable-dialect warning reports our gap, not the user's SQL", + code: "L007", + expected_counts: false, + }, + ExitCountTestCase { + description: "a SQL lint rule finding still fails", + code: "SQLCLEAN005", + expected_counts: true, + }, + ExitCountTestCase { + description: "an engine fault still fails", + code: "L003", + expected_counts: true, + }, + ]; + for test_case in test_cases { + assert_eq!( + counts_toward_exit(test_case.code), + test_case.expected_counts, + "{}", + test_case.description + ); + } +} diff --git a/crates/sqlbuild-rules/src/lint_text/tests/test_types.rs b/crates/sqlbuild-rules/src/lint_text/tests/test_types.rs index da78f48bc7..94f883b554 100644 --- a/crates/sqlbuild-rules/src/lint_text/tests/test_types.rs +++ b/crates/sqlbuild-rules/src/lint_text/tests/test_types.rs @@ -56,6 +56,12 @@ pub(crate) struct LintBodiesTestCase { pub(crate) expected_findings: Vec>, } +pub(crate) struct ExitCountTestCase { + pub(crate) description: &'static str, + pub(crate) code: &'static str, + pub(crate) expected_counts: bool, +} + pub(crate) struct UnavailableLintTestCase { pub(crate) description: &'static str, pub(crate) response: serde_json::Value, diff --git a/src/sqlbuild/.agents/skills/sqlbuild/references/docs/concepts/rules.md b/src/sqlbuild/.agents/skills/sqlbuild/references/docs/concepts/rules.md index 09d9a86da7..aa7890c26d 100644 --- a/src/sqlbuild/.agents/skills/sqlbuild/references/docs/concepts/rules.md +++ b/src/sqlbuild/.agents/skills/sqlbuild/references/docs/concepts/rules.md @@ -144,6 +144,11 @@ These checks run through ordinary compilation. There is intentionally no separat lifecycle. If an SQL requirement affects whether the project is acceptable, the compiler enforces it with the rest of the configured Rules. +If SQL Rules cannot run because SQLBuild does not parse the project's dialect yet, `sqb compile`, +`sqb rules run` and `sqb format` show one `L007` warning per file that names the dialect. `L007` +never changes an exit code: it reports a gap in SQLBuild's dialect support, not a problem in your +SQL, so the file's SQL Rules simply did not run. + Not every traditional lint concern should become a diagnostic. SQLBuild separates three kinds of SQL responsibility: diff --git a/website/src/content/docs/docs/concepts/rules.mdx b/website/src/content/docs/docs/concepts/rules.mdx index 8f761a9ee0..0ea8a21f5c 100644 --- a/website/src/content/docs/docs/concepts/rules.mdx +++ b/website/src/content/docs/docs/concepts/rules.mdx @@ -132,6 +132,11 @@ These checks run through ordinary compilation. There is intentionally no separat lifecycle. If an SQL requirement affects whether the project is acceptable, the compiler enforces it with the rest of the configured Rules. +If SQL Rules cannot run because SQLBuild does not parse the project's dialect yet, `sqb compile`, +`sqb rules run` and `sqb format` show one `L007` warning per file that names the dialect. `L007` +never changes an exit code: it reports a gap in SQLBuild's dialect support, not a problem in your +SQL, so the file's SQL Rules simply did not run. + Not every traditional lint concern should become a diagnostic. SQLBuild separates three kinds of SQL responsibility: From 2fb45477f311e8defc7da3e134fee9f277f89e63 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sat, 7 Nov 2026 18:55:08 +0000 Subject: [PATCH 006/106] fix(lint): keep L007 out of rule exceptions and the plan rules gate The plan and build rules gate failed on any finding, so an L007-only project could not plan. Rule exceptions also validate every finding's code as a rule code and rejected L007 with an invalid-lifecycle error. The gate now counts only findings that count toward exit, and L007 passes through rule exceptions unsuppressed. An end-to-end test replays a cached finding as L007 and as a rule code through compile, rules run, plan and build. --- .../_helpers/compile_driver/plan_pipeline.rs | 14 ++++- .../src/engine/_helpers/evaluation.rs | 6 ++ .../cli/commands/main/rules/_test_types.py | 7 +++ .../main/rules/test_sql_quality_rules.py | 63 +++++++++++++++++++ 4 files changed, 87 insertions(+), 3 deletions(-) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/plan_pipeline.rs b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/plan_pipeline.rs index 59da81f65f..f8340fe4d9 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/plan_pipeline.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compile_driver/plan_pipeline.rs @@ -1,9 +1,10 @@ //! The compile-and-plan pipeline of execution commands: compile, rules gate, then planning. use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyModule, PyModuleMethods, PyResult, Python}; -use pyo3::types::PyDict; +use pyo3::types::{PyDict, PyTuple}; use pyo3::{IntoPyObject, PyErr, pyfunction, wrap_pyfunction}; use sqlbuild_host::entries::models::ExecutorHost; +use sqlbuild_rules::lint_text::main::counts_toward_exit::counts_toward_exit; use sqlbuild_rules::rule_results::main::default_rules_notice::default_rules_notice; use crate::bindings::_helpers::boundary::python_calls::{KeywordArguments, keywords}; @@ -135,10 +136,17 @@ fn run_rules_gate<'py>( let message: String = rules_progress(elapsed, &result, true)?; on_progress.call1((message,))?; } - let findings: Bound<'py, PyAny> = native_field(&result, "findings")?; - if !findings.is_truthy()? { + let mut failing: Vec> = Vec::new(); + for finding in native_field(&result, "findings")?.try_iter()? { + let finding: Bound<'py, PyAny> = finding?; + if counts_toward_exit(&native_field(&finding, "code")?.extract::()?) { + failing.push(finding); + } + } + if failing.is_empty() { return Ok(()); } + let findings: Bound<'py, PyAny> = PyTuple::new(py, failing)?.into_any(); let summary: Bound<'py, PyAny> = new_record_with( py, RecordId::RulesResult, diff --git a/crates/sqlbuild-rules/src/engine/_helpers/evaluation.rs b/crates/sqlbuild-rules/src/engine/_helpers/evaluation.rs index 64d11d4ead..d76c61a0cb 100644 --- a/crates/sqlbuild-rules/src/engine/_helpers/evaluation.rs +++ b/crates/sqlbuild-rules/src/engine/_helpers/evaluation.rs @@ -1,6 +1,7 @@ use crate::configuration::main::validate as config; use crate::constants::{API_VERSION, NATIVE_BUILD_IDENTITY, RELATION_EXCEPTION_RULES}; use crate::engine::_helpers::cache::{Cache, PendingBucketWrite, RuleCacheEntry}; +use crate::lint_text::constants::LINT_UNAVAILABLE_CODE; use crate::models::{ EvaluateRequest, EvaluateResponse, Fault, Model, RuleException, RuleMetadata, RuleScope, RulesCodeGrammar, RulesConfig, @@ -580,11 +581,15 @@ pub(crate) fn finalize_findings( apply_fault_policy(&request, evaluated_codes, findings) } +/// Apply rule exceptions to `faults`; lint-unavailable warnings report our gap, not a rule, so they pass through unsuppressed. fn apply_fault_policy( request: &EvaluateRequest, evaluated_codes: &[String], faults: Vec, ) -> Result, String> { + let (unavailable, faults): (Vec, Vec) = faults + .into_iter() + .partition(|fault| fault.code == LINT_UNAVAILABLE_CODE); let unevaluated: BTreeSet<_> = faults .iter() .filter(|fault| fault.unevaluated) @@ -641,6 +646,7 @@ fn apply_fault_policy( )); fault }) + .chain(unavailable) .collect()) } diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/rules/_test_types.py b/tests/e2e/src/sqlbuild/cli/commands/main/rules/_test_types.py index 6e710ee77e..977b4079b0 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/rules/_test_types.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/rules/_test_types.py @@ -179,6 +179,13 @@ class RankingSortDefaultCase: expected_codes: tuple[str, ...] +@dataclass(frozen=True) +class ReplayedFindingExitCase: + description: str + code: str + expected_exit: int + + @dataclass(frozen=True) class FixableReportingCase: description: str diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/rules/test_sql_quality_rules.py b/tests/e2e/src/sqlbuild/cli/commands/main/rules/test_sql_quality_rules.py index 09961d554d..a22ba9759c 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/rules/test_sql_quality_rules.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/rules/test_sql_quality_rules.py @@ -9,12 +9,14 @@ import pytest +from sqlbuild._native import rules_bulk_cache_path from tests.e2e.src.sqlbuild.cli.commands.main.rules._test_types import ( ConstantLiteralCase, DuplicateLiteralCase, FixableReportingCase, RankingKeyProofCase, RankingSortDefaultCase, + ReplayedFindingExitCase, SqlQualityRuleCase, ) from tests.e2e.src.sqlbuild.cli.commands.main.rules.helpers import ( @@ -557,5 +559,66 @@ def test_given_repeated_long_literal_when_running_rules_then_help_names_the_shar ), result.stdout +@pytest.mark.parametrize( + "test_case", + ( + ReplayedFindingExitCase( + description="a SQL rule finding fails every command", + code="SQLCLEAN043", + expected_exit=1, + ), + ReplayedFindingExitCase( + description="a lint-unavailable warning fails no command", + code="L007", + expected_exit=0, + ), + ), + ids=lambda case: case.description, +) +def test_given_cached_sql_finding_when_running_commands_then_only_rule_findings_fail( + test_case: ReplayedFindingExitCase, tmp_path: Path +) -> None: + write_quality_project( + project_dir=tmp_path, + toml=( + 'name = "orders"\nadapter = "duckdb"\ndefault_target = "dev"\n\n' + '[connection]\ndatabase = "plan.duckdb"\n\n[targets.dev]\nschema = "analytics"\n\n' + '[rules]\nselect = ["SQLCLEAN043"]\n' + ), + upstream=_RAW_HEADER + _UPSTREAM_SQL, + summary=( + _SUMMARY_HEADER + "SELECT o.order_id, ROW_NUMBER() OVER (ORDER BY o.amount," + " o.status, o.note, o.customer_id, o.amount + 1, o.order_id) AS rn\n" + 'FROM __ref("raw_orders") AS o\n' + ), + ) + seeded: subprocess.CompletedProcess[str] = run_unevaluated_rules_cli( + tmp_path, "compile", "--json" + ) + assert diagnostic_codes(seeded) == ("SQLCLEAN043",), seeded.stdout + seeded.stderr + # Replay the cached finding under the case's code: the engine cannot emit L007 yet. + cache: Path = rules_bulk_cache_path(project_dir=tmp_path, file_name="sql.json") + stored: dict[str, Any] = json.loads(cache.read_text(encoding="utf-8")) + for entry in stored["entries"].values(): + for finding in entry["findings"]: + finding["code"] = test_case.code + _ = cache.write_text(json.dumps(stored), encoding="utf-8") + + compiled: subprocess.CompletedProcess[str] = run_unevaluated_rules_cli( + tmp_path, "compile", "--json" + ) + exits: tuple[int, ...] = ( + compiled.returncode, + run_unevaluated_rules_cli(tmp_path, "rules", "run", "SQLCLEAN043").returncode, + run_unevaluated_rules_cli(tmp_path, "plan").returncode, + run_unevaluated_rules_cli(tmp_path, "build").returncode, + ) + + assert (diagnostic_codes(compiled), exits) == ( + (test_case.code,), + (test_case.expected_exit,) * len(exits), + ), compiled.stdout + compiled.stderr + + if __name__ == "__main__": pytest.main([__file__, "-vv"]) From 35724086051ecc7ad02691887c4bfdf99c11b3f8 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sat, 7 Nov 2026 19:11:26 +0000 Subject: [PATCH 007/106] docs(rules): name plan and build, not format, in the L007 exit note --- .../skills/sqlbuild/references/docs/concepts/rules.md | 8 ++++---- website/src/content/docs/docs/concepts/rules.mdx | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/sqlbuild/.agents/skills/sqlbuild/references/docs/concepts/rules.md b/src/sqlbuild/.agents/skills/sqlbuild/references/docs/concepts/rules.md index aa7890c26d..0b2aac11f9 100644 --- a/src/sqlbuild/.agents/skills/sqlbuild/references/docs/concepts/rules.md +++ b/src/sqlbuild/.agents/skills/sqlbuild/references/docs/concepts/rules.md @@ -144,10 +144,10 @@ These checks run through ordinary compilation. There is intentionally no separat lifecycle. If an SQL requirement affects whether the project is acceptable, the compiler enforces it with the rest of the configured Rules. -If SQL Rules cannot run because SQLBuild does not parse the project's dialect yet, `sqb compile`, -`sqb rules run` and `sqb format` show one `L007` warning per file that names the dialect. `L007` -never changes an exit code: it reports a gap in SQLBuild's dialect support, not a problem in your -SQL, so the file's SQL Rules simply did not run. +If SQL Rules cannot run because SQLBuild does not parse the project's dialect yet, `sqb compile` +and `sqb rules run` show one `L007` warning per file that names the dialect. `L007` never changes +an exit code, including for `sqb plan` and `sqb build`: it reports a gap in SQLBuild's dialect +support, not a problem in your SQL, so the file's SQL Rules simply did not run. Not every traditional lint concern should become a diagnostic. SQLBuild separates three kinds of SQL responsibility: diff --git a/website/src/content/docs/docs/concepts/rules.mdx b/website/src/content/docs/docs/concepts/rules.mdx index 0ea8a21f5c..884993b4a5 100644 --- a/website/src/content/docs/docs/concepts/rules.mdx +++ b/website/src/content/docs/docs/concepts/rules.mdx @@ -132,10 +132,10 @@ These checks run through ordinary compilation. There is intentionally no separat lifecycle. If an SQL requirement affects whether the project is acceptable, the compiler enforces it with the rest of the configured Rules. -If SQL Rules cannot run because SQLBuild does not parse the project's dialect yet, `sqb compile`, -`sqb rules run` and `sqb format` show one `L007` warning per file that names the dialect. `L007` -never changes an exit code: it reports a gap in SQLBuild's dialect support, not a problem in your -SQL, so the file's SQL Rules simply did not run. +If SQL Rules cannot run because SQLBuild does not parse the project's dialect yet, `sqb compile` +and `sqb rules run` show one `L007` warning per file that names the dialect. `L007` never changes +an exit code, including for `sqb plan` and `sqb build`: it reports a gap in SQLBuild's dialect +support, not a problem in your SQL, so the file's SQL Rules simply did not run. Not every traditional lint concern should become a diagnostic. SQLBuild separates three kinds of SQL responsibility: From 47c7a6b0a8465e57999f5e4941bbbcc567b7a36b Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sat, 7 Nov 2026 19:21:35 +0000 Subject: [PATCH 008/106] test(orchestration): count the store statements a wide catalog listing issues A same-run time ratio cannot see one store round trip per node: that cost is linear. Tests now count the statements each task issues, and the wide listing must read builds 100 nodes per statement: 17 statements for 1,000 nodes and 37 for 3,000 on SQLite and PostgreSQL, 1,008 with one node per statement. The ratio check stays and now guards super-linear growth only. --- .../src/store/_helpers/mod.rs | 2 + .../src/store/_helpers/statement_count.rs | 13 +++++ .../src/store/main/query.rs | 2 + .../tests/api/node_status/helpers.rs | 56 ++++++++++++++----- 4 files changed, 58 insertions(+), 15 deletions(-) create mode 100644 crates/sqlbuild-orchestration/src/store/_helpers/statement_count.rs diff --git a/crates/sqlbuild-orchestration/src/store/_helpers/mod.rs b/crates/sqlbuild-orchestration/src/store/_helpers/mod.rs index 507d3bc78e..420775f72e 100644 --- a/crates/sqlbuild-orchestration/src/store/_helpers/mod.rs +++ b/crates/sqlbuild-orchestration/src/store/_helpers/mod.rs @@ -5,3 +5,5 @@ pub(crate) mod open_sqlite; pub(crate) mod recall_rows; pub(crate) mod recall_schema; pub(crate) mod release_version; +#[cfg(test)] +pub(crate) mod statement_count; diff --git a/crates/sqlbuild-orchestration/src/store/_helpers/statement_count.rs b/crates/sqlbuild-orchestration/src/store/_helpers/statement_count.rs new file mode 100644 index 0000000000..4b4e2c2db0 --- /dev/null +++ b/crates/sqlbuild-orchestration/src/store/_helpers/statement_count.rs @@ -0,0 +1,13 @@ +//! Test-only count of the store statements one task issues. + +use std::cell::Cell; + +tokio::task_local! { + /// The statements issued so far by a task that counts them. + pub(crate) static STATEMENTS: Cell; +} + +/// Count one statement when the current task counts them. +pub(crate) fn count_statement() { + let _ = STATEMENTS.try_with(|count| count.set(count.get() + 1)); +} diff --git a/crates/sqlbuild-orchestration/src/store/main/query.rs b/crates/sqlbuild-orchestration/src/store/main/query.rs index 9e79cdc7a1..f62a6d157c 100644 --- a/crates/sqlbuild-orchestration/src/store/main/query.rs +++ b/crates/sqlbuild-orchestration/src/store/main/query.rs @@ -10,6 +10,8 @@ pub(crate) async fn query( sql: &str, parameters: Vec, ) -> Result<(ControlTransaction, Vec), ControlError> { + #[cfg(test)] + crate::store::_helpers::statement_count::count_statement(); let connection = transaction .connection .take() diff --git a/crates/sqlbuild-orchestration/tests/api/node_status/helpers.rs b/crates/sqlbuild-orchestration/tests/api/node_status/helpers.rs index a07b031bbd..00d6459944 100644 --- a/crates/sqlbuild-orchestration/tests/api/node_status/helpers.rs +++ b/crates/sqlbuild-orchestration/tests/api/node_status/helpers.rs @@ -1,6 +1,7 @@ //! Real stored evidence never substitutes observation clocks for consumed inputs. use serde_json::{Value, json}; +use std::cell::Cell; use uuid::Uuid; use crate::credentials::models::{AuditContext, DefaultRole}; @@ -8,6 +9,7 @@ use crate::runs::tests::helpers::snapshot; use crate::snapshots::main::publish_snapshot::publish_snapshot; use crate::snapshots::models::SnapshotInput; use crate::snapshots::models::SnapshotNode; +use crate::store::_helpers::statement_count::STATEMENTS; use crate::store::errors::ControlError; use crate::store::main::{begin::begin, commit::commit, query::query}; use crate::store::models::{ControlStore, Parameter, TransactionMode}; @@ -19,6 +21,9 @@ use crate::warehouse_metadata::main::{ }; use crate::warehouse_metadata::models::{MetadataBatch, NodeStatusQuery}; +/// Statements a catalog listing issues besides one build read per 100 nodes: 7 fixed, 1 per extra namespace shape. +const WIDE_LISTING_FIXED_STATEMENTS: usize = 8; + pub(crate) async fn node_status_reads( store: &ControlStore, credentials: (&AuditContext, Uuid), @@ -243,7 +248,12 @@ pub(crate) async fn node_status_reads( ); assert!(!json!(listed).to_string().contains("metadata_json")); listing_ignores_build_history(store, credentials, (location, &listed)).await; - listing_reads_wide_targets_in_batches(store, credentials, (location, input.clone())).await; + listing_wide_targets_is_batched_without_super_linear_growth( + store, + credentials, + (location, input.clone()), + ) + .await; assert!(matches!( read_person_node_status( store, @@ -358,8 +368,8 @@ async fn listing_ignores_build_history( ); } -/// Three thousand built nodes, half in a named database, keep their own builds, and listing them grows linearly. -async fn listing_reads_wide_targets_in_batches( +/// Three thousand nodes, half in a named database, keep their own builds, read 100 per statement without super-linear growth. +async fn listing_wide_targets_is_batched_without_super_linear_growth( store: &ControlStore, credentials: (&AuditContext, Uuid), (location, mut input): (Uuid, SnapshotInput), @@ -367,33 +377,36 @@ async fn listing_reads_wide_targets_in_batches( let names: Vec = (0..3_000).map(|index| format!("wide_{index:04}")).collect(); let mut medians: Vec = Vec::new(); for (first, size) in [(0, 1_000), (1_000, names.len())] { - medians.push( - list_wide_target( - store, - credentials, - (location, &mut input), - (&names, first, size), - ) - .await, + let (median, statements) = list_wide_target( + store, + credentials, + (location, &mut input), + (&names, first, size), + ) + .await; + assert!( + statements <= size.div_ceil(100) + WIDE_LISTING_FIXED_STATEMENTS, + "listing {size} nodes issued {statements} store statements; builds must be read 100 or more nodes per statement" ); + medians.push(median); } let ratio: f64 = medians[1].as_secs_f64() / medians[0].as_secs_f64(); assert!( ratio < 6.0, - "listing {} nodes took {:?}, {ratio:.1} times the {:?} for 1,000; listing must stay linear", + "listing {} nodes took {:?}, {ratio:.1} times the {:?} for 1,000; listing must not grow super-linearly", names.len(), medians[1], medians[0] ); } -/// Add `names[first..size]` with one build each and return the median time to list the first `size`. +/// Add `names[first..size]` with one build each; return the median listing time and one listing's statement count. async fn list_wide_target( store: &ControlStore, credentials: (&AuditContext, Uuid), (location, input): (Uuid, &mut SnapshotInput), (names, first, size): (&[String], usize, usize), -) -> std::time::Duration { +) -> (std::time::Duration, usize) { let database = |index: usize| (index % 2 == 1).then(|| "analytics_db".to_owned()); let built_at = |index: usize| { chrono::DateTime::parse_from_rfc3339("2026-02-01T00:00:00Z") @@ -492,5 +505,18 @@ async fn list_wide_target( assert_eq!(built, size, "every wide node keeps its own build"); } timings.sort(); - timings[1] + let statements = STATEMENTS + .scope(Cell::new(0), async { + list_person_catalog_nodes( + store, + credentials, + (location, "development"), + DefaultRole::None, + ) + .await + .expect("counted wide catalog"); + STATEMENTS.with(Cell::get) + }) + .await; + (timings[1], statements) } From c00a9da1ed720fb32dd4e396242fcbfe9b6ec524 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sat, 7 Nov 2026 20:05:23 +0000 Subject: [PATCH 009/106] refactor(analysis): share diagnostic location and the catalog's options view --- .../semantic_validation/_helpers/catalog.rs | 7 +---- .../_helpers/function_names.rs | 26 ++++++++++++++----- .../_helpers/sqlite_temporal_casts.rs | 11 ++++---- .../_helpers/truth_tests.rs | 11 ++++---- 4 files changed, 31 insertions(+), 24 deletions(-) diff --git a/crates/sqlbuild-analysis/src/semantic_validation/_helpers/catalog.rs b/crates/sqlbuild-analysis/src/semantic_validation/_helpers/catalog.rs index 9dae17ad2e..9ac0e4b46d 100644 --- a/crates/sqlbuild-analysis/src/semantic_validation/_helpers/catalog.rs +++ b/crates/sqlbuild-analysis/src/semantic_validation/_helpers/catalog.rs @@ -175,14 +175,9 @@ impl ProjectCatalog { pub fn with_relations(&self, relations: Relations) -> Self { let mut catalog = Self { - dialect: self.dialect, - options: self.options.clone(), - quoted_ignore_case: self.quoted_ignore_case, tables: self.tables.clone(), - overrides: Vec::new(), analysis_tables: self.analysis_tables.clone(), - analysis_pool: Arc::clone(&self.analysis_pool), - running_analyses: Arc::clone(&self.running_analyses), + ..self.analysis_view() }; catalog.update_relations(relations); catalog diff --git a/crates/sqlbuild-analysis/src/semantic_validation/_helpers/function_names.rs b/crates/sqlbuild-analysis/src/semantic_validation/_helpers/function_names.rs index fe9a00ead3..0c697b9487 100644 --- a/crates/sqlbuild-analysis/src/semantic_validation/_helpers/function_names.rs +++ b/crates/sqlbuild-analysis/src/semantic_validation/_helpers/function_names.rs @@ -104,6 +104,21 @@ impl Cursor { } self.byte = byte; } + + /// `error` located at the byte range `start..end` of `sql`, with its character span. + pub(crate) fn locate( + &mut self, + sql: &str, + (start, end): (usize, usize), + error: ValidationError, + ) -> ValidationError { + self.advance(sql, start); + let first: usize = self.characters; + let last: usize = first + sql[start..end].chars().count(); + error + .with_location(self.line, self.column) + .with_span(Some(first), Some(last)) + } } /// Read `dialect SPELLING arguments ACCEPTED` rows; `#` starts a comment. @@ -156,12 +171,11 @@ pub(crate) fn unsupported_function_errors(sql: &str, dialect: DialectType) -> Ve "Unknown function '{}' for dialect {dialect:?}; did you mean {}?", call.name, call.suggestion ); - cursor.advance(sql, call.start); - let start: usize = cursor.characters; - let end: usize = start + sql[call.start..call.end].chars().count(); - ValidationError::error(message, UNKNOWN_FUNCTION) - .with_location(cursor.line, cursor.column) - .with_span(Some(start), Some(end)) + cursor.locate( + sql, + (call.start, call.end), + ValidationError::error(message, UNKNOWN_FUNCTION), + ) }) .collect() } diff --git a/crates/sqlbuild-analysis/src/semantic_validation/_helpers/sqlite_temporal_casts.rs b/crates/sqlbuild-analysis/src/semantic_validation/_helpers/sqlite_temporal_casts.rs index c3a37312e8..fe304fd8b0 100644 --- a/crates/sqlbuild-analysis/src/semantic_validation/_helpers/sqlite_temporal_casts.rs +++ b/crates/sqlbuild-analysis/src/semantic_validation/_helpers/sqlite_temporal_casts.rs @@ -35,12 +35,11 @@ pub(crate) fn sqlite_temporal_cast_warnings( '2026-01-01 10:00:00' becomes 2026", cast.type_name ); - cursor.advance(sql, cast.start); - let start: usize = cursor.characters; - let end: usize = start + sql[cast.start..cast.end].chars().count(); - ValidationError::warning(message, SQLITE_TEMPORAL_CAST_CODE) - .with_location(cursor.line, cursor.column) - .with_span(Some(start), Some(end)) + cursor.locate( + sql, + (cast.start, cast.end), + ValidationError::warning(message, SQLITE_TEMPORAL_CAST_CODE), + ) }) .collect() } diff --git a/crates/sqlbuild-analysis/src/semantic_validation/_helpers/truth_tests.rs b/crates/sqlbuild-analysis/src/semantic_validation/_helpers/truth_tests.rs index 896b12f20d..350eac4856 100644 --- a/crates/sqlbuild-analysis/src/semantic_validation/_helpers/truth_tests.rs +++ b/crates/sqlbuild-analysis/src/semantic_validation/_helpers/truth_tests.rs @@ -37,12 +37,11 @@ pub(crate) fn unsupported_truth_test_errors( spelling(&test), rewrite(dialect, &test) ); - cursor.advance(sql, test.start); - let start: usize = cursor.characters; - let end: usize = start + sql[test.start..test.end].chars().count(); - ValidationError::error(message, UNSUPPORTED_TRUTH_TEST_CODE) - .with_location(cursor.line, cursor.column) - .with_span(Some(start), Some(end)) + cursor.locate( + sql, + (test.start, test.end), + ValidationError::error(message, UNSUPPORTED_TRUTH_TEST_CODE), + ) }) .collect() } From 3cd6dff4e619c23ff6cf20bdf8832b61f4a83fa1 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 01:13:50 +0000 Subject: [PATCH 010/106] feat(contracts): prove dynamic pivots from SQLQuack parse trees The dynamic column contract proof parses pivot SQL with SQLQuack's shared parser (DuckDB, MotherDuck, Snowflake) instead of Polyglot. DuckDB trees match Polyglot's exactly; a Snowflake aggregate keeps its argument in a call's argument list, which the proof now reads. Other dialects keep the existing unsupported-dialect message. --- .../_helpers/dynamic_pivot.rs | 49 ++++++++++++++++--- .../assembly/analysis_session/constants.rs | 1 + .../tests/test_analysis_session.rs | 14 ++++++ 3 files changed, 57 insertions(+), 7 deletions(-) diff --git a/crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dynamic_pivot.rs b/crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dynamic_pivot.rs index e49ac7c85c..1204a40133 100644 --- a/crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dynamic_pivot.rs +++ b/crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dynamic_pivot.rs @@ -7,14 +7,15 @@ use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use serde_json::{Map, Value}; use sqlbuild_core::panics::main::catch_compiler_panic::catch_compiler_panic; use sqlbuild_core::panics::main::native_failure::native_failure; +use sqlquack_syntax::{CallMode, Dialect, Expression, ParseOptions}; use crate::assembly::analysis_session::_helpers::dict_walk::{ casefold, column_name, dict_list, identifier_name, is_single_wildcard, nested, node_key, - parse_one_or_error, payload, relation_name, render_type, to_dict, truthy, upper, + payload, relation_name, render_type, truthy, upper, }; use crate::assembly::analysis_session::constants::{ ALIAS_AST_KIND, CAST_AST_KIND, DUCKDB_DIALECT, DYNAMIC_VALUE_SOURCE_KINDS, MOTHERDUCK_DIALECT, - PIVOT_AST_KIND, QUERY_AST_KINDS, SELECT_AST_KIND, SIMPLIFIED_PIVOT_DIALECTS, + PIVOT_AST_KIND, QUERY_AST_KINDS, SELECT_AST_KIND, SIMPLIFIED_PIVOT_DIALECTS, SNOWFLAKE_DIALECT, SUPPORTED_PIVOT_DIALECTS, TYPE_PRESERVING_AGGREGATES, UNKNOWN_NULLABILITY, UNKNOWN_TYPE, }; use crate::assembly::analysis_session::models::{ @@ -115,11 +116,33 @@ pub(crate) fn pivot_outcome( .map_err(|reason| native_failure(PIVOT_CONTEXT, &reason)) } -/// Python's `parse_one(...).to_dict()`, or the parse error Python reports. +/// The one statement of `sql` as SQLQuack's tagged JSON tree, or why it does not parse. fn parsed(sql: &str, dialect: &str) -> Result, String> { - parse_one_or_error(sql, dialect)? - .map(|expression| to_dict(&expression)) - .map_or_else(|error| Ok(Err(error)), |value| value.map(Ok)) + let dialect: Dialect = if dialect == SNOWFLAKE_DIALECT { + Dialect::Snowflake + } else { + Dialect::DuckDB + }; + let parsed = sqlquack_syntax::parse_statements_with( + sql, + dialect, + &ParseOptions::default(), + CallMode::Faithful, + ); + let mut statements: Vec = match parsed { + Ok(statements) => statements, + Err(error) => return Ok(Err(error.to_string())), + }; + if statements.len() != 1 { + return Ok(Err(format!( + "Expected 1 statement, found {}", + statements.len() + ))); + } + let statement: Expression = statements.pop().ok_or_else(|| "no statement".to_owned())?; + serde_json::to_value(statement) + .map(Ok) + .map_err(|error| error.to_string()) } fn proof( @@ -362,6 +385,18 @@ fn has_dynamic_values(field: &Value) -> bool { .any(|value| DYNAMIC_VALUE_SOURCE_KINDS.contains(&node_key(Some(value)))) } +/// The one argument of a call SQLQuack keeps by name, such as Snowflake's `SUM(amount)`. +fn sole_argument(payload: &Map) -> Option<&Value> { + match payload + .get("args") + .and_then(Value::as_array) + .map(Vec::as_slice) + { + Some([argument]) => Some(argument), + _ => None, + } +} + /// Python's `_aggregate_fact`: aggregate name, value column and type-preserving inferred type. fn aggregate_fact<'a>( aggregate: &'a Value, @@ -380,7 +415,7 @@ fn aggregate_fact<'a>( name = upper(authored); } let preserving: bool = TYPE_PRESERVING_AGGREGATES.contains(&name.as_str()); - let value: Option<&Value> = payload.get("this"); + let value: Option<&Value> = payload.get("this").or_else(|| sole_argument(payload)); let mut value_name: Option<&str> = column_name(value); let mut inferred: Option = None; let cast: Option<&Map> = value diff --git a/crates/sqlbuild-analysis/src/assembly/analysis_session/constants.rs b/crates/sqlbuild-analysis/src/assembly/analysis_session/constants.rs index 5da96a56e2..fb2ff454b3 100644 --- a/crates/sqlbuild-analysis/src/assembly/analysis_session/constants.rs +++ b/crates/sqlbuild-analysis/src/assembly/analysis_session/constants.rs @@ -63,6 +63,7 @@ pub(crate) const SUPPORTED_PIVOT_DIALECTS: [&str; 3] = ["duckdb", "motherduck", pub(crate) const SIMPLIFIED_PIVOT_DIALECTS: [&str; 2] = ["duckdb", "motherduck"]; pub(crate) const DUCKDB_DIALECT: &str = "duckdb"; pub(crate) const MOTHERDUCK_DIALECT: &str = "motherduck"; +pub(crate) const SNOWFLAKE_DIALECT: &str = "snowflake"; /// The `to_dict()` node keys Python's dynamic pivot proof walks. pub(crate) const PIVOT_AST_KIND: &str = "pivot"; pub(crate) const SELECT_AST_KIND: &str = "select"; diff --git a/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_analysis_session.rs b/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_analysis_session.rs index 77aab0583f..e558d426c7 100644 --- a/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_analysis_session.rs +++ b/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_analysis_session.rs @@ -444,6 +444,20 @@ fn given_dynamic_pivots_when_proving_then_matches_python_or_defers() { false, ), }, + PivotTestCase { + description: "a Snowflake cast aggregate renders its target type", + dialect: "snowflake", + sql: "WITH base AS (SELECT customer_id, status, amount FROM __source(\"raw_orders\")) \ + SELECT * FROM base \ + PIVOT (MAX(CAST(amount AS DECIMAL(12, 2))) FOR status IN (ANY ORDER BY status))", + family: Some(("status", "amount", "MAX")), + expected_outcome: proven( + &[("customer_id", "INTEGER", "non_null")], + Some("DECIMAL(12, 2)"), + "raw_orders", + false, + ), + }, PivotTestCase { description: "a passthrough redeclaring the upstream families", dialect: "duckdb", From db2387f93f0c7958335490dd97764eace73b9be8 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 01:56:43 +0000 Subject: [PATCH 011/106] fix(contracts): refuse pivot SQL nested deeper than 512 levels before serialising it --- .../_helpers/dynamic_pivot.rs | 25 ++++++++-- .../assembly/analysis_session/constants.rs | 2 + .../tests/test_analysis_session.rs | 46 +++++++++++++++++-- .../analysis_session/tests/test_types.rs | 8 ++++ 4 files changed, 75 insertions(+), 6 deletions(-) diff --git a/crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dynamic_pivot.rs b/crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dynamic_pivot.rs index 1204a40133..e3de483bf9 100644 --- a/crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dynamic_pivot.rs +++ b/crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dynamic_pivot.rs @@ -7,7 +7,7 @@ use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use serde_json::{Map, Value}; use sqlbuild_core::panics::main::catch_compiler_panic::catch_compiler_panic; use sqlbuild_core::panics::main::native_failure::native_failure; -use sqlquack_syntax::{CallMode, Dialect, Expression, ParseOptions}; +use sqlquack_syntax::{CallMode, Dialect, Expression, ExpressionWalk, ParseOptions}; use crate::assembly::analysis_session::_helpers::dict_walk::{ casefold, column_name, dict_list, identifier_name, is_single_wildcard, nested, node_key, @@ -15,8 +15,9 @@ use crate::assembly::analysis_session::_helpers::dict_walk::{ }; use crate::assembly::analysis_session::constants::{ ALIAS_AST_KIND, CAST_AST_KIND, DUCKDB_DIALECT, DYNAMIC_VALUE_SOURCE_KINDS, MOTHERDUCK_DIALECT, - PIVOT_AST_KIND, QUERY_AST_KINDS, SELECT_AST_KIND, SIMPLIFIED_PIVOT_DIALECTS, SNOWFLAKE_DIALECT, - SUPPORTED_PIVOT_DIALECTS, TYPE_PRESERVING_AGGREGATES, UNKNOWN_NULLABILITY, UNKNOWN_TYPE, + PIVOT_AST_KIND, PIVOT_MAX_AST_DEPTH, QUERY_AST_KINDS, SELECT_AST_KIND, + SIMPLIFIED_PIVOT_DIALECTS, SNOWFLAKE_DIALECT, SUPPORTED_PIVOT_DIALECTS, + TYPE_PRESERVING_AGGREGATES, UNKNOWN_NULLABILITY, UNKNOWN_TYPE, }; use crate::assembly::analysis_session::models::{ ColumnFact, ContractProof, DynamicFamily, PivotModel, PivotOutcome, PivotTables, @@ -140,11 +141,29 @@ fn parsed(sql: &str, dialect: &str) -> Result, String> { ))); } let statement: Expression = statements.pop().ok_or_else(|| "no statement".to_owned())?; + if deeper_than(&statement, PIVOT_MAX_AST_DEPTH) { + return Ok(Err(format!( + "E_GUARD_AST_DEPTH_EXCEEDED: value {} exceeds configured limit {PIVOT_MAX_AST_DEPTH}", + PIVOT_MAX_AST_DEPTH + 1 + ))); + } serde_json::to_value(statement) .map(Ok) .map_err(|error| error.to_string()) } +/// Whether `statement` nests more than `limit` levels, walked iteratively so depth cannot overflow. +fn deeper_than(statement: &Expression, limit: usize) -> bool { + let mut stack: Vec<(&Expression, usize)> = vec![(statement, 1)]; + while let Some((node, depth)) = stack.pop() { + if depth > limit { + return true; + } + stack.extend(node.children().into_iter().map(|child| (child, depth + 1))); + } + false +} + fn proof( root: &Value, dialect: &str, diff --git a/crates/sqlbuild-analysis/src/assembly/analysis_session/constants.rs b/crates/sqlbuild-analysis/src/assembly/analysis_session/constants.rs index fb2ff454b3..106d04d447 100644 --- a/crates/sqlbuild-analysis/src/assembly/analysis_session/constants.rs +++ b/crates/sqlbuild-analysis/src/assembly/analysis_session/constants.rs @@ -94,6 +94,8 @@ pub(crate) const RENDERED_TYPE_NAMES: [(&str, &str); 12] = [ ]; /// The stack a standalone dynamic pivot proof parses on, as the analysis workers have. pub(crate) const PIVOT_WORKER_STACK_BYTES: usize = 16 * 1024 * 1024; +/// The deepest pivot statement the proof serialises; deeper SQL fails the proof instead of the stack. +pub(crate) const PIVOT_MAX_AST_DEPTH: usize = 512; /// Python's `COMPACT_RELATION_STUB_PREFIX`: the relation name a shared query binds instead. pub(crate) const RELATION_STUB_PREFIX: &str = "__sqlbuild_project_input_"; /// Python's `COMPACT_REFERENCE_MARKER_PATTERN`: a reference call and the name it reads. diff --git a/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_analysis_session.rs b/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_analysis_session.rs index e558d426c7..05dcaad310 100644 --- a/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_analysis_session.rs +++ b/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_analysis_session.rs @@ -9,9 +9,9 @@ use crate::assembly::analysis_session::tests::helpers::{ session_lines, session_steps, }; use crate::assembly::analysis_session::tests::test_types::{ - CteRecoveryTestCase, KeptAnalysisTestCase, LegacyAnalysisTestCase, ModelSpec, PivotTestCase, - SessionFactsTestCase, SessionFailureTestCase, SessionTestCase, StarExpansionTestCase, - UnscheduledTestCase, + CteRecoveryTestCase, DeepPivotTestCase, KeptAnalysisTestCase, LegacyAnalysisTestCase, + ModelSpec, PivotTestCase, SessionFactsTestCase, SessionFailureTestCase, SessionTestCase, + StarExpansionTestCase, UnscheduledTestCase, }; /// A model whose missing column stays untyped over a known input, so completion re-analyses it. @@ -756,3 +756,43 @@ fn given_a_finished_session_when_reading_model_analyses_then_keeps_typed_columns ); } } + +#[test] +fn given_deeply_nested_pivot_input_when_proving_then_the_proof_fails_without_aborting() { + let too_deep = failed( + "dynamic pivot SQL could not be parsed: \ + E_GUARD_AST_DEPTH_EXCEEDED: value 513 exceeds configured limit 512", + ); + let test_cases = [ + DeepPivotTestCase { + description: "a 50,000-term DuckDB projection", + dialect: "duckdb", + terms: 50_000, + expected_outcome: too_deep.clone(), + }, + DeepPivotTestCase { + description: "a 50,000-term Snowflake projection", + dialect: "snowflake", + terms: 50_000, + expected_outcome: too_deep, + }, + ]; + for test_case in test_cases { + let sql = format!( + "WITH base AS (SELECT customer_id, status, amount, 1{} AS k \ + FROM __source(\"raw_orders\")) \ + SELECT * FROM base PIVOT (MAX(amount) FOR status IN (ANY ORDER BY status))", + " + 1".repeat(test_case.terms) + ); + let request = pivot_request(test_case.dialect, &sql, Some(("status", "amount", "MAX"))); + let session = finished_session(request.tables.clone()); + + let standalone = prove_dynamic_contracts(&request).expect("the proofs run"); + let in_session = + prove_finished_dynamic_contracts(&session, &request.models).expect("the proofs run"); + + let expected = vec![test_case.expected_outcome, PivotOutcome::Absent]; + assert_eq!(standalone, expected, "{}", test_case.description); + assert_eq!(in_session, expected, "{}", test_case.description); + } +} diff --git a/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_types.rs b/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_types.rs index aa29f89c8d..1c338650e8 100644 --- a/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_types.rs +++ b/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_types.rs @@ -230,3 +230,11 @@ pub(crate) struct TypedMarkerCacheTestCase { pub(crate) sql: &'static str, pub(crate) expected_hits: &'static [bool], } + +/// A pivot over a CTE whose projection is a left-nested sum of `terms` ones. +pub(crate) struct DeepPivotTestCase { + pub(crate) description: &'static str, + pub(crate) dialect: &'static str, + pub(crate) terms: usize, + pub(crate) expected_outcome: PivotOutcome, +} From 630b2d8a6ff192819bbb0bb2d808429b847a7ae7 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 02:21:51 +0000 Subject: [PATCH 012/106] wip: read rule facts with SQLQuack --- Cargo.lock | 2 +- crates/sqlbuild-rules/Cargo.toml | 2 +- .../src/query_facts/_helpers/facts.rs | 90 ++++++++++--------- .../src/query_facts/_helpers/tokens.rs | 66 ++++++-------- .../src/query_facts/constants.rs | 8 ++ .../sqlbuild-rules/src/query_facts/models.rs | 12 ++- .../src/rules/_helpers/sql_test_rules.rs | 5 +- 7 files changed, 99 insertions(+), 86 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a9f5f63d09..9e33888683 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4431,7 +4431,6 @@ dependencies = [ "blake3", "fensu-policy", "globset", - "polyglot-sql", "rayon", "regex", "rustc-hash", @@ -4445,6 +4444,7 @@ dependencies = [ "sqlbuild-scopes", "sqlbuild-sqltext", "sqlclean", + "sqlquack-syntax", "stacker", "tempfile", "toml", diff --git a/crates/sqlbuild-rules/Cargo.toml b/crates/sqlbuild-rules/Cargo.toml index 535ee3687a..1070397264 100644 --- a/crates/sqlbuild-rules/Cargo.toml +++ b/crates/sqlbuild-rules/Cargo.toml @@ -11,7 +11,7 @@ rust-version.workspace = true blake3.workspace = true fensu-policy.workspace = true globset.workspace = true -polyglot-sql.workspace = true +sqlquack-syntax.workspace = true rayon.workspace = true regex.workspace = true rustc-hash.workspace = true diff --git a/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs b/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs index 5af9afca09..e2518efce7 100644 --- a/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs +++ b/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs @@ -1,33 +1,47 @@ use crate::query_facts::_helpers::shape::stub_call; use crate::query_facts::_helpers::tokens::{ComparisonOperator, QueryTokens, SelectTokens}; use crate::query_facts::constants::{ - MAX_FUNCTION_CALL_DEPTH, TABLE_FUNCTION_STUB, TRIPLE_QUOTE_LENGTH, + DUCKDB_DIALECTS, MAX_FUNCTION_CALL_DEPTH, POSTGRES_DIALECTS, SNOWFLAKE_DIALECT, SQLITE_DIALECT, + TABLE_FUNCTION_STUB, TRIPLE_QUOTE_LENGTH, }; use crate::query_facts::models::{ CallKind, ColumnFact, ComparisonFacts, ComparisonSpan, CteFacts, ExpressionItem, ItemFacts, ModelShape, ParsedFixture, QueryFacts, RelationFacts, SelectFacts, SetFacts, SourceText, }; -use polyglot_sql::dialects::Dialect; -use polyglot_sql::expressions::{Identifier, Literal, Select, TableRef, With}; -use polyglot_sql::{ComplexityGuardOptions, Expression, ExpressionWalk, ParseOptions}; -use sqlbuild_analysis::lineage::main::parser_dialect::parser_dialect; +use sqlquack_syntax::expressions::names::models::{Identifier, Literal, TableRef}; +use sqlquack_syntax::expressions::queries::models::{Select, With}; +use sqlquack_syntax::{CallMode, ComplexityGuardOptions, Dialect, Expression, ExpressionWalk, ParseOptions}; use std::cell::{Cell, RefCell}; use std::collections::{HashMap, HashSet}; use std::ops::Range; +/// The SQLQuack grammar that reads `dialect_name` for built-in rules, or `None` when none does. +pub(crate) fn facts_dialect(dialect_name: &str) -> Option { + if DUCKDB_DIALECTS.contains(&dialect_name) { + return Some(Dialect::DuckDB); + } + if POSTGRES_DIALECTS.contains(&dialect_name) { + return Some(Dialect::PostgreSQL); + } + match dialect_name { + SNOWFLAKE_DIALECT => Some(Dialect::Snowflake), + SQLITE_DIALECT => Some(Dialect::SQLite), + _ => None, + } +} + fn parse_query(sql: &str, dialect_name: &str) -> Result<(Expression, Dialect), String> { - let dialect_type = parser_dialect(Some(dialect_name)) - .map_err(|unknown| format!("unknown dialect {unknown}"))?; - let dialect = Dialect::get(dialect_type); + let dialect = facts_dialect(dialect_name) + .ok_or_else(|| format!("no SQL parser for dialect {dialect_name}"))?; let options = ParseOptions { complexity_guard: Some(ComplexityGuardOptions { max_function_call_depth: Some(MAX_FUNCTION_CALL_DEPTH), ..Default::default() }), }; - let mut statements = dialect - .parse_with_options(sql, &options) - .map_err(|error| error.to_string())?; + let mut statements = + sqlquack_syntax::parse_statements_with(sql, dialect, &options, CallMode::FaithfulLocated) + .map_err(|error| error.to_string())?; if statements.len() != 1 { return Err("expected one statement".to_owned()); } @@ -38,16 +52,18 @@ fn parse_query(sql: &str, dialect_name: &str) -> Result<(Expression, Dialect), S Ok((statement, dialect)) } +fn code_tokens(sql: &str, dialect: Dialect) -> Result, String> { + let tokens = sqlquack_syntax::tokenize(sql, dialect).map_err(|error| error.to_string())?; + Ok(QueryTokens::new(tokens, sql)) +} + /// Parse a stubbed SQL-test fixture as one query. pub(crate) fn parse_fixture( shape: ModelShape, dialect_name: &str, ) -> Result { let (statement, dialect) = parse_query(&shape.sql, dialect_name)?; - let tokens = dialect - .tokenize(&shape.sql) - .map_err(|error| error.to_string())?; - let tokens = QueryTokens::new(tokens, &shape.sql); + let tokens = code_tokens(&shape.sql, dialect)?; let selects = tokens.selects(); let order = tokens.top_level_word("ORDER").unwrap_or(tokens.len()); let order_end = ["LIMIT", "OFFSET", "FETCH"] @@ -72,15 +88,13 @@ pub(crate) fn parse_fixture( }) } -/// Parse normalized model SQL with Polyglot and read the facts built-in rules evaluate. +/// Parse normalized model SQL with SQLQuack and read the facts built-in rules evaluate. pub(crate) fn query_facts(sql: &str, dialect_name: &str) -> Result { let (statement, dialect) = parse_query(sql, dialect_name)?; - let tokens = dialect.tokenize(sql).map_err(|error| error.to_string())?; - let tokens = QueryTokens::new(tokens, sql); + let tokens = code_tokens(sql, dialect)?; let builder = Builder { selects: tokens.selects(), tokens, - dialect, spans: RefCell::default(), cursor: Cell::default(), claimed: RefCell::default(), @@ -104,7 +118,6 @@ fn is_query(expression: &Expression) -> bool { struct Builder<'a> { tokens: QueryTokens<'a>, selects: Vec, - dialect: Dialect, spans: RefCell>>, cursor: Cell, claimed: RefCell>, @@ -303,18 +316,9 @@ impl Builder<'_> { let range = item.map(|item| self.tokens.expression_range(item, alias.is_some())); let unwrapped = unwrap_paren(value); let cast_type = match unwrapped { - Expression::Cast(cast) | Expression::TryCast(cast) | Expression::SafeCast(cast) => { - Some( - range - .as_ref() - .and_then(|range| self.tokens.cast_type(range)) - .unwrap_or_else(|| { - self.dialect - .generate(&Expression::DataType(cast.to.clone())) - .unwrap_or_default() - }), - ) - } + Expression::Cast(_) | Expression::TryCast(_) | Expression::SafeCast(_) => range + .as_ref() + .and_then(|range| self.tokens.cast_type(range)), _ => None, }; ItemFacts::Expression(ExpressionItem { @@ -361,15 +365,13 @@ impl Builder<'_> { if let Some(span) = self.spans.borrow().get(&key) { return *span; } - let anchors = anchors(parts); - let generated = if anchors == (None, None) { - self.dialect.generate(node).unwrap_or_default() - } else { - String::new() + let anchors = match anchors(parts) { + (None, None) => (node.source_span().map(|span| span.start), None), + anchors => anchors, }; let span = self .tokens - .comparison_range(parts.operator, anchors, parts.negated, &generated); + .comparison_range(parts.operator, anchors, parts.negated); self.spans.borrow_mut().insert(key, span); span } @@ -404,7 +406,10 @@ impl Builder<'_> { let mut fact = ComparisonFacts { span, generated_sql: if span.is_none() { - self.dialect.generate(node).unwrap_or_default() + node.source_span() + .and_then(|span| self.tokens.slice(span.start, span.end)) + .unwrap_or_default() + .to_owned() } else { String::new() }, @@ -432,7 +437,8 @@ impl Builder<'_> { if let Some(generated) = string_literal(literal) { let position = string_tokens .iter() - .position(|(_, _, text)| *text == literal.value_str()); + .position(|(_, _, text)| *text == generated) + .or_else(|| (!string_tokens.is_empty()).then_some(0)); let range = position.map(|index| { let (start, end, _) = string_tokens.remove(index); (start, end) @@ -668,7 +674,7 @@ fn own_span(expression: &Expression) -> Option { Expression::Column(column) => column.span.or(column.name.span), Expression::Identifier(identifier) => identifier.span, Expression::Star(star) => star.table.as_ref().and_then(|table| table.span), - Expression::Function(function) => function.span, + Expression::Function(function) => function.span.0, Expression::Table(table) => table .catalog .as_ref() @@ -692,7 +698,7 @@ fn span_bounds(expression: &Expression) -> Option<(usize, usize)> { Expression::Column(column) => column.span.or(column.name.span), Expression::Identifier(identifier) => identifier.span, Expression::Star(star) => star.table.as_ref().and_then(|table| table.span), - Expression::Function(function) => function.span, + Expression::Function(function) => function.span.0, Expression::Count(count) => count.span, Expression::Table(table) => table.name.span, _ => None, diff --git a/crates/sqlbuild-rules/src/query_facts/_helpers/tokens.rs b/crates/sqlbuild-rules/src/query_facts/_helpers/tokens.rs index 9e6863c110..55dbf894df 100644 --- a/crates/sqlbuild-rules/src/query_facts/_helpers/tokens.rs +++ b/crates/sqlbuild-rules/src/query_facts/_helpers/tokens.rs @@ -1,6 +1,8 @@ use crate::query_facts::constants::MIN_CAST_TOKENS; -use crate::query_facts::models::ComparisonSpan; -use polyglot_sql::tokens::{Token, TokenType}; +use crate::query_facts::models::{CodeToken, ComparisonSpan}; +use sqlquack_syntax::LosslessToken; +use sqlquack_syntax::TokenKind; +use sqlquack_syntax::tokens::models::TokenType; use std::cell::RefCell; use std::collections::HashSet; use std::ops::Range; @@ -150,32 +152,35 @@ pub(crate) struct SelectTokens { /// The code tokens of a normalized query, without comments, over its text. pub(crate) struct QueryTokens<'a> { - tokens: Vec, + tokens: Vec>, sql: &'a str, bytes: Vec, used: RefCell>, } impl<'a> QueryTokens<'a> { - pub(crate) fn new(tokens: Vec, sql: &'a str) -> Self { + /// The code tokens of `tokens`, a lossless tokenization of `sql`, at character offsets. + pub(crate) fn new(tokens: Vec, sql: &'a str) -> Self { let mut bytes: Vec = sql.char_indices().map(|(byte, _)| byte).collect(); bytes.push(sql.len()); + let character = |byte: usize| bytes.partition_point(|offset| *offset < byte); + let tokens = tokens + .into_iter() + .filter_map(|token| match token.kind { + TokenKind::Significant(token_type) => Some(CodeToken { + token_type, + text: &sql[token.start..token.end], + start: character(token.start), + end: character(token.end), + }), + TokenKind::Whitespace | TokenKind::LineComment | TokenKind::BlockComment => None, + }) + .collect(); Self { sql, bytes, used: RefCell::default(), - tokens: tokens - .into_iter() - .filter(|token| { - !matches!( - token.token_type, - TokenType::BlockComment - | TokenType::LineComment - | TokenType::Space - | TokenType::Break - ) - }) - .collect(), + tokens, } } @@ -185,11 +190,11 @@ impl<'a> QueryTokens<'a> { } pub(crate) fn start(&self, index: usize) -> usize { - self.tokens[index].span.start + self.tokens[index].start } pub(crate) fn end(&self, index: usize) -> usize { - self.tokens[index].span.end + self.tokens[index].end } fn kind(&self, index: usize) -> Option { @@ -369,14 +374,14 @@ impl<'a> QueryTokens<'a> { parameterized && (cast || conversion) } - /// The string-literal tokens within a normalized range, as character range and parsed text. + /// The string-literal tokens within a normalized range, as character range and source text. pub(crate) fn string_tokens(&self, start: usize, end: usize) -> Vec<(usize, usize, &str)> { self.tokens .iter() .filter(|token| { - token.span.start >= start && token.span.end <= end && string_token(token.token_type) + token.start >= start && token.end <= end && string_token(token.token_type) }) - .map(|token| (token.span.start, token.span.end, token.text.as_str())) + .map(|token| (token.start, token.end, token.text)) .collect() } @@ -595,7 +600,6 @@ impl<'a> QueryTokens<'a> { operator: ComparisonOperator, (left_end, right_start): (Option, Option), negated: bool, - generated: &str, ) -> Option { let anchor = match (left_end, right_start) { (Some(end), _) => (0..self.tokens.len()).find(|index| { @@ -608,16 +612,7 @@ impl<'a> QueryTokens<'a> { && self.operator(*index, operator) && !self.used.borrow().contains(index) }), - (None, None) => { - let wanted = compact(generated); - (0..self.tokens.len()).find(|index| { - self.operator(*index, operator) && !self.used.borrow().contains(index) && { - let (first, last) = self.extent(*index, operator, negated); - self.slice(self.start(first), self.end(last)) - .is_some_and(|text| compact(text) == wanted) - } - }) - } + (None, None) => None, }?; self.used.borrow_mut().insert(anchor); let (first, last) = self.extent(anchor, operator, negated); @@ -685,13 +680,6 @@ impl<'a> QueryTokens<'a> { } } -fn compact(text: &str) -> String { - text.chars() - .filter(|character| !character.is_whitespace()) - .flat_map(char::to_lowercase) - .collect() -} - fn string_token(token_type: TokenType) -> bool { matches!( token_type, diff --git a/crates/sqlbuild-rules/src/query_facts/constants.rs b/crates/sqlbuild-rules/src/query_facts/constants.rs index 564143160d..db572236e9 100644 --- a/crates/sqlbuild-rules/src/query_facts/constants.rs +++ b/crates/sqlbuild-rules/src/query_facts/constants.rs @@ -10,3 +10,11 @@ pub(crate) const UDF_CALL: &str = "__udf"; pub(crate) const TABLE_FUNCTION_CALL: &str = "__table_fn"; pub(crate) const MIN_CAST_TOKENS: usize = 2; pub(crate) const TRIPLE_QUOTE_LENGTH: usize = 3; +/// Dialect names read with the shared DuckDB grammar; `generic` matches the fingerprint tokenizer. +pub(crate) const DUCKDB_DIALECTS: [&str; 3] = ["duckdb", "motherduck", "generic"]; +/// Dialect names read with the shared PostgreSQL grammar. +pub(crate) const POSTGRES_DIALECTS: [&str; 2] = ["postgres", "postgresql"]; +/// The Snowflake dialect name. +pub(crate) const SNOWFLAKE_DIALECT: &str = "snowflake"; +/// The SQLite dialect name. +pub(crate) const SQLITE_DIALECT: &str = "sqlite"; diff --git a/crates/sqlbuild-rules/src/query_facts/models.rs b/crates/sqlbuild-rules/src/query_facts/models.rs index 35dd961830..c36b32e080 100644 --- a/crates/sqlbuild-rules/src/query_facts/models.rs +++ b/crates/sqlbuild-rules/src/query_facts/models.rs @@ -1,4 +1,5 @@ -use polyglot_sql::Expression; +use sqlquack_syntax::Expression; +use sqlquack_syntax::tokens::models::TokenType; use std::collections::BTreeSet; use crate::lint_text::main::map_expanded_offset::map_expanded_offset; @@ -366,3 +367,12 @@ impl<'a> ModelSource<'a> { self.sql.get(from..to.max(from)).unwrap_or_default() } } + +/// One code token of a normalized query: its type, source text and character range. +#[derive(Debug)] +pub(crate) struct CodeToken<'a> { + pub(crate) token_type: TokenType, + pub(crate) text: &'a str, + pub(crate) start: usize, + pub(crate) end: usize, +} diff --git a/crates/sqlbuild-rules/src/rules/_helpers/sql_test_rules.rs b/crates/sqlbuild-rules/src/rules/_helpers/sql_test_rules.rs index ae4c1d8d29..b4ac7cc0ac 100644 --- a/crates/sqlbuild-rules/src/rules/_helpers/sql_test_rules.rs +++ b/crates/sqlbuild-rules/src/rules/_helpers/sql_test_rules.rs @@ -8,8 +8,9 @@ use crate::query_facts::main::stub_call::stub_call; use crate::query_facts::models::{CallKind, ParsedFixture}; use crate::rules::_helpers::evaluation::path_fault; use crate::rules::models::ProjectEvaluationRequest; -use polyglot_sql::Expression; -use polyglot_sql::expressions::{Literal, Select}; +use sqlquack_syntax::Expression; +use sqlquack_syntax::expressions::names::models::Literal; +use sqlquack_syntax::expressions::queries::models::Select; use serde::{Deserialize, Serialize}; use sqlbuild_analysis::compiler::main::sql_test_fixture_facts::fixture_facts; use std::cell::RefCell; From d9e86d0d49855b42f2e094646f93ff5957840c02 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 02:37:35 +0000 Subject: [PATCH 013/106] wip: locate cast types and Snowflake date parts without a generator --- .../src/query_facts/_helpers/facts.rs | 45 ++++++++++++++++--- .../src/query_facts/_helpers/tokens.rs | 7 +++ .../src/query_facts/constants.rs | 4 ++ 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs b/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs index e2518efce7..6f5a147e31 100644 --- a/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs +++ b/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs @@ -1,7 +1,7 @@ use crate::query_facts::_helpers::shape::stub_call; use crate::query_facts::_helpers::tokens::{ComparisonOperator, QueryTokens, SelectTokens}; use crate::query_facts::constants::{ - DUCKDB_DIALECTS, MAX_FUNCTION_CALL_DEPTH, POSTGRES_DIALECTS, SNOWFLAKE_DIALECT, SQLITE_DIALECT, + DATE_DIFF_ARGUMENTS, DATE_DIFF_CALLS, DUCKDB_DIALECTS, MAX_FUNCTION_CALL_DEPTH, POSTGRES_DIALECTS, SNOWFLAKE_DIALECT, SQLITE_DIALECT, TABLE_FUNCTION_STUB, TRIPLE_QUOTE_LENGTH, }; use crate::query_facts::models::{ @@ -95,6 +95,7 @@ pub(crate) fn query_facts(sql: &str, dialect_name: &str) -> Result bool { struct Builder<'a> { tokens: QueryTokens<'a>, selects: Vec, + dialect: Dialect, spans: RefCell>>, cursor: Cell, claimed: RefCell>, @@ -316,9 +318,14 @@ impl Builder<'_> { let range = item.map(|item| self.tokens.expression_range(item, alias.is_some())); let unwrapped = unwrap_paren(value); let cast_type = match unwrapped { - Expression::Cast(_) | Expression::TryCast(_) | Expression::SafeCast(_) => range + Expression::Cast(cast) | Expression::TryCast(cast) | Expression::SafeCast(cast) => range .as_ref() - .and_then(|range| self.tokens.cast_type(range)), + .and_then(|range| self.tokens.cast_type(range)) + .or_else(|| { + let span = cast.span.0?; + self.tokens + .cast_type(&self.tokens.covering(span.start, span.end)) + }), _ => None, }; ItemFacts::Expression(ExpressionItem { @@ -425,6 +432,7 @@ impl Builder<'_> { let mut string_tokens: Vec<(usize, usize, &str)> = span.map_or_else(Vec::new, |span| { self.tokens.string_tokens(span.start, span.end) }); + let units = date_part_arguments(node, self.dialect); for value in node.dfs() { match value { Expression::Column(column) => { @@ -433,7 +441,7 @@ impl Builder<'_> { Expression::Identifier(identifier) => { fact.columns.insert(identifier.name.clone()); } - Expression::Literal(literal) => { + Expression::Literal(literal) if !units.contains(&std::ptr::from_ref(value)) => { if let Some(generated) = string_literal(literal) { let position = string_tokens .iter() @@ -458,9 +466,10 @@ impl Builder<'_> { .extend(operand.dfs().filter_map(direct_column)); } if direct_string_literal(operand).is_none() - && operand - .dfs() - .any(|value| direct_string_literal(value).is_some()) + && operand.dfs().any(|value| { + !units.contains(&std::ptr::from_ref(value)) + && direct_string_literal(value).is_some() + }) { fact.modified_string_literal = true; } @@ -469,6 +478,28 @@ impl Builder<'_> { } } +/// The quoted first arguments of Snowflake's three-argument `DATEDIFF` calls, which are date parts. +fn date_part_arguments(root: &Expression, dialect: Dialect) -> HashSet<*const Expression> { + if dialect != Dialect::Snowflake { + return HashSet::new(); + } + root.dfs() + .filter_map(|node| match node { + Expression::Function(function) + if function.args.len() == DATE_DIFF_ARGUMENTS + && DATE_DIFF_CALLS.contains(&function.name.to_ascii_uppercase().as_str()) => + { + function + .args + .first() + .filter(|argument| direct_string_literal(argument).is_some()) + .map(std::ptr::from_ref) + } + _ => None, + }) + .collect() +} + fn comparison_parts(node: &Expression) -> Option> { let (operator, operands, negated) = match node { Expression::Eq(op) => (ComparisonOperator::Eq, vec![&op.left, &op.right], false), diff --git a/crates/sqlbuild-rules/src/query_facts/_helpers/tokens.rs b/crates/sqlbuild-rules/src/query_facts/_helpers/tokens.rs index 55dbf894df..18a1b33bb7 100644 --- a/crates/sqlbuild-rules/src/query_facts/_helpers/tokens.rs +++ b/crates/sqlbuild-rules/src/query_facts/_helpers/tokens.rs @@ -374,6 +374,13 @@ impl<'a> QueryTokens<'a> { parameterized && (cast || conversion) } + /// The tokens within characters `start` to `end`, as a token range. + pub(crate) fn covering(&self, start: usize, end: usize) -> Range { + let first = self.tokens.partition_point(|token| token.start < start); + let last = self.tokens.partition_point(|token| token.end <= end); + first..last.max(first) + } + /// The string-literal tokens within a normalized range, as character range and source text. pub(crate) fn string_tokens(&self, start: usize, end: usize) -> Vec<(usize, usize, &str)> { self.tokens diff --git a/crates/sqlbuild-rules/src/query_facts/constants.rs b/crates/sqlbuild-rules/src/query_facts/constants.rs index db572236e9..a87219f95e 100644 --- a/crates/sqlbuild-rules/src/query_facts/constants.rs +++ b/crates/sqlbuild-rules/src/query_facts/constants.rs @@ -18,3 +18,7 @@ pub(crate) const POSTGRES_DIALECTS: [&str; 2] = ["postgres", "postgresql"]; pub(crate) const SNOWFLAKE_DIALECT: &str = "snowflake"; /// The SQLite dialect name. pub(crate) const SQLITE_DIALECT: &str = "sqlite"; +/// Snowflake calls whose quoted first argument is a date part when they take three arguments. +pub(crate) const DATE_DIFF_CALLS: [&str; 4] = ["DATEDIFF", "DATE_DIFF", "TIMEDIFF", "TIMESTAMPDIFF"]; +/// The argument count at which a `DATE_DIFF_CALLS` call reads its first argument as a date part. +pub(crate) const DATE_DIFF_ARGUMENTS: usize = 3; From a2909a3e102d00a4acfa63e563ad6ecad66e26b1 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 02:42:54 +0000 Subject: [PATCH 014/106] wip: report built-in rules unavailable for unparsed dialects --- crates/sqlbuild-rules/src/constants.rs | 3 ++ .../engine/tests/test_empty_input_tests.rs | 19 +++---- .../src/engine/tests/test_evaluation.rs | 14 ++--- .../src/engine/tests/test_query_facts.rs | 54 ++++++++++++------- .../src/query_facts/_helpers/facts.rs | 26 +++++---- .../src/query_facts/constants.rs | 3 +- .../src/query_facts/main/facts_available.rs | 6 +++ .../src/query_facts/main/mod.rs | 1 + .../src/query_facts/main/query_facts.rs | 2 +- .../sqlbuild-rules/src/query_facts/models.rs | 2 +- .../src/rules/_helpers/evaluation.rs | 34 ++++++++++++ .../src/rules/_helpers/sql_test_rules.rs | 54 ++++++++++++++----- 12 files changed, 155 insertions(+), 63 deletions(-) create mode 100644 crates/sqlbuild-rules/src/query_facts/main/facts_available.rs diff --git a/crates/sqlbuild-rules/src/constants.rs b/crates/sqlbuild-rules/src/constants.rs index 37b4e3f953..e3f4b5ad79 100644 --- a/crates/sqlbuild-rules/src/constants.rs +++ b/crates/sqlbuild-rules/src/constants.rs @@ -55,3 +55,6 @@ pub(crate) const ALLOWED_QUALIFIERS_OPTION: &str = "allowed_qualifiers"; pub(crate) const ALLOWED_PATHS_KEY: &str = "paths"; /// The published layers when a project does not configure them. pub(crate) const DEFAULT_PUBLISHED_LAYERS: [&str; 2] = ["mart", "mart_v"]; +/// What to do when SQLBuild does not parse a project's dialect for built-in rules. +pub(crate) const RULES_UNAVAILABLE_REMEDIATION: &str = + "These rules did not run. Review this file by hand until built-in rules support this dialect."; diff --git a/crates/sqlbuild-rules/src/engine/tests/test_empty_input_tests.rs b/crates/sqlbuild-rules/src/engine/tests/test_empty_input_tests.rs index 3c9f34d22b..92657d96c2 100644 --- a/crates/sqlbuild-rules/src/engine/tests/test_empty_input_tests.rs +++ b/crates/sqlbuild-rules/src/engine/tests/test_empty_input_tests.rs @@ -14,6 +14,7 @@ const NO_ROWS_ASSERTION: (&str, &str) = ( "__assert__empty_inputs_produce_no_rows", "SELECT 1 AS unexpected_row FROM __ref(\"orders\")", ); +const UNAVAILABLE_MESSAGE: &str = "L007 tests/unit/test_orders__empty_inputs_produce_no_rows.sql: SQLBuild cannot parse mssql SQL for built-in rules yet, so these rules were not evaluated: SQBRTEST203"; const FLAGGED_MESSAGE: &str = "SQBRTEST203 tests/unit/test_orders__empty_inputs_produce_no_rows.sql: unit test block 1 (\"orders__empty_inputs_produce_no_rows\") mocks only empty inputs and asserts only that no rows are produced"; #[test] @@ -419,7 +420,7 @@ fn given_cached_minimum_tests_when_filler_becomes_real_test_then_model_cache_is_ } #[test] -fn given_sql_server_conversions_when_evaluating_empty_input_rule_then_type_arguments_are_not_calls() +fn given_sql_server_conversions_when_evaluating_empty_input_rule_then_reports_the_dialect_unavailable() -> Result<(), String> { let project_dir = TempDir::new().map_err(|error| error.to_string())?; let empty_orders = ( @@ -428,7 +429,7 @@ fn given_sql_server_conversions_when_evaluating_empty_input_rule_then_type_argum ); let test_cases = [ test_types::EmptyInputTestRuleTestCase { - description: "a CONVERT to a parameterized type under a false predicate is an empty mock", + description: "a CONVERT to a parameterized type under a false predicate is an empty mock, reported unavailable until SQLBuild parses the dialect", test: helpers::empty_input_test_fact( "orders__empty_inputs_produce_no_rows", &[( @@ -439,10 +440,10 @@ fn given_sql_server_conversions_when_evaluating_empty_input_rule_then_type_argum &[NO_ROWS_ASSERTION], ), allowed_tests: json!([]), - expected_messages: &[FLAGGED_MESSAGE], + expected_messages: &[UNAVAILABLE_MESSAGE], }, test_types::EmptyInputTestRuleTestCase { - description: "a CONVERT with a style argument is an empty mock", + description: "a CONVERT with a style argument is an empty mock, reported unavailable until SQLBuild parses the dialect", test: helpers::empty_input_test_fact( "orders__empty_inputs_produce_no_rows", &[( @@ -453,10 +454,10 @@ fn given_sql_server_conversions_when_evaluating_empty_input_rule_then_type_argum &[NO_ROWS_ASSERTION], ), allowed_tests: json!([]), - expected_messages: &[FLAGGED_MESSAGE], + expected_messages: &[UNAVAILABLE_MESSAGE], }, test_types::EmptyInputTestRuleTestCase { - description: "a TRY_CONVERT to a parameterized type keeps a bare existence check", + description: "a TRY_CONVERT to a parameterized type keeps a bare existence check, reported unavailable until SQLBuild parses the dialect", test: helpers::empty_input_test_fact( "orders__empty_inputs_produce_no_rows", &[empty_orders], @@ -467,10 +468,10 @@ fn given_sql_server_conversions_when_evaluating_empty_input_rule_then_type_argum )], ), allowed_tests: json!([]), - expected_messages: &[FLAGGED_MESSAGE], + expected_messages: &[UNAVAILABLE_MESSAGE], }, test_types::EmptyInputTestRuleTestCase { - description: "a function call in a CONVERT value argument still counts", + description: "a function call in a CONVERT value argument still counts, reported unavailable until SQLBuild parses the dialect", test: helpers::empty_input_test_fact( "orders__empty_inputs_produce_no_rows", &[( @@ -481,7 +482,7 @@ fn given_sql_server_conversions_when_evaluating_empty_input_rule_then_type_argum &[NO_ROWS_ASSERTION], ), allowed_tests: json!([]), - expected_messages: &[], + expected_messages: &[UNAVAILABLE_MESSAGE], }, ]; diff --git a/crates/sqlbuild-rules/src/engine/tests/test_evaluation.rs b/crates/sqlbuild-rules/src/engine/tests/test_evaluation.rs index 26cef0a1e1..7b77c64f26 100644 --- a/crates/sqlbuild-rules/src/engine/tests/test_evaluation.rs +++ b/crates/sqlbuild-rules/src/engine/tests/test_evaluation.rs @@ -27,11 +27,13 @@ fn given_dependency_tokens_when_classifying_imports_then_preserves_dialect_and_o expected_messages: &[], }, test_types::DependencyTokenTestCase { - description: "backtick quoting and comment-separated source arguments", + description: "backtick quoting and comment-separated source arguments, reported unavailable until SQLBuild parses the dialect", expected_columns: &[], dialect: "bigquery", query_sql: "WITH imported AS (SELECT * FROM __source /* ignored */ (`Orders`)) SELECT order_id FROM imported", - expected_messages: &[], + expected_messages: &[ + "SQLBuild cannot parse bigquery SQL for built-in rules yet, so these rules were not evaluated: SQBRMODEL101", + ], }, test_types::DependencyTokenTestCase { description: "dollar strings do not contribute spurious calls", @@ -552,22 +554,22 @@ fn given_dialect_quoting_around_table_functions_when_evaluating_rules_then_model expected_code: "SQBRCONTRACT101", }, test_types::DialectEvaluationTestCase { - description: "BigQuery backslash-escaped quote inside table function arguments", + description: "BigQuery backslash-escaped quote inside table function arguments, reported unavailable until SQLBuild parses the dialect", dialect: "bigquery", query_sql: concat!( "SELECT item_id ", "FROM __table_fn(\"inventory_items\", 'a\\')')(42)" ), - expected_code: "SQBRCONTRACT101", + expected_code: "L007", }, test_types::DialectEvaluationTestCase { - description: "BigQuery backtick identifier containing a parenthesis in arguments", + description: "BigQuery backtick identifier containing a parenthesis in arguments, reported unavailable until SQLBuild parses the dialect", dialect: "bigquery", query_sql: concat!( "SELECT item_id ", "FROM __table_fn(\"inventory_items\", `batch)size`)(42)" ), - expected_code: "SQBRCONTRACT101", + expected_code: "L007", }, ]; diff --git a/crates/sqlbuild-rules/src/engine/tests/test_query_facts.rs b/crates/sqlbuild-rules/src/engine/tests/test_query_facts.rs index b03abed424..fa81d833c5 100644 --- a/crates/sqlbuild-rules/src/engine/tests/test_query_facts.rs +++ b/crates/sqlbuild-rules/src/engine/tests/test_query_facts.rs @@ -32,13 +32,13 @@ fn given_dialect_sql_when_reading_query_facts_then_findings_match_the_authored_s expected_faults: &[], }, QueryFactsTestCase { - description: "an apostrophe in a BigQuery # comment opens no string", + description: "an apostrophe in a BigQuery # comment opens no string, reported unavailable until SQLBuild parses the dialect", dialect: "bigquery", query_sql: "WITH imported AS (\n # don't read the archive\n SELECT * FROM __ref(\"orders\")\n)\nSELECT order_id FROM imported", select: IMPORT_RULES, enum_columns: &[], columns: json!([]), - expected_faults: &[], + expected_faults: &[("L007", 1, 1, "cannot parse bigquery SQL for built-in rules")], }, QueryFactsTestCase { description: "a DuckDB FROM-first import CTE star is exempt without a SELECT keyword", @@ -115,6 +115,30 @@ fn given_dialect_sql_when_reading_query_facts_then_findings_match_the_authored_s columns: json!([]), expected_faults: &[("SQBRDECLARATION102", 1, 35, "3 < 7")], }, + QueryFactsTestCase { + description: "DuckDB literal comparisons on separate lines each keep their own position", + dialect: "duckdb", + query_sql: "SELECT order_id FROM orders\nWHERE 3 < 7\n AND 5 > 2", + select: NUMERIC_RULE, + enum_columns: &[], + columns: json!([]), + expected_faults: &[ + ("SQBRDECLARATION102", 2, 7, "3 < 7"), + ("SQBRDECLARATION102", 3, 7, "5 > 2"), + ], + }, + QueryFactsTestCase { + description: "Snowflake literal comparisons on separate lines each keep their own position", + dialect: "snowflake", + query_sql: "SELECT order_id FROM orders\nWHERE 3 < 7\n AND 5 > 2", + select: NUMERIC_RULE, + enum_columns: &[], + columns: json!([]), + expected_faults: &[ + ("SQBRDECLARATION102", 2, 7, "3 < 7"), + ("SQBRDECLARATION102", 3, 7, "5 > 2"), + ], + }, QueryFactsTestCase { description: "unaliased later union branches keep their cast types", dialect: "duckdb", @@ -181,22 +205,22 @@ fn given_dialect_sql_when_reading_query_facts_then_findings_match_the_authored_s )], }, QueryFactsTestCase { - description: "a BigQuery double-quoted string compared with an enum column", + description: "a BigQuery double-quoted string compared with an enum column, reported unavailable until SQLBuild parses the dialect", dialect: "bigquery", query_sql: "SELECT order_id FROM orders WHERE status = \"active\"", select: ENUM_RULE, enum_columns: STATUS, columns: json!([]), - expected_faults: &[("SQBRDECLARATION101", 1, 35, "")], + expected_faults: &[("L007", 1, 1, "cannot parse bigquery SQL for built-in rules")], }, QueryFactsTestCase { - description: "a BigQuery triple-quoted string compared with an enum column", + description: "a BigQuery triple-quoted string compared with an enum column, reported unavailable until SQLBuild parses the dialect", dialect: "bigquery", query_sql: "SELECT order_id FROM orders WHERE status = '''active'''", select: ENUM_RULE, enum_columns: STATUS, columns: json!([]), - expected_faults: &[("SQBRDECLARATION101", 1, 35, "")], + expected_faults: &[("L007", 1, 1, "cannot parse bigquery SQL for built-in rules")], }, QueryFactsTestCase { description: "a Postgres escape string compared with an enum column", @@ -226,32 +250,22 @@ fn given_dialect_sql_when_reading_query_facts_then_findings_match_the_authored_s expected_faults: &[("SQBRDECLARATION102", 1, 35, "amount == 42")], }, QueryFactsTestCase { - description: "a parenthesized T-SQL TOP argument is not projection", + description: "a parenthesized T-SQL TOP argument is not projection, reported unavailable until SQLBuild parses the dialect", dialect: "mssql", query_sql: "SELECT TOP (5) amount + 1 AS amount FROM orders", select: OUTPUT_TYPE_RULE, enum_columns: &[], columns: json!([{"name": "amount", "type": "INTEGER", "type_proven": true}]), - expected_faults: &[( - "SQBRCONTRACT105", - 1, - 16, - "calculated without an outer explicit cast", - )], + expected_faults: &[("L007", 1, 1, "cannot parse mssql SQL for built-in rules")], }, QueryFactsTestCase { - description: "a T-SQL TOP PERCENT clause is not projection", + description: "a T-SQL TOP PERCENT clause is not projection, reported unavailable until SQLBuild parses the dialect", dialect: "mssql", query_sql: "SELECT TOP 5 PERCENT amount + 1 AS amount FROM orders", select: OUTPUT_TYPE_RULE, enum_columns: &[], columns: json!([{"name": "amount", "type": "INTEGER", "type_proven": true}]), - expected_faults: &[( - "SQBRCONTRACT105", - 1, - 22, - "calculated without an outer explicit cast", - )], + expected_faults: &[("L007", 1, 1, "cannot parse mssql SQL for built-in rules")], }, QueryFactsTestCase { description: "a placeholder in relation position still parses", diff --git a/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs b/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs index 6f5a147e31..4cc9c14753 100644 --- a/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs +++ b/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs @@ -1,8 +1,8 @@ use crate::query_facts::_helpers::shape::stub_call; use crate::query_facts::_helpers::tokens::{ComparisonOperator, QueryTokens, SelectTokens}; use crate::query_facts::constants::{ - DATE_DIFF_ARGUMENTS, DATE_DIFF_CALLS, DUCKDB_DIALECTS, MAX_FUNCTION_CALL_DEPTH, POSTGRES_DIALECTS, SNOWFLAKE_DIALECT, SQLITE_DIALECT, - TABLE_FUNCTION_STUB, TRIPLE_QUOTE_LENGTH, + DATE_DIFF_ARGUMENTS, DATE_DIFF_CALLS, DUCKDB_DIALECTS, MAX_FUNCTION_CALL_DEPTH, + POSTGRES_DIALECTS, SNOWFLAKE_DIALECT, SQLITE_DIALECT, TABLE_FUNCTION_STUB, TRIPLE_QUOTE_LENGTH, }; use crate::query_facts::models::{ CallKind, ColumnFact, ComparisonFacts, ComparisonSpan, CteFacts, ExpressionItem, ItemFacts, @@ -10,7 +10,9 @@ use crate::query_facts::models::{ }; use sqlquack_syntax::expressions::names::models::{Identifier, Literal, TableRef}; use sqlquack_syntax::expressions::queries::models::{Select, With}; -use sqlquack_syntax::{CallMode, ComplexityGuardOptions, Dialect, Expression, ExpressionWalk, ParseOptions}; +use sqlquack_syntax::{ + CallMode, ComplexityGuardOptions, Dialect, Expression, ExpressionWalk, ParseOptions, +}; use std::cell::{Cell, RefCell}; use std::collections::{HashMap, HashSet}; use std::ops::Range; @@ -318,14 +320,16 @@ impl Builder<'_> { let range = item.map(|item| self.tokens.expression_range(item, alias.is_some())); let unwrapped = unwrap_paren(value); let cast_type = match unwrapped { - Expression::Cast(cast) | Expression::TryCast(cast) | Expression::SafeCast(cast) => range - .as_ref() - .and_then(|range| self.tokens.cast_type(range)) - .or_else(|| { - let span = cast.span.0?; - self.tokens - .cast_type(&self.tokens.covering(span.start, span.end)) - }), + Expression::Cast(cast) | Expression::TryCast(cast) | Expression::SafeCast(cast) => { + range + .as_ref() + .and_then(|range| self.tokens.cast_type(range)) + .or_else(|| { + let span = cast.span.0?; + self.tokens + .cast_type(&self.tokens.covering(span.start, span.end)) + }) + } _ => None, }; ItemFacts::Expression(ExpressionItem { diff --git a/crates/sqlbuild-rules/src/query_facts/constants.rs b/crates/sqlbuild-rules/src/query_facts/constants.rs index a87219f95e..c350e969ae 100644 --- a/crates/sqlbuild-rules/src/query_facts/constants.rs +++ b/crates/sqlbuild-rules/src/query_facts/constants.rs @@ -19,6 +19,7 @@ pub(crate) const SNOWFLAKE_DIALECT: &str = "snowflake"; /// The SQLite dialect name. pub(crate) const SQLITE_DIALECT: &str = "sqlite"; /// Snowflake calls whose quoted first argument is a date part when they take three arguments. -pub(crate) const DATE_DIFF_CALLS: [&str; 4] = ["DATEDIFF", "DATE_DIFF", "TIMEDIFF", "TIMESTAMPDIFF"]; +pub(crate) const DATE_DIFF_CALLS: [&str; 4] = + ["DATEDIFF", "DATE_DIFF", "TIMEDIFF", "TIMESTAMPDIFF"]; /// The argument count at which a `DATE_DIFF_CALLS` call reads its first argument as a date part. pub(crate) const DATE_DIFF_ARGUMENTS: usize = 3; diff --git a/crates/sqlbuild-rules/src/query_facts/main/facts_available.rs b/crates/sqlbuild-rules/src/query_facts/main/facts_available.rs new file mode 100644 index 0000000000..3fc8193c5f --- /dev/null +++ b/crates/sqlbuild-rules/src/query_facts/main/facts_available.rs @@ -0,0 +1,6 @@ +//! `facts_available`: whether SQLBuild parses a dialect's SQL for built-in rules. + +/// Whether SQLBuild parses `dialect`'s SQL for the built-in rules that read query facts. +pub(crate) fn facts_available(dialect: &str) -> bool { + crate::query_facts::_helpers::facts::facts_dialect(dialect).is_some() +} diff --git a/crates/sqlbuild-rules/src/query_facts/main/mod.rs b/crates/sqlbuild-rules/src/query_facts/main/mod.rs index 12999bd6c5..a4b8b0f0c7 100644 --- a/crates/sqlbuild-rules/src/query_facts/main/mod.rs +++ b/crates/sqlbuild-rules/src/query_facts/main/mod.rs @@ -1,4 +1,5 @@ pub mod dependency_texts; +pub mod facts_available; pub mod model_shape; pub mod parse_fixture; pub mod query_facts; diff --git a/crates/sqlbuild-rules/src/query_facts/main/query_facts.rs b/crates/sqlbuild-rules/src/query_facts/main/query_facts.rs index dba714d2c9..e246caab16 100644 --- a/crates/sqlbuild-rules/src/query_facts/main/query_facts.rs +++ b/crates/sqlbuild-rules/src/query_facts/main/query_facts.rs @@ -1,6 +1,6 @@ use crate::query_facts::models::QueryFacts; -/// Parse normalized model SQL with Polyglot and read the facts built-in rules evaluate. +/// Parse normalized model SQL with SQLQuack and read the facts built-in rules evaluate. pub(crate) fn query_facts(sql: &str, dialect: &str) -> Result { crate::query_facts::_helpers::facts::query_facts(sql, dialect) } diff --git a/crates/sqlbuild-rules/src/query_facts/models.rs b/crates/sqlbuild-rules/src/query_facts/models.rs index c36b32e080..a5688f1e06 100644 --- a/crates/sqlbuild-rules/src/query_facts/models.rs +++ b/crates/sqlbuild-rules/src/query_facts/models.rs @@ -193,7 +193,7 @@ impl QueryFacts { } } -/// An SQL-test fixture query parsed with Polyglot, with the function-call facts its rules read. +/// An SQL-test fixture query parsed with SQLQuack, with the function-call facts its rules read. #[derive(Debug)] pub(crate) struct ParsedFixture { pub(crate) statement: Expression, diff --git a/crates/sqlbuild-rules/src/rules/_helpers/evaluation.rs b/crates/sqlbuild-rules/src/rules/_helpers/evaluation.rs index 733cabe15d..4e3256ff5e 100644 --- a/crates/sqlbuild-rules/src/rules/_helpers/evaluation.rs +++ b/crates/sqlbuild-rules/src/rules/_helpers/evaluation.rs @@ -6,9 +6,12 @@ use crate::constants::{ }; use crate::constants::{ MIN_CUSTOM_RULE_TEST_CASES_KEY, MODEL_NAME_LAYERS, PUBLISHED_READ_CODE, PUBLISHED_SHAPE_CODE, + RULES_UNAVAILABLE_REMEDIATION, }; +use crate::lint_text::constants::LINT_UNAVAILABLE_CODE; use crate::models::{Declaration, EvaluateRequest, Fault, Model, RuleMetadata, RulesConfig}; use crate::query_facts::main::dependency_texts::dependency_texts; +use crate::query_facts::main::facts_available::facts_available; use crate::query_facts::main::model_shape::model_shape; use crate::query_facts::main::query_facts::query_facts; use crate::query_facts::models::{ @@ -134,6 +137,17 @@ fn evaluate_model_inner(request: ModelEvaluationRequest<'_>) -> Result) -> faults.into_inner() } +/// One non-failing warning that `dialect` SQL is not parsed, so the built-in rules `codes` did not run on `path`. +pub(crate) fn rules_unavailable<'a>( + path: &str, + codes: impl Iterator, + dialect: &str, +) -> Fault { + Fault { + unevaluated: false, + code: LINT_UNAVAILABLE_CODE.to_owned(), + path: path.to_owned(), + line: 1, + column: 1, + message: format!( + "SQLBuild cannot parse {dialect} SQL for built-in rules yet, so these rules were not evaluated: {}", + codes.collect::>().join(", ") + ), + remediation: RULES_UNAVAILABLE_REMEDIATION.to_owned(), + } +} + fn requires_parsed_model(selected: &BTreeMap) -> bool { selected.values().any(|rule| requires_model_query(rule)) } diff --git a/crates/sqlbuild-rules/src/rules/_helpers/sql_test_rules.rs b/crates/sqlbuild-rules/src/rules/_helpers/sql_test_rules.rs index b4ac7cc0ac..9eb7006c5d 100644 --- a/crates/sqlbuild-rules/src/rules/_helpers/sql_test_rules.rs +++ b/crates/sqlbuild-rules/src/rules/_helpers/sql_test_rules.rs @@ -2,19 +2,20 @@ use crate::models::{ DeclarationKind, EvaluateRequest, Fault, Model, ResourceKind, RuleMetadata, RulesConfig, ScopeResource, SqlScenarioFact, SqlTestCteFact, SqlTestFact, SqlTestMode, }; +use crate::query_facts::main::facts_available::facts_available; use crate::query_facts::main::model_shape::model_shape; use crate::query_facts::main::parse_fixture::parse_fixture; use crate::query_facts::main::stub_call::stub_call; use crate::query_facts::models::{CallKind, ParsedFixture}; -use crate::rules::_helpers::evaluation::path_fault; +use crate::rules::_helpers::evaluation::{path_fault, rules_unavailable}; use crate::rules::models::ProjectEvaluationRequest; +use serde::{Deserialize, Serialize}; +use sqlbuild_analysis::compiler::main::sql_test_fixture_facts::fixture_facts; use sqlquack_syntax::Expression; use sqlquack_syntax::expressions::names::models::Literal; use sqlquack_syntax::expressions::queries::models::Select; -use serde::{Deserialize, Serialize}; -use sqlbuild_analysis::compiler::main::sql_test_fixture_facts::fixture_facts; use std::cell::RefCell; -use std::collections::{BTreeMap, HashMap}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::path::Path; use std::rc::Rc; @@ -43,17 +44,35 @@ fn evaluated_project(evaluation: ProjectEvaluationRequest<'_>) -> Result> = HashMap::new(); - for rule in evaluation.selected.values().filter(|rule| { - matches!( - rule.code.as_str(), - "SQBRTEST201" | "SQBRTEST202" | EMPTY_INPUT_RULE_CODE - ) - }) { + let fixture_rules: Vec<&RuleMetadata> = evaluation + .selected + .values() + .copied() + .filter(|rule| { + matches!( + rule.code.as_str(), + "SQBRTEST201" | "SQBRTEST202" | EMPTY_INPUT_RULE_CODE + ) + }) + .collect(); + let dialect: &str = &evaluation.request.dialect; + if !fixture_rules.is_empty() && !facts_available(dialect) { + let paths: BTreeSet<&str> = tests + .iter() + .filter(|test| reads_fixtures(test)) + .map(|test| test.source_path.as_str()) + .collect(); + faults.extend(paths.into_iter().map(|path| { + rules_unavailable( + path, + fixture_rules.iter().map(|rule| rule.code.as_str()), + dialect, + ) + })); + } + for rule in fixture_rules.iter().filter(|_| facts_available(dialect)) { for test in &tests { - if !matches!(test.mode, SqlTestMode::Model) - || test.has_macro_mocks - || test.has_model_query_overrides - { + if !reads_fixtures(test) { continue; } for cte in test @@ -115,6 +134,13 @@ fn evaluated_project(evaluation: ProjectEvaluationRequest<'_>) -> Result bool { + matches!(test.mode, SqlTestMode::Model) + && !test.has_macro_mocks + && !test.has_model_query_overrides +} + fn canonical_roots( rule: &RuleMetadata, tests: &[&SqlTestFact], From 7a85582c3a919895d00cfee3d08c4806422d716a Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 02:45:46 +0000 Subject: [PATCH 015/106] wip: e2e for built-in rules on unparsed dialects --- .../cli/commands/main/rules/_test_types.py | 9 +++ .../main/rules/test_sql_quality_rules.py | 62 +++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/rules/_test_types.py b/tests/e2e/src/sqlbuild/cli/commands/main/rules/_test_types.py index 977b4079b0..61a0546429 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/rules/_test_types.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/rules/_test_types.py @@ -179,6 +179,15 @@ class RankingSortDefaultCase: expected_codes: tuple[str, ...] +@dataclass(frozen=True) +class UnparsedDialectRulesCase: + description: str + adapter_toml: str + expected_codes: tuple[str, ...] + expected_message: str + expected_exit: int + + @dataclass(frozen=True) class ReplayedFindingExitCase: description: str diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/rules/test_sql_quality_rules.py b/tests/e2e/src/sqlbuild/cli/commands/main/rules/test_sql_quality_rules.py index a22ba9759c..f7c51df4c7 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/rules/test_sql_quality_rules.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/rules/test_sql_quality_rules.py @@ -18,6 +18,7 @@ RankingSortDefaultCase, ReplayedFindingExitCase, SqlQualityRuleCase, + UnparsedDialectRulesCase, ) from tests.e2e.src.sqlbuild.cli.commands.main.rules.helpers import ( diagnostic_codes, @@ -622,3 +623,64 @@ def test_given_cached_sql_finding_when_running_commands_then_only_rule_findings_ if __name__ == "__main__": pytest.main([__file__, "-vv"]) + + +@pytest.mark.parametrize( + "test_case", + ( + UnparsedDialectRulesCase( + description="a parsed dialect reports the built-in rule finding", + adapter_toml='adapter = "duckdb"\n', + expected_codes=("SQBRMODEL102",), + expected_message="SELECT * is allowed only", + expected_exit=1, + ), + UnparsedDialectRulesCase( + description="an unparsed dialect warns once per model and fails nothing", + adapter_toml=( + 'adapter = "bigquery"\ndefault_target = "dev"\n\n' + '[targets.dev]\ndatabase = "warehouse"\nschema = "analytics"\n' + ), + expected_codes=("L007", "L007"), + expected_message=( + "SQLBuild cannot parse bigquery SQL for built-in rules yet," + " so these rules were not evaluated: SQBRMODEL102" + ), + expected_exit=0, + ), + ), + ids=lambda case: case.description, +) +def test_given_project_dialect_when_running_built_in_rules_then_unparsed_dialects_only_warn( + test_case: UnparsedDialectRulesCase, tmp_path: Path +) -> None: + write_quality_project( + project_dir=tmp_path, + toml=( + 'name = "orders"\n' + + test_case.adapter_toml + + '\n[rules]\nselect = ["SQBRMODEL102"]\n' + ), + upstream=_RAW_HEADER + _UPSTREAM_SQL, + summary=_SUMMARY_HEADER + 'SELECT * FROM __ref("raw_orders")\n', + ) + + compiled: subprocess.CompletedProcess[str] = run_unevaluated_rules_cli( + tmp_path, "compile", "--json" + ) + ran: subprocess.CompletedProcess[str] = run_unevaluated_rules_cli( + tmp_path, "rules", "run", "SQBRMODEL102" + ) + + messages: list[str] = [ + str(item["message"]) for item in json.loads(compiled.stdout)["diagnostics"] + ] + assert ( + diagnostic_codes(compiled), + all(test_case.expected_message in message for message in messages), + (compiled.returncode, ran.returncode), + ) == ( + test_case.expected_codes, + True, + (test_case.expected_exit, test_case.expected_exit), + ), compiled.stdout + compiled.stderr + ran.stdout + ran.stderr From 4fad955e74e30c3e548db10a29f1717715bb9f19 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 02:54:51 +0000 Subject: [PATCH 016/106] chore(deps): bump sqlquack to a9dde6a0 --- Cargo.lock | 24 ++++++++++++------------ Cargo.toml | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a9f5f63d09..6269367f6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1319,7 +1319,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2174,7 +2174,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cde48f36530cbd91b65f050101e6c056d6eb743cea2a8f0f19beef69c6742ff8" dependencies = [ "scopeguard", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3199,7 +3199,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3637,7 +3637,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4091,7 +4091,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4499,7 +4499,7 @@ dependencies = [ [[package]] name = "sqlquack-syntax" version = "0.2.0" -source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=4d0c6bc56da0e44eb164601a2af673ae7998257d#4d0c6bc56da0e44eb164601a2af673ae7998257d" +source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=a9dde6a00b144c6306425cfa84516ad816d8c49c#a9dde6a00b144c6306425cfa84516ad816d8c49c" dependencies = [ "serde", "serde_json", @@ -4512,7 +4512,7 @@ dependencies = [ [[package]] name = "sqlquack-syntax-derive" version = "0.2.0" -source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=4d0c6bc56da0e44eb164601a2af673ae7998257d#4d0c6bc56da0e44eb164601a2af673ae7998257d" +source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=a9dde6a00b144c6306425cfa84516ad816d8c49c#a9dde6a00b144c6306425cfa84516ad816d8c49c" dependencies = [ "proc-macro2", "quote", @@ -4522,7 +4522,7 @@ dependencies = [ [[package]] name = "sqlquack-syntax-postgres" version = "0.2.0" -source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=4d0c6bc56da0e44eb164601a2af673ae7998257d#4d0c6bc56da0e44eb164601a2af673ae7998257d" +source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=a9dde6a00b144c6306425cfa84516ad816d8c49c#a9dde6a00b144c6306425cfa84516ad816d8c49c" dependencies = [ "sqlquack-syntax", ] @@ -4618,7 +4618,7 @@ dependencies = [ "cfg-if", "libc", "psm", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4740,7 +4740,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5054,7 +5054,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5387,7 +5387,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f03cafe024..65a30d1f3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -177,8 +177,8 @@ sqlbuild-state = { path = "crates/sqlbuild-state" } # SQL lint Rules, quality Rules and the formatter. sqlclean = { git = "https://github.com/sqlstack-io/sqlclean.git", rev = "cf79656d247277a30bc81ef39b6dee420e721221" } # The lossless SQL tokenizer behind query fingerprints. -sqlquack-syntax = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "4d0c6bc56da0e44eb164601a2af673ae7998257d" } -sqlquack-syntax-postgres = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "4d0c6bc56da0e44eb164601a2af673ae7998257d" } +sqlquack-syntax = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "a9dde6a00b144c6306425cfa84516ad816d8c49c" } +sqlquack-syntax-postgres = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "a9dde6a00b144c6306425cfa84516ad816d8c49c" } stacker = "0.1" syn = { version = "2", features = ["full", "visit", "extra-traits"] } subtle = "2" From adc798472febfcb5d53120778dd20a0b7d2397ee Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 02:57:11 +0000 Subject: [PATCH 017/106] wip: docs and tests for built-in rules on unparsed dialects --- .../references/docs/concepts/rules.md | 26 +++++++++++++++---- .../main/rules/test_clickhouse_comments.py | 8 +++--- .../sqlbuild/cli/commands/main/test_rules.py | 13 +++++++--- .../src/content/docs/docs/concepts/rules.mdx | 25 ++++++++++++++---- 4 files changed, 54 insertions(+), 18 deletions(-) diff --git a/src/sqlbuild/.agents/skills/sqlbuild/references/docs/concepts/rules.md b/src/sqlbuild/.agents/skills/sqlbuild/references/docs/concepts/rules.md index 0b2aac11f9..2f7792a7a3 100644 --- a/src/sqlbuild/.agents/skills/sqlbuild/references/docs/concepts/rules.md +++ b/src/sqlbuild/.agents/skills/sqlbuild/references/docs/concepts/rules.md @@ -12,6 +12,7 @@ Online: https://sqlbuild.com/docs/concepts/rules/ - How Rules work - SQL Rules - Built-in and custom Rules +- Dialect support - One definition of project validity - Rules compared with tests, audits, and contracts - Findings and intentional exceptions @@ -144,11 +145,6 @@ These checks run through ordinary compilation. There is intentionally no separat lifecycle. If an SQL requirement affects whether the project is acceptable, the compiler enforces it with the rest of the configured Rules. -If SQL Rules cannot run because SQLBuild does not parse the project's dialect yet, `sqb compile` -and `sqb rules run` show one `L007` warning per file that names the dialect. `L007` never changes -an exit code, including for `sqb plan` and `sqb build`: it reports a gap in SQLBuild's dialect -support, not a problem in your SQL, so the file's SQL Rules simply did not run. - Not every traditional lint concern should become a diagnostic. SQLBuild separates three kinds of SQL responsibility: @@ -183,6 +179,26 @@ select = [ Define typed model or project checks over compiler-owned facts with `@rule`, `RuleContext`, and `Finding`. +## Dialect support + +SQL Rules, and the built-in Rules that read SQL, such as `SQBRMODEL102` and the SQL-test Rules +`SQBRTEST201` to `SQBRTEST203`, run only where SQLBuild parses the project's dialect. Built-in Rules +that read SQL support these adapters: + +| Adapter | Built-in Rules that read SQL | +|---|---| +| DuckDB, MotherDuck | Supported | +| PostgreSQL | Supported | +| Snowflake | Supported | +| SQLite | Supported | +| BigQuery, ClickHouse, Databricks, SQL Server | Not yet | + +Where SQLBuild does not parse the dialect yet, `sqb compile` and `sqb rules run` show `L007` +warnings that name it: one per file for SQL Rules, and one per model or SQL test listing the +built-in Rules that did not run. `L007` never changes an exit code, including for `sqb plan` and +`sqb build`: it reports a gap in SQLBuild's dialect support, not a problem in your SQL. A syntax +error in a supported dialect still fails the Rules that read it. + ## One definition of project validity `sqb compile` is authoritative. SQLBuild evaluates a project in this order: diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/rules/test_clickhouse_comments.py b/tests/e2e/src/sqlbuild/cli/commands/main/rules/test_clickhouse_comments.py index 0cc24a9f8b..ba1ee05735 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/rules/test_clickhouse_comments.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/rules/test_clickhouse_comments.py @@ -23,14 +23,14 @@ "test_case", [ ClickHouseCommentE2ETestCase( - description="a hash comment with an apostrophe is skipped", + description="a hash comment with an apostrophe is skipped, reported unavailable until SQLBuild parses the dialect", comment="# don't keep the star", - expected_location="models/orders.sql:4:8 [SQBRMODEL102]", + expected_location="models/orders.sql:1:1 [L007]", ), ClickHouseCommentE2ETestCase( - description="a double-slash comment with an apostrophe is skipped", + description="a double-slash comment with an apostrophe is skipped, reported unavailable until SQLBuild parses the dialect", comment="// don't keep the star", - expected_location="models/orders.sql:4:8 [SQBRMODEL102]", + expected_location="models/orders.sql:1:1 [L007]", ), ], ids=lambda case: case.description, diff --git a/tests/integration/src/sqlbuild/cli/commands/main/test_rules.py b/tests/integration/src/sqlbuild/cli/commands/main/test_rules.py index 66c39436dc..e10b1b007d 100644 --- a/tests/integration/src/sqlbuild/cli/commands/main/test_rules.py +++ b/tests/integration/src/sqlbuild/cli/commands/main/test_rules.py @@ -2142,10 +2142,14 @@ def test_given_commented_dependency_call_when_running_import_rule_then_comment_i @pytest.mark.parametrize( "test_case", - [RulesIntegrationTestCase("quoted comment marker preserves real call", 1, "SQBRMODEL101")], + [ + RulesIntegrationTestCase( + "quoted comment marker" + ", reported unavailable until SQLBuild parses the dialect", 0, "SQBRMODEL101" + ) + ], ids=lambda case: case.description, ) -def test_given_bigquery_quoted_comment_marker_when_running_import_rule_then_real_call_is_detected( +def test_given_bigquery_quoted_comment_marker_when_running_import_rule_then_reports_dialect_unavailable( test_case: RulesIntegrationTestCase, tmp_path: Path, capsys: pytest.CaptureFixture[str], @@ -2194,8 +2198,9 @@ def test_given_bigquery_quoted_comment_marker_when_running_import_rule_then_real payload: dict[str, object] = json.loads(capsys.readouterr().out) findings: list[dict[str, object]] = cast(list[dict[str, object]], payload["findings"]) assert [finding["message"] for finding in findings] == [ - 'import CTE "imported_orders" contains transformation logic' - ] + "SQLBuild cannot parse bigquery SQL for built-in rules yet," + " so these rules were not evaluated: SQBRMODEL101" + ] * 2 @pytest.mark.parametrize( diff --git a/website/src/content/docs/docs/concepts/rules.mdx b/website/src/content/docs/docs/concepts/rules.mdx index 884993b4a5..a1e0eb1845 100644 --- a/website/src/content/docs/docs/concepts/rules.mdx +++ b/website/src/content/docs/docs/concepts/rules.mdx @@ -132,11 +132,6 @@ These checks run through ordinary compilation. There is intentionally no separat lifecycle. If an SQL requirement affects whether the project is acceptable, the compiler enforces it with the rest of the configured Rules. -If SQL Rules cannot run because SQLBuild does not parse the project's dialect yet, `sqb compile` -and `sqb rules run` show one `L007` warning per file that names the dialect. `L007` never changes -an exit code, including for `sqb plan` and `sqb build`: it reports a gap in SQLBuild's dialect -support, not a problem in your SQL, so the file's SQL Rules simply did not run. - Not every traditional lint concern should become a diagnostic. SQLBuild separates three kinds of SQL responsibility: @@ -173,6 +168,26 @@ select = [ `Finding`. +## Dialect support + +SQL Rules, and the built-in Rules that read SQL, such as `SQBRMODEL102` and the SQL-test Rules +`SQBRTEST201` to `SQBRTEST203`, run only where SQLBuild parses the project's dialect. Built-in Rules +that read SQL support these adapters: + +| Adapter | Built-in Rules that read SQL | +|---|---| +| DuckDB, MotherDuck | Supported | +| PostgreSQL | Supported | +| Snowflake | Supported | +| SQLite | Supported | +| BigQuery, ClickHouse, Databricks, SQL Server | Not yet | + +Where SQLBuild does not parse the dialect yet, `sqb compile` and `sqb rules run` show `L007` +warnings that name it: one per file for SQL Rules, and one per model or SQL test listing the +built-in Rules that did not run. `L007` never changes an exit code, including for `sqb plan` and +`sqb build`: it reports a gap in SQLBuild's dialect support, not a problem in your SQL. A syntax +error in a supported dialect still fails the Rules that read it. + ## One definition of project validity `sqb compile` is authoritative. SQLBuild evaluates a project in this order: From 088bd97557a7cdb681db950952b18b8ec7bc12f8 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 03:00:19 +0000 Subject: [PATCH 018/106] wip: format tests --- .../cli/commands/main/rules/test_sql_quality_rules.py | 4 +--- .../integration/src/sqlbuild/cli/commands/main/test_rules.py | 4 +++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/rules/test_sql_quality_rules.py b/tests/e2e/src/sqlbuild/cli/commands/main/rules/test_sql_quality_rules.py index f7c51df4c7..9199ee3e4b 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/rules/test_sql_quality_rules.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/rules/test_sql_quality_rules.py @@ -657,9 +657,7 @@ def test_given_project_dialect_when_running_built_in_rules_then_unparsed_dialect write_quality_project( project_dir=tmp_path, toml=( - 'name = "orders"\n' - + test_case.adapter_toml - + '\n[rules]\nselect = ["SQBRMODEL102"]\n' + 'name = "orders"\n' + test_case.adapter_toml + '\n[rules]\nselect = ["SQBRMODEL102"]\n' ), upstream=_RAW_HEADER + _UPSTREAM_SQL, summary=_SUMMARY_HEADER + 'SELECT * FROM __ref("raw_orders")\n', diff --git a/tests/integration/src/sqlbuild/cli/commands/main/test_rules.py b/tests/integration/src/sqlbuild/cli/commands/main/test_rules.py index e10b1b007d..1ad17c825b 100644 --- a/tests/integration/src/sqlbuild/cli/commands/main/test_rules.py +++ b/tests/integration/src/sqlbuild/cli/commands/main/test_rules.py @@ -2144,7 +2144,9 @@ def test_given_commented_dependency_call_when_running_import_rule_then_comment_i "test_case", [ RulesIntegrationTestCase( - "quoted comment marker" + ", reported unavailable until SQLBuild parses the dialect", 0, "SQBRMODEL101" + "quoted comment marker" + ", reported unavailable until SQLBuild parses the dialect", + 0, + "SQBRMODEL101", ) ], ids=lambda case: case.description, From 8b2a9d3ec5eab3880b0ff079e4cee780580dc20b Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 03:14:04 +0000 Subject: [PATCH 019/106] fix(rules): refuse rule facts for statements deeper than 512 levels --- .../src/engine/tests/test_evaluation.rs | 45 +++++++++++++------ .../src/engine/tests/test_types.rs | 2 +- .../src/query_facts/_helpers/facts.rs | 20 ++++++++- .../src/query_facts/constants.rs | 2 + 4 files changed, 54 insertions(+), 15 deletions(-) diff --git a/crates/sqlbuild-rules/src/engine/tests/test_evaluation.rs b/crates/sqlbuild-rules/src/engine/tests/test_evaluation.rs index 7b77c64f26..0dbcbddc3e 100644 --- a/crates/sqlbuild-rules/src/engine/tests/test_evaluation.rs +++ b/crates/sqlbuild-rules/src/engine/tests/test_evaluation.rs @@ -518,7 +518,8 @@ fn given_snowflake_expression_when_evaluating_rules_then_uses_project_dialect() "CAST(amounts AS ARRAY(NUMBER(38, 10))) AS amounts, ", "OBJECT_CONSTRUCT_KEEP_NULL('group_id', group_id) AS details, ", "FROM __table_fn(\"inventory_items\")(42)" - ), + ) + .to_owned(), expected_code: "SQBRCONTRACT101", }]; @@ -550,7 +551,8 @@ fn given_dialect_quoting_around_table_functions_when_evaluating_rules_then_model query_sql: concat!( "SELECT 'it\\'s (' AS label, item_id ", "FROM __table_fn(\"inventory_items\")(42)" - ), + ) + .to_owned(), expected_code: "SQBRCONTRACT101", }, test_types::DialectEvaluationTestCase { @@ -559,7 +561,8 @@ fn given_dialect_quoting_around_table_functions_when_evaluating_rules_then_model query_sql: concat!( "SELECT item_id ", "FROM __table_fn(\"inventory_items\", 'a\\')')(42)" - ), + ) + .to_owned(), expected_code: "L007", }, test_types::DialectEvaluationTestCase { @@ -568,7 +571,8 @@ fn given_dialect_quoting_around_table_functions_when_evaluating_rules_then_model query_sql: concat!( "SELECT item_id ", "FROM __table_fn(\"inventory_items\", `batch)size`)(42)" - ), + ) + .to_owned(), expected_code: "L007", }, ]; @@ -593,12 +597,27 @@ fn given_dialect_quoting_around_table_functions_when_evaluating_rules_then_model #[test] fn given_unparseable_model_when_evaluating_then_retains_named_rule_fault() -> Result<(), String> { let project_dir = TempDir::new().map_err(|error| error.to_string())?; - let test_cases = [test_types::DialectEvaluationTestCase { - description: "invalid query is missing evidence rather than a passing contract", - dialect: "duckdb", - query_sql: "SELECT (", - expected_code: "SQBRCONTRACT101", - }]; + let deep_sql = format!("SELECT {}1 AS order_id FROM orders", "1 + ".repeat(50_000)); + let test_cases = [ + test_types::DialectEvaluationTestCase { + description: "invalid query is missing evidence rather than a passing contract", + dialect: "duckdb", + query_sql: "SELECT (".to_owned(), + expected_code: "SQBRCONTRACT101", + }, + test_types::DialectEvaluationTestCase { + description: "a DuckDB expression deeper than the AST limit is refused, not a crash", + dialect: "duckdb", + query_sql: deep_sql.clone(), + expected_code: "SQBRCONTRACT101", + }, + test_types::DialectEvaluationTestCase { + description: "a Snowflake expression deeper than the AST limit is refused, not a crash", + dialect: "snowflake", + query_sql: deep_sql, + expected_code: "SQBRCONTRACT101", + }, + ]; for test_case in test_cases { let config = json!({"select": ["SQBRCONTRACT101"], "cache": {"enabled": false}}); let mut request: Value = serde_json::from_str(&helpers::request(&project_dir, &config)) @@ -641,19 +660,19 @@ fn given_comment_apostrophes_when_generic_fallback_normalizes_rules_sql_then_mod test_types::DialectEvaluationTestCase { description: "line comment apostrophe before a colon path", dialect: "postgres", - query_sql: "-- don't drop the customer path\nSELECT payload:customer_id AS customer_id FROM orders", + query_sql: "-- don't drop the customer path\nSELECT payload:customer_id AS customer_id FROM orders".to_owned(), expected_code: "SQBRCONTRACT101", }, test_types::DialectEvaluationTestCase { description: "block comment apostrophe before a colon path", dialect: "postgres", - query_sql: "/* customer's path */ SELECT payload:customer_id AS customer_id FROM orders", + query_sql: "/* customer's path */ SELECT payload:customer_id AS customer_id FROM orders".to_owned(), expected_code: "SQBRCONTRACT101", }, test_types::DialectEvaluationTestCase { description: "colon inside a line comment stays commented", dialect: "postgres", - query_sql: "SELECT payload:customer_id AS customer_id -- note: it's nested\nFROM orders", + query_sql: "SELECT payload:customer_id AS customer_id -- note: it's nested\nFROM orders".to_owned(), expected_code: "SQBRCONTRACT101", }, ]; diff --git a/crates/sqlbuild-rules/src/engine/tests/test_types.rs b/crates/sqlbuild-rules/src/engine/tests/test_types.rs index fb3fd5bbea..4ac63e0f05 100644 --- a/crates/sqlbuild-rules/src/engine/tests/test_types.rs +++ b/crates/sqlbuild-rules/src/engine/tests/test_types.rs @@ -35,7 +35,7 @@ pub(crate) struct DeclarationScopeTestCase { pub(crate) struct DialectEvaluationTestCase { pub(crate) description: &'static str, pub(crate) dialect: &'static str, - pub(crate) query_sql: &'static str, + pub(crate) query_sql: String, pub(crate) expected_code: &'static str, } diff --git a/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs b/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs index 4cc9c14753..98719dae46 100644 --- a/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs +++ b/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs @@ -1,7 +1,7 @@ use crate::query_facts::_helpers::shape::stub_call; use crate::query_facts::_helpers::tokens::{ComparisonOperator, QueryTokens, SelectTokens}; use crate::query_facts::constants::{ - DATE_DIFF_ARGUMENTS, DATE_DIFF_CALLS, DUCKDB_DIALECTS, MAX_FUNCTION_CALL_DEPTH, + DATE_DIFF_ARGUMENTS, DATE_DIFF_CALLS, DUCKDB_DIALECTS, MAX_AST_DEPTH, MAX_FUNCTION_CALL_DEPTH, POSTGRES_DIALECTS, SNOWFLAKE_DIALECT, SQLITE_DIALECT, TABLE_FUNCTION_STUB, TRIPLE_QUOTE_LENGTH, }; use crate::query_facts::models::{ @@ -48,12 +48,30 @@ fn parse_query(sql: &str, dialect_name: &str) -> Result<(Expression, Dialect), S return Err("expected one statement".to_owned()); } let statement = statements.remove(0); + if deeper_than(&statement, MAX_AST_DEPTH) { + return Err(format!( + "E_GUARD_AST_DEPTH_EXCEEDED: value {} exceeds configured limit {MAX_AST_DEPTH}", + MAX_AST_DEPTH + 1 + )); + } if !is_query(&statement) { return Err("expected a query".to_owned()); } Ok((statement, dialect)) } +/// Whether `statement` nests deeper than `limit` levels, measured without recursion. +fn deeper_than(statement: &Expression, limit: usize) -> bool { + let mut stack: Vec<(&Expression, usize)> = vec![(statement, 1)]; + while let Some((node, depth)) = stack.pop() { + if depth > limit { + return true; + } + stack.extend(node.children().into_iter().map(|child| (child, depth + 1))); + } + false +} + fn code_tokens(sql: &str, dialect: Dialect) -> Result, String> { let tokens = sqlquack_syntax::tokenize(sql, dialect).map_err(|error| error.to_string())?; Ok(QueryTokens::new(tokens, sql)) diff --git a/crates/sqlbuild-rules/src/query_facts/constants.rs b/crates/sqlbuild-rules/src/query_facts/constants.rs index c350e969ae..70b6cd8b84 100644 --- a/crates/sqlbuild-rules/src/query_facts/constants.rs +++ b/crates/sqlbuild-rules/src/query_facts/constants.rs @@ -6,6 +6,8 @@ pub(crate) const UDF_STUB: &str = "__sqlbuild_udf_"; pub(crate) const PLACEHOLDER_PREFIX: &[u8] = b"@@@"; pub(crate) const PLACEHOLDER_VALUE: &str = "NULL"; pub(crate) const MAX_FUNCTION_CALL_DEPTH: usize = 128; +/// The deepest statement built-in rules read, Polyglot's former AST-depth limit. +pub(crate) const MAX_AST_DEPTH: usize = 512; pub(crate) const UDF_CALL: &str = "__udf"; pub(crate) const TABLE_FUNCTION_CALL: &str = "__table_fn"; pub(crate) const MIN_CAST_TOKENS: usize = 2; From d9fa004b5fef26bfbf43f7ed213efb06c40b8f6d Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 03:41:04 +0000 Subject: [PATCH 020/106] fix(rules): anchor nested comparisons and Snowflake LAST_DAY date parts --- .../src/engine/tests/test_query_facts.rs | 18 ++++++++++ .../src/query_facts/_helpers/facts.rs | 36 +++++++++++++------ .../src/query_facts/constants.rs | 13 ++++--- 3 files changed, 52 insertions(+), 15 deletions(-) diff --git a/crates/sqlbuild-rules/src/engine/tests/test_query_facts.rs b/crates/sqlbuild-rules/src/engine/tests/test_query_facts.rs index fa81d833c5..fca1058f78 100644 --- a/crates/sqlbuild-rules/src/engine/tests/test_query_facts.rs +++ b/crates/sqlbuild-rules/src/engine/tests/test_query_facts.rs @@ -115,6 +115,24 @@ fn given_dialect_sql_when_reading_query_facts_then_findings_match_the_authored_s columns: json!([]), expected_faults: &[("SQBRDECLARATION102", 1, 35, "3 < 7")], }, + QueryFactsTestCase { + description: "an anchorless comparison nested in another keeps its own position", + dialect: "snowflake", + query_sql: "SELECT order_id FROM orders\nWHERE CASE WHEN 3 = 2 THEN 10 ELSE 20 END = 1", + select: NUMERIC_RULE, + enum_columns: &[], + columns: json!([]), + expected_faults: &[("SQBRDECLARATION102", 2, 17, "3 = 2")], + }, + QueryFactsTestCase { + description: "a Snowflake LAST_DAY date part compared with an enum column is no enum literal", + dialect: "snowflake", + query_sql: "SELECT order_id FROM orders WHERE LAST_DAY(placed_at, 'month') = status", + select: ENUM_RULE, + enum_columns: STATUS, + columns: json!([]), + expected_faults: &[], + }, QueryFactsTestCase { description: "DuckDB literal comparisons on separate lines each keep their own position", dialect: "duckdb", diff --git a/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs b/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs index 98719dae46..b1ee3aa54f 100644 --- a/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs +++ b/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs @@ -1,8 +1,9 @@ use crate::query_facts::_helpers::shape::stub_call; use crate::query_facts::_helpers::tokens::{ComparisonOperator, QueryTokens, SelectTokens}; use crate::query_facts::constants::{ - DATE_DIFF_ARGUMENTS, DATE_DIFF_CALLS, DUCKDB_DIALECTS, MAX_AST_DEPTH, MAX_FUNCTION_CALL_DEPTH, - POSTGRES_DIALECTS, SNOWFLAKE_DIALECT, SQLITE_DIALECT, TABLE_FUNCTION_STUB, TRIPLE_QUOTE_LENGTH, + DUCKDB_DIALECTS, MAX_AST_DEPTH, MAX_FUNCTION_CALL_DEPTH, POSTGRES_DIALECTS, + SNOWFLAKE_DATE_PART_ARGUMENTS, SNOWFLAKE_DIALECT, SQLITE_DIALECT, TABLE_FUNCTION_STUB, + TRIPLE_QUOTE_LENGTH, }; use crate::query_facts::models::{ CallKind, ColumnFact, ComparisonFacts, ComparisonSpan, CteFacts, ExpressionItem, ItemFacts, @@ -395,7 +396,13 @@ impl Builder<'_> { return *span; } let anchors = match anchors(parts) { - (None, None) => (node.source_span().map(|span| span.start), None), + (None, None) => ( + parts.operands[0] + .source_span() + .map(|span| span.end) + .or_else(|| node.source_span().map(|span| span.start)), + None, + ), anchors => anchors, }; let span = self @@ -463,7 +470,15 @@ impl Builder<'_> { Expression::Identifier(identifier) => { fact.columns.insert(identifier.name.clone()); } - Expression::Literal(literal) if !units.contains(&std::ptr::from_ref(value)) => { + Expression::Literal(literal) if units.contains(&std::ptr::from_ref(value)) => { + if let Some(unit) = string_literal(literal) + && let Some(index) = + string_tokens.iter().position(|(_, _, text)| *text == unit) + { + string_tokens.remove(index); + } + } + Expression::Literal(literal) => { if let Some(generated) = string_literal(literal) { let position = string_tokens .iter() @@ -500,20 +515,21 @@ impl Builder<'_> { } } -/// The quoted first arguments of Snowflake's three-argument `DATEDIFF` calls, which are date parts. +/// The quoted date-part arguments of Snowflake calls such as `DATEDIFF('day', a, b)`. fn date_part_arguments(root: &Expression, dialect: Dialect) -> HashSet<*const Expression> { if dialect != Dialect::Snowflake { return HashSet::new(); } root.dfs() .filter_map(|node| match node { - Expression::Function(function) - if function.args.len() == DATE_DIFF_ARGUMENTS - && DATE_DIFF_CALLS.contains(&function.name.to_ascii_uppercase().as_str()) => - { + Expression::Function(function) => { + let name = function.name.to_ascii_uppercase(); + let (_, _, index) = SNOWFLAKE_DATE_PART_ARGUMENTS + .iter() + .find(|(call, count, _)| *call == name && *count == function.args.len())?; function .args - .first() + .get(*index) .filter(|argument| direct_string_literal(argument).is_some()) .map(std::ptr::from_ref) } diff --git a/crates/sqlbuild-rules/src/query_facts/constants.rs b/crates/sqlbuild-rules/src/query_facts/constants.rs index 70b6cd8b84..a473ddfef1 100644 --- a/crates/sqlbuild-rules/src/query_facts/constants.rs +++ b/crates/sqlbuild-rules/src/query_facts/constants.rs @@ -20,8 +20,11 @@ pub(crate) const POSTGRES_DIALECTS: [&str; 2] = ["postgres", "postgresql"]; pub(crate) const SNOWFLAKE_DIALECT: &str = "snowflake"; /// The SQLite dialect name. pub(crate) const SQLITE_DIALECT: &str = "sqlite"; -/// Snowflake calls whose quoted first argument is a date part when they take three arguments. -pub(crate) const DATE_DIFF_CALLS: [&str; 4] = - ["DATEDIFF", "DATE_DIFF", "TIMEDIFF", "TIMESTAMPDIFF"]; -/// The argument count at which a `DATE_DIFF_CALLS` call reads its first argument as a date part. -pub(crate) const DATE_DIFF_ARGUMENTS: usize = 3; +/// Snowflake calls with a date-part argument, as (name, argument count, date-part index). +pub(crate) const SNOWFLAKE_DATE_PART_ARGUMENTS: [(&str, usize, usize); 5] = [ + ("DATEDIFF", 3, 0), + ("DATE_DIFF", 3, 0), + ("TIMEDIFF", 3, 0), + ("TIMESTAMPDIFF", 3, 0), + ("LAST_DAY", 2, 1), +]; From 449de8da5e80b280164a0a1d2ee49e00ca731d14 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 04:13:41 +0000 Subject: [PATCH 021/106] feat(scenario): map scenario column types with SQLQuack's type parsers Local scenario capture no longer asks Polyglot to read warehouse column types. Postgres types go through sqlquack-syntax-postgres's type parser, and Snowflake and BigQuery types through their SQLQuack cast-target parse, behind one native function (parse_warehouse_type). DuckDB and MotherDuck spellings pass through unchanged. Databricks nested types map recursively to DuckDB's LIST, MAP and STRUCT, and a nested spelling that cannot be mapped is refused with a named message instead of falling back to JSON. Other types keep their integer width or fall back to VARCHAR. --- Cargo.lock | 15 +- Cargo.toml | 7 +- crates/sqlbuild-analysis/Cargo.toml | 1 + crates/sqlbuild-analysis/src/lib.rs | 1 + .../src/warehouse_types/constants.rs | 12 + .../src/warehouse_types/main/mod.rs | 1 + .../main/parse_warehouse_type.rs | 59 ++++ .../src/warehouse_types/mod.rs | 4 + .../src/bindings/_helpers/analysis/queries.rs | 12 + python-boundary.toml | 5 - src/sqlbuild/_native.pyi | 1 + .../sql_analysis/main/import_polyglot.py | 13 - .../scenario/_helpers/capture/columns.py | 257 +++++++++++------- src/sqlbuild/executor/scenario/constants.py | 81 +++++- .../executor/scenario/test_capture_types.py | 2 +- .../executor/scenario/_helpers/_test_types.py | 8 + .../scenario/_helpers/test_capture_columns.py | 112 +++++--- .../executor/scenario/main/test_capture.py | 6 +- 18 files changed, 420 insertions(+), 177 deletions(-) create mode 100644 crates/sqlbuild-analysis/src/warehouse_types/constants.rs create mode 100644 crates/sqlbuild-analysis/src/warehouse_types/main/mod.rs create mode 100644 crates/sqlbuild-analysis/src/warehouse_types/main/parse_warehouse_type.rs create mode 100644 crates/sqlbuild-analysis/src/warehouse_types/mod.rs delete mode 100644 src/sqlbuild/compiler/sql_analysis/main/import_polyglot.py diff --git a/Cargo.lock b/Cargo.lock index a9f5f63d09..ed3f75a96c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4143,6 +4143,7 @@ dependencies = [ "sqlbuild-sqltext", "sqlclean", "sqlquack-syntax", + "sqlquack-syntax-bigquery", "sqlquack-syntax-postgres", ] @@ -4499,7 +4500,7 @@ dependencies = [ [[package]] name = "sqlquack-syntax" version = "0.2.0" -source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=4d0c6bc56da0e44eb164601a2af673ae7998257d#4d0c6bc56da0e44eb164601a2af673ae7998257d" +source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=0ed31b9ce7c5644dde6dd8fd9b4a143458bb86c6#0ed31b9ce7c5644dde6dd8fd9b4a143458bb86c6" dependencies = [ "serde", "serde_json", @@ -4509,10 +4510,18 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "sqlquack-syntax-bigquery" +version = "0.2.0" +source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=0ed31b9ce7c5644dde6dd8fd9b4a143458bb86c6#0ed31b9ce7c5644dde6dd8fd9b4a143458bb86c6" +dependencies = [ + "sqlquack-syntax", +] + [[package]] name = "sqlquack-syntax-derive" version = "0.2.0" -source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=4d0c6bc56da0e44eb164601a2af673ae7998257d#4d0c6bc56da0e44eb164601a2af673ae7998257d" +source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=0ed31b9ce7c5644dde6dd8fd9b4a143458bb86c6#0ed31b9ce7c5644dde6dd8fd9b4a143458bb86c6" dependencies = [ "proc-macro2", "quote", @@ -4522,7 +4531,7 @@ dependencies = [ [[package]] name = "sqlquack-syntax-postgres" version = "0.2.0" -source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=4d0c6bc56da0e44eb164601a2af673ae7998257d#4d0c6bc56da0e44eb164601a2af673ae7998257d" +source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=0ed31b9ce7c5644dde6dd8fd9b4a143458bb86c6#0ed31b9ce7c5644dde6dd8fd9b4a143458bb86c6" dependencies = [ "sqlquack-syntax", ] diff --git a/Cargo.toml b/Cargo.toml index f03cafe024..cb12ecd72f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -176,9 +176,10 @@ sqlbuild-sqltext = { path = "crates/sqlbuild-sqltext" } sqlbuild-state = { path = "crates/sqlbuild-state" } # SQL lint Rules, quality Rules and the formatter. sqlclean = { git = "https://github.com/sqlstack-io/sqlclean.git", rev = "cf79656d247277a30bc81ef39b6dee420e721221" } -# The lossless SQL tokenizer behind query fingerprints. -sqlquack-syntax = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "4d0c6bc56da0e44eb164601a2af673ae7998257d" } -sqlquack-syntax-postgres = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "4d0c6bc56da0e44eb164601a2af673ae7998257d" } +# SQLQuack's lossless tokenizer behind query fingerprints and its parsers for warehouse types. +sqlquack-syntax = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "0ed31b9ce7c5644dde6dd8fd9b4a143458bb86c6" } +sqlquack-syntax-bigquery = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "0ed31b9ce7c5644dde6dd8fd9b4a143458bb86c6" } +sqlquack-syntax-postgres = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "0ed31b9ce7c5644dde6dd8fd9b4a143458bb86c6" } stacker = "0.1" syn = { version = "2", features = ["full", "visit", "extra-traits"] } subtle = "2" diff --git a/crates/sqlbuild-analysis/Cargo.toml b/crates/sqlbuild-analysis/Cargo.toml index 80a5ae2313..9a7d4b5a89 100644 --- a/crates/sqlbuild-analysis/Cargo.toml +++ b/crates/sqlbuild-analysis/Cargo.toml @@ -27,6 +27,7 @@ sqlbuild-scopes.workspace = true sqlbuild-sqltext.workspace = true sqlclean.workspace = true sqlquack-syntax.workspace = true +sqlquack-syntax-bigquery.workspace = true sqlquack-syntax-postgres.workspace = true [lints] diff --git a/crates/sqlbuild-analysis/src/lib.rs b/crates/sqlbuild-analysis/src/lib.rs index 2339caa1b3..8f59f24c60 100644 --- a/crates/sqlbuild-analysis/src/lib.rs +++ b/crates/sqlbuild-analysis/src/lib.rs @@ -50,3 +50,4 @@ pub mod sql_test_attachment; pub mod sql_tokens; pub mod target_paths; pub mod type_system; +pub mod warehouse_types; diff --git a/crates/sqlbuild-analysis/src/warehouse_types/constants.rs b/crates/sqlbuild-analysis/src/warehouse_types/constants.rs new file mode 100644 index 0000000000..3a91139238 --- /dev/null +++ b/crates/sqlbuild-analysis/src/warehouse_types/constants.rs @@ -0,0 +1,12 @@ +//! Constants of warehouse type parsing. + +/// The tree key of a cast. +pub(crate) const CAST_KEY: &str = "cast"; +/// The tree key of a cast's target type. +pub(crate) const CAST_TARGET_KEY: &str = "to"; +/// The dialect whose type names go through PostgreSQL's own type grammar. +pub(crate) const POSTGRES_DIALECT: &str = "postgres"; +/// The dialect whose type names are parsed as a Snowflake cast's target. +pub(crate) const SNOWFLAKE_DIALECT: &str = "snowflake"; +/// The dialect whose type names are parsed as a BigQuery cast's target. +pub(crate) const BIGQUERY_DIALECT: &str = "bigquery"; diff --git a/crates/sqlbuild-analysis/src/warehouse_types/main/mod.rs b/crates/sqlbuild-analysis/src/warehouse_types/main/mod.rs new file mode 100644 index 0000000000..fdbebafce0 --- /dev/null +++ b/crates/sqlbuild-analysis/src/warehouse_types/main/mod.rs @@ -0,0 +1 @@ +pub mod parse_warehouse_type; diff --git a/crates/sqlbuild-analysis/src/warehouse_types/main/parse_warehouse_type.rs b/crates/sqlbuild-analysis/src/warehouse_types/main/parse_warehouse_type.rs new file mode 100644 index 0000000000..dc6682be9a --- /dev/null +++ b/crates/sqlbuild-analysis/src/warehouse_types/main/parse_warehouse_type.rs @@ -0,0 +1,59 @@ +//! `parse_warehouse_type`: a warehouse type name as SQLQuack's parser for its dialect reads it. + +use serde_json::Value; +use sqlquack_syntax::tokens::models::Dialect; +use sqlquack_syntax::{Expression, ParseOptions, parse_strict}; + +use crate::warehouse_types::constants::{ + BIGQUERY_DIALECT, CAST_KEY, CAST_TARGET_KEY, POSTGRES_DIALECT, SNOWFLAKE_DIALECT, +}; + +/// The type tree of `spelling` in `dialect`, None without a parser, or why the parser rejects it. +pub fn parse_warehouse_type(spelling: &str, dialect: &str) -> Result, String> { + let cast: String = format!("SELECT CAST(NULL AS {spelling})"); + let statements: Vec = match dialect { + POSTGRES_DIALECT => { + let data_type = sqlquack_syntax_postgres::parse_type(spelling) + .map_err(|error| format!("{error:?}"))?; + return serde_json::to_value(data_type) + .map(Some) + .map_err(|error| error.to_string()); + } + SNOWFLAKE_DIALECT => parse_strict(&cast, Dialect::Snowflake, &ParseOptions::default()) + .map_err(|error| format!("{error:?}"))?, + BIGQUERY_DIALECT => { + sqlquack_syntax_bigquery::parse_strict(&cast) + .map_err(|error| format!("{error:?}"))? + .statements + } + _ => return Ok(None), + }; + let tree: Value = serde_json::to_value(&statements).map_err(|error| error.to_string())?; + match (statements.len(), cast_targets(&tree).as_slice()) { + (1, [target]) => Ok(Some((*target).clone())), + _ => Err(format!("`{spelling}` is not a single type name")), + } +} + +/// Every cast's target type in `tree`, in order. +fn cast_targets(tree: &Value) -> Vec<&Value> { + let mut targets: Vec<&Value> = Vec::new(); + let mut pending: Vec<&Value> = vec![tree]; + while let Some(node) = pending.pop() { + match node { + Value::Object(fields) => { + for (key, value) in fields.iter().rev() { + pending.push(value); + if key == CAST_KEY + && let Some(target) = value.get(CAST_TARGET_KEY) + { + targets.push(target); + } + } + } + Value::Array(items) => pending.extend(items.iter().rev()), + _ => {} + } + } + targets +} diff --git a/crates/sqlbuild-analysis/src/warehouse_types/mod.rs b/crates/sqlbuild-analysis/src/warehouse_types/mod.rs new file mode 100644 index 0000000000..c0ebb26c0f --- /dev/null +++ b/crates/sqlbuild-analysis/src/warehouse_types/mod.rs @@ -0,0 +1,4 @@ +//! Warehouse column type names as SQLQuack's parser for each warehouse dialect reads them. + +pub(crate) mod constants; +pub mod main; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/analysis/queries.rs b/crates/sqlbuild-python/src/bindings/_helpers/analysis/queries.rs index 98a6ce5ace..cf5e3d7ae4 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/analysis/queries.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/analysis/queries.rs @@ -14,11 +14,23 @@ fn query_fingerprint(py: Python<'_>, sql: &str, dialect: &str) -> PyResult PyResult> { + let tree: Option = + sqlbuild_analysis::warehouse_types::main::parse_warehouse_type::parse_warehouse_type( + spelling, dialect, + ) + .map_err(value_error)?; + Ok(tree.as_ref().map(serde_json::Value::to_string)) +} + pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> { crate::bindings::_helpers::analysis::binding_catalog::register(module)?; crate::bindings::_helpers::analysis::required_columns::register(module)?; crate::bindings::_helpers::references::explicit_references::register(module)?; crate::bindings::_helpers::analysis::binding_locations::register(module)?; module.add_function(wrap_pyfunction!(query_fingerprint, module)?)?; + module.add_function(wrap_pyfunction!(parse_warehouse_type, module)?)?; Ok(()) } diff --git a/python-boundary.toml b/python-boundary.toml index 5829d6322f..984c32c3f2 100644 --- a/python-boundary.toml +++ b/python-boundary.toml @@ -572,11 +572,6 @@ path = "compiler/auditing/main/get_audit_factory_definition.py" rule = 3 note = "Reads the declaration attached to a project audit factory." -[[stays]] -path = "compiler/sql_analysis/main/import_polyglot.py" -rule = 4 -note = "Imports the Polyglot Python package for the Python API." - [[stays]] path = "compiler/sql_analysis/main/import_polyglot_sql.py" rule = 4 diff --git a/src/sqlbuild/_native.pyi b/src/sqlbuild/_native.pyi index f75bac9647..f3dfb47056 100644 --- a/src/sqlbuild/_native.pyi +++ b/src/sqlbuild/_native.pyi @@ -3459,6 +3459,7 @@ class PreparedCompileArtifacts: def format_sql_json(request_json: str) -> str: ... def query_fingerprint(sql: str, dialect: str) -> str: ... +def parse_warehouse_type(spelling: str, dialect: str) -> str | None: ... def lint_sql_json(request_json: str) -> str: ... def prepare_lint_sql( request: LintPreparationRequest, diff --git a/src/sqlbuild/compiler/sql_analysis/main/import_polyglot.py b/src/sqlbuild/compiler/sql_analysis/main/import_polyglot.py deleted file mode 100644 index d38e3925d8..0000000000 --- a/src/sqlbuild/compiler/sql_analysis/main/import_polyglot.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Public required Polyglot import entry.""" - -from typing import Any - -from sqlbuild.compiler.sql_analysis._helpers.polyglot import ( - import_polyglot as _import_polyglot, -) - - -def import_polyglot() -> Any: - """Return SQLBuild's required Polyglot SQL module.""" - - return _import_polyglot() diff --git a/src/sqlbuild/executor/scenario/_helpers/capture/columns.py b/src/sqlbuild/executor/scenario/_helpers/capture/columns.py index 6614e7cf63..d389a37bf7 100644 --- a/src/sqlbuild/executor/scenario/_helpers/capture/columns.py +++ b/src/sqlbuild/executor/scenario/_helpers/capture/columns.py @@ -2,29 +2,39 @@ from __future__ import annotations +import json import logging +import re from dataclasses import dataclass from typing import Any +from sqlbuild._native import parse_warehouse_type from sqlbuild.adapter.contract.classes.base_adapter import BaseAdapter from sqlbuild.adapter.contract.models import ColumnInfo from sqlbuild.adapter.contract.types import TypeDialect -from sqlbuild.compiler.sql_analysis.main.import_polyglot import import_polyglot from sqlbuild.diagnostics.main.log_debug_event import log_debug_event from sqlbuild.errors.contracts.exceptions import ExecutorInputError from sqlbuild.executor.scenario.constants import ( BIGQUERY_BIGNUMERIC_TYPE, - BIGQUERY_BYTES_TYPE, BIGQUERY_TIMESTAMP_TYPE, BIGQUERY_VARCHAR_TYPES, BIGQUERY_WIDEN_TO_DOUBLE_TYPES, + DATABRICKS_ARRAY_TYPE, + DATABRICKS_INTERVAL_PREFIX, + DATABRICKS_MAP_ARGUMENT_COUNT, + DATABRICKS_MAP_TYPE, + DATABRICKS_NESTED_TYPES, + DATABRICKS_STRUCT_FIELD_PATTERN, + DATABRICKS_STRUCT_TYPE, DATABRICKS_TIMESTAMP_NTZ_TYPE, DATABRICKS_TIMESTAMP_TYPE, + DATABRICKS_VARIANT_TYPE, DATABRICKS_VOID_TYPE, DUCKDB_BLOB_TYPE, DUCKDB_HUGEINT_TYPE, DUCKDB_INTEGER_TYPE, DUCKDB_LIST_TYPE, + DUCKDB_SPATIAL_TYPES, DUCKDB_UHUGEINT_TYPE, FALLBACK_BINARY_TYPES, FALLBACK_BOOLEAN_TYPES, @@ -32,20 +42,22 @@ FALLBACK_DECIMAL_TYPES, FALLBACK_FLOAT_TYPES, FALLBACK_JSON_TYPES, - FALLBACK_POLYGLOT_BASE_TYPES, - FALLBACK_SIGNED_INTEGER_TYPES, + FALLBACK_SIGNED_INTEGER_LOCAL_TYPES, FALLBACK_STRUCTURED_TYPES, FALLBACK_TIME_TYPES, FALLBACK_TIMESTAMP_TOKEN, FALLBACK_TIMESTAMP_TYPES, FALLBACK_UNSIGNED_INTEGER_TYPES, FALLBACK_VARCHAR_TYPES, + GENERIC_LOCAL_BASE_TYPES, GENERIC_WIDEN_TO_DOUBLE_TYPES, LOCAL_TYPE_BIGINT, LOCAL_TYPE_BOOLEAN, LOCAL_TYPE_DATE, LOCAL_TYPE_DECIMAL, + LOCAL_TYPE_DIALECTS, LOCAL_TYPE_DOUBLE, + LOCAL_TYPE_FLOAT, LOCAL_TYPE_INT, LOCAL_TYPE_INT128, LOCAL_TYPE_JSON, @@ -55,16 +67,10 @@ LOCAL_TYPE_TIME, LOCAL_TYPE_TIMESTAMP, LOCAL_TYPE_TIMESTAMPTZ, + LOCAL_TYPE_TIMETZ, LOCAL_TYPE_UINT128, LOCAL_TYPE_VARCHAR, - MALFORMED_RENDERED_TYPE_TOKEN, - POLYGLOT_ARRAY_TYPE, - POLYGLOT_COLLECTION_TYPES, - POLYGLOT_CUSTOM_TYPE, - POLYGLOT_DECIMAL_TYPES, - POLYGLOT_OBJECT_TYPE, - POLYGLOT_SPATIAL_AND_MONEY_TYPES, - POLYGLOT_VARCHAR_TYPES, + LOCAL_TYPE_VARIANT, POSTGRES_BIGSERIAL_TYPE, POSTGRES_PARAMETERIZED_DECIMAL_TYPES, POSTGRES_PARAMETERIZED_TEXT_TYPES, @@ -79,6 +85,7 @@ SNOWFLAKE_TIMEZONE_TIMESTAMP_TYPES, SNOWFLAKE_VECTOR_TYPE, SNOWFLAKE_WIDEN_TO_DOUBLE_TYPES, + STRUCT_FIELD_NAME_PATTERN, TYPE_ARGUMENT_CLOSE_ANGLE, TYPE_ARGUMENT_CLOSE_PAREN, TYPE_ARGUMENT_CLOSE_TOKENS, @@ -86,7 +93,16 @@ TYPE_ARGUMENT_OPEN_TOKENS, TYPE_ARGUMENT_SEPARATOR, TYPE_PATTERN_WILDCARD, - UNTYPED_ARRAY_TYPE, + TYPE_TREE_COLLECTIONS, + TYPE_TREE_DECIMAL, + TYPE_TREE_FLOAT, + TYPE_TREE_KIND, + TYPE_TREE_LOCAL_TYPES, + TYPE_TREE_MAP, + TYPE_TREE_STRUCT, + TYPE_TREE_TEXT_KINDS, + TYPE_TREE_TIME, + TYPE_TREE_TIMESTAMP, ) from sqlbuild.executor.scenario.models import ScenarioSnapshotColumn @@ -194,12 +210,12 @@ def local_type_for_warehouse_type( if dialect_type is not None: return dialect_type - polyglot_local_type: str | None = _local_type_with_polyglot( + parsed_local_type: str | None = _local_type_with_sqlquack( warehouse_type=warehouse_type, sql_analysis_dialect=sql_analysis_dialect, ) - if polyglot_local_type is not None: - return polyglot_local_type + if parsed_local_type is not None: + return parsed_local_type return _fallback_local_type_for_warehouse_type(warehouse_type) @@ -277,10 +293,10 @@ def _dialect_pre_local_type(*, warehouse_type: str, sql_analysis_dialect: str | if dialect == TypeDialect.BIGQUERY: return _bigquery_pre_local_type(base=base, args=args) if dialect == TypeDialect.DATABRICKS: - return _databricks_pre_local_type(base=base, args=args) - if dialect == TypeDialect.DUCKDB: + return _databricks_local_type(warehouse_type) + if dialect in LOCAL_TYPE_DIALECTS: return _duckdb_pre_local_type(base=base, args=args) - if dialect is None and base not in FALLBACK_POLYGLOT_BASE_TYPES: + if dialect is None and base not in GENERIC_LOCAL_BASE_TYPES: return LOCAL_TYPE_VARCHAR return None @@ -317,22 +333,72 @@ def _bigquery_pre_local_type(*, base: str, args: tuple[str, ...]) -> str | None: return None -def _databricks_pre_local_type(*, base: str, args: tuple[str, ...]) -> str | None: +def _databricks_local_type(type_text: str) -> str | None: + """Map a Databricks type, nesting included; None leaves a scalar to the fallback.""" + + pattern: _TypePattern = _split_type_text(type_text.strip()) + base: str = pattern.base.upper() + if base in DATABRICKS_NESTED_TYPES: + return _databricks_nested_local_type(type_text=type_text.strip(), pattern=pattern) if base == DATABRICKS_TIMESTAMP_TYPE: return LOCAL_TYPE_TIMESTAMPTZ if base == DATABRICKS_TIMESTAMP_NTZ_TYPE: return LOCAL_TYPE_TIMESTAMP if base == DATABRICKS_VOID_TYPE: return LOCAL_TYPE_VARCHAR + if base.startswith(DATABRICKS_INTERVAL_PREFIX): + return " ".join(base.split()) + if base == DATABRICKS_VARIANT_TYPE: + return LOCAL_TYPE_VARIANT return None +def _databricks_nested_local_type(*, type_text: str, pattern: _TypePattern) -> str: + base: str = pattern.base.upper() + args: tuple[str, ...] = pattern.args + if base == DATABRICKS_ARRAY_TYPE and len(args) == 1: + return _databricks_element_local_type(element=args[0], type_text=type_text) + "[]" + if base == DATABRICKS_MAP_TYPE and len(args) == DATABRICKS_MAP_ARGUMENT_COUNT: + key: str = _databricks_element_local_type(element=args[0], type_text=type_text) + value: str = _databricks_element_local_type(element=args[1], type_text=type_text) + return f"MAP({key}, {value})" + if base == DATABRICKS_STRUCT_TYPE and args: + fields: list[str] = [] + for field in args: + match: re.Match[str] | None = DATABRICKS_STRUCT_FIELD_PATTERN.fullmatch(field) + if match is None: + raise _unmapped_databricks_type(element=field, type_text=type_text) + name: str = match.group("name").strip("`") + field_type: str = _databricks_element_local_type( + element=match.group("type"), type_text=type_text + ) + fields.append(f"{_struct_field_name(name)} {field_type}") + return f"STRUCT({', '.join(fields)})" + raise _unmapped_databricks_type(element=type_text, type_text=type_text) + + +def _databricks_element_local_type(*, element: str, type_text: str) -> str: + local_type: str | None = _databricks_local_type(element) or _known_local_type(element) + if local_type is None: + raise _unmapped_databricks_type(element=element, type_text=type_text) + return local_type + + +def _unmapped_databricks_type(*, element: str, type_text: str) -> ExecutorInputError: + return ExecutorInputError( + f"Databricks type '{type_text}' has no local DuckDB type: SQLBuild cannot map " + f"'{element.strip()}' inside it", + code=SCENARIO_LOCAL_TYPE_INVALID, + help="Add a scenario local type override for this warehouse type.", + ) + + def _duckdb_pre_local_type(*, base: str, args: tuple[str, ...]) -> str | None: if base == DUCKDB_HUGEINT_TYPE: return LOCAL_TYPE_INT128 if base == DUCKDB_UHUGEINT_TYPE: return LOCAL_TYPE_UINT128 - if base == DUCKDB_BLOB_TYPE: + if base == DUCKDB_BLOB_TYPE or base in DUCKDB_SPATIAL_TYPES: return LOCAL_TYPE_VARCHAR if base == DUCKDB_LIST_TYPE and args: inner_type: str = _fallback_local_type_for_warehouse_type(args[0]) @@ -342,68 +408,37 @@ def _duckdb_pre_local_type(*, base: str, args: tuple[str, ...]) -> str | None: return None -def _local_type_with_polyglot( +def _local_type_with_sqlquack( *, warehouse_type: str, sql_analysis_dialect: str | None ) -> str | None: - polyglot_module: Any = import_polyglot() + """Map a type through SQLQuack's parser for its dialect; DuckDB spellings are already local.""" + dialect: TypeDialect | None = _coerce_type_dialect(sql_analysis_dialect) + if dialect in LOCAL_TYPE_DIALECTS: + return warehouse_type.strip() try: - data_type: Any = polyglot_module.parse_data_type( - warehouse_type, - dialect=sql_analysis_dialect or "generic", - ) - except polyglot_module.PolyglotError as error: + tree_json: str | None = parse_warehouse_type(warehouse_type, sql_analysis_dialect or "") + except ValueError as error: log_debug_event( logger=_DEBUG_LOGGER, - message="scenario snapshot type polyglot conversion failed; falling back", + message="scenario snapshot type parse failed; falling back", warehouse_type=warehouse_type, sql_analysis_dialect=sql_analysis_dialect, sqlbuild_error=str(error), ) return None - - type_name: str = _polyglot_type_name(data_type) + if tree_json is None: + return None base_type: str = _parse_type_pattern(warehouse_type).base - if type_name in POLYGLOT_DECIMAL_TYPES: - rendered_bigdecimal_type: str = data_type.sql(dialect="duckdb").strip() - if MALFORMED_RENDERED_TYPE_TOKEN not in rendered_bigdecimal_type: - return rendered_bigdecimal_type - return _decimal_local_type_from_polyglot(data_type) or LOCAL_TYPE_VARCHAR - if type_name in POLYGLOT_VARCHAR_TYPES: - return LOCAL_TYPE_VARCHAR - if type_name == POLYGLOT_OBJECT_TYPE: - return LOCAL_TYPE_JSON - if type_name == POLYGLOT_ARRAY_TYPE and not _polyglot_element_type(data_type): - return LOCAL_TYPE_JSON - if type_name in POLYGLOT_COLLECTION_TYPES and _polyglot_element_type(data_type): - return data_type.sql(dialect="duckdb").strip() - if type_name in POLYGLOT_SPATIAL_AND_MONEY_TYPES: - return LOCAL_TYPE_VARCHAR - dialect: TypeDialect | None = _coerce_type_dialect(sql_analysis_dialect) if dialect == TypeDialect.POSTGRES and base_type in POSTGRES_VARCHAR_TYPES: return LOCAL_TYPE_VARCHAR if dialect == TypeDialect.POSTGRES and base_type in POSTGRES_SERIAL_TYPES: return _postgres_serial_local_type(base_type) - if ( - dialect == TypeDialect.BIGQUERY - and base_type == BIGQUERY_BYTES_TYPE - and data_type.expressions - ): - return LOCAL_TYPE_VARCHAR if dialect == TypeDialect.SNOWFLAKE and base_type == SNOWFLAKE_VECTOR_TYPE: return LOCAL_TYPE_JSON - - local_type: str = data_type.sql(dialect="duckdb").strip() - if ( - not local_type - or local_type == UNTYPED_ARRAY_TYPE - or MALFORMED_RENDERED_TYPE_TOKEN in local_type - ): + local_type: str | None = _local_type_from_tree(json.loads(tree_json)) + if local_type is None: return _fallback_local_type_for_warehouse_type(warehouse_type) - if local_type.startswith("BLOB"): - return "VARCHAR" - if local_type.startswith("TIMESTAMPTZ("): - return "TIMESTAMPTZ" if local_type == LOCAL_TYPE_REAL and _should_widen_real_to_double( warehouse_type=warehouse_type, sql_analysis_dialect=sql_analysis_dialect, @@ -412,15 +447,65 @@ def _local_type_with_polyglot( return local_type +def _local_type_from_tree(tree: dict[str, Any]) -> str | None: + """The DuckDB type of a SQLQuack type tree, or None when it has no fixed local type.""" + + kind: str = str(tree.get(TYPE_TREE_KIND, "")) + if kind in TYPE_TREE_LOCAL_TYPES: + return TYPE_TREE_LOCAL_TYPES[kind] + if kind in TYPE_TREE_TEXT_KINDS: + length: Any = tree.get("length") + return LOCAL_TYPE_TEXT if length is None else f"{LOCAL_TYPE_TEXT}({length})" + if kind == TYPE_TREE_FLOAT: + return LOCAL_TYPE_REAL if tree.get("real_spelling") else LOCAL_TYPE_FLOAT + if kind == TYPE_TREE_DECIMAL: + arguments: list[str] = [ + str(value) for value in (tree.get("precision"), tree.get("scale")) if value is not None + ] + return LOCAL_TYPE_DECIMAL + (f"({', '.join(arguments)})" if arguments else "") + if kind == TYPE_TREE_TIME: + return LOCAL_TYPE_TIMETZ if tree.get("timezone") else LOCAL_TYPE_TIME + if kind == TYPE_TREE_TIMESTAMP: + return LOCAL_TYPE_TIMESTAMPTZ if tree.get("timezone") else LOCAL_TYPE_TIMESTAMP + if kind in TYPE_TREE_COLLECTIONS: + element: str | None = _local_type_from_tree(tree.get("element_type") or {}) + return None if element is None else f"{element}[]" + if kind == TYPE_TREE_STRUCT: + fields: list[str] = [] + for field in tree.get("fields") or []: + field_type: str | None = _local_type_from_tree(field.get("data_type") or {}) + if field_type is None: + return None + fields.append(f"{_struct_field_name(str(field.get('name', '')))} {field_type}") + return f"STRUCT({', '.join(fields)})" if fields else None + if kind == TYPE_TREE_MAP: + key: str | None = _local_type_from_tree(tree.get("key_type") or {}) + value: str | None = _local_type_from_tree(tree.get("value_type") or {}) + return None if key is None or value is None else f"MAP({key}, {value})" + return None + + +def _struct_field_name(name: str) -> str: + if STRUCT_FIELD_NAME_PATTERN.fullmatch(name): + return name + return '"' + name.replace('"', '""') + '"' + + def _fallback_local_type_for_warehouse_type(warehouse_type: str) -> str: """Return a conservative local type when SQL analysis cannot classify a type.""" + return _known_local_type(warehouse_type) or LOCAL_TYPE_VARCHAR + + +def _known_local_type(warehouse_type: str) -> str | None: + """The local type of a type name SQLBuild knows by its base name alone, else None.""" + normalized_type: str = warehouse_type.strip().upper() - base_type: str = normalized_type.split("(", 1)[0].strip() + base_type: str = _parse_type_pattern(normalized_type).base if base_type in FALLBACK_BOOLEAN_TYPES: return LOCAL_TYPE_BOOLEAN - if base_type in FALLBACK_SIGNED_INTEGER_TYPES: - return LOCAL_TYPE_BIGINT + if base_type in FALLBACK_SIGNED_INTEGER_LOCAL_TYPES: + return FALLBACK_SIGNED_INTEGER_LOCAL_TYPES[base_type] if base_type in FALLBACK_UNSIGNED_INTEGER_TYPES: return LOCAL_TYPE_BIGINT if base_type in FALLBACK_FLOAT_TYPES: @@ -441,7 +526,7 @@ def _fallback_local_type_for_warehouse_type(warehouse_type: str) -> str: return LOCAL_TYPE_VARCHAR if base_type in FALLBACK_STRUCTURED_TYPES: return LOCAL_TYPE_JSON - return LOCAL_TYPE_VARCHAR + return None def _postgres_serial_local_type(base_type: str) -> str: @@ -453,7 +538,10 @@ def _postgres_serial_local_type(base_type: str) -> str: def _parse_type_pattern(type_text: str) -> _TypePattern: - normalized_type: str = type_text.strip().upper() + return _split_type_text(type_text.strip().upper()) + + +def _split_type_text(normalized_type: str) -> _TypePattern: if not normalized_type: return _TypePattern(base="") open_index: int = _first_type_arg_open_index(normalized_type) @@ -538,37 +626,6 @@ def _decimal_local_type(normalized_type: str) -> str: return f"DECIMAL({precision_scale})" -def _decimal_local_type_from_polyglot(data_type: Any) -> str | None: - precision_scale: str | None = _polyglot_precision_scale(data_type) - if precision_scale is None: - return LOCAL_TYPE_DECIMAL - return f"DECIMAL({precision_scale})" - - -def _polyglot_precision_scale(data_type: Any) -> str | None: - args: dict[str, Any] = dict(getattr(data_type, "args", {}) or {}) - values: list[str] = [ - str(value) for value in (args.get("precision"), args.get("scale")) if value is not None - ] - if not values: - return None - return ", ".join(values) - - -def _polyglot_type_name(data_type: Any) -> str: - args: dict[str, Any] = dict(getattr(data_type, "args", {}) or {}) - data_type_name: str = str(args.get("data_type", "")).upper() - if data_type_name == POLYGLOT_CUSTOM_TYPE: - return str(args.get("name", "")).upper().replace(" ", "") - return data_type_name - - -def _polyglot_element_type(data_type: Any) -> dict[str, Any] | None: - args: dict[str, Any] = dict(getattr(data_type, "args", {}) or {}) - element_type: Any = args.get("element_type") - return element_type if isinstance(element_type, dict) else None - - def _should_widen_real_to_double(*, warehouse_type: str, sql_analysis_dialect: str | None) -> bool: normalized_type: str = warehouse_type.strip().upper() base_type: str = normalized_type.split("(", 1)[0].strip() diff --git a/src/sqlbuild/executor/scenario/constants.py b/src/sqlbuild/executor/scenario/constants.py index 1f87454379..53ac952975 100644 --- a/src/sqlbuild/executor/scenario/constants.py +++ b/src/sqlbuild/executor/scenario/constants.py @@ -1,5 +1,9 @@ """Scenario execution and local replay constants.""" +import re + +from sqlbuild.adapter.contract.types import TypeDialect + SCENARIO_EXEC_FIXTURE_FAILED: str = "X501" SCENARIO_EXEC_SEED_FAILED: str = "X502" SCENARIO_EXEC_MODEL_FAILED: str = "X503" @@ -35,6 +39,9 @@ LOCAL_TYPE_REAL: str = "REAL" LOCAL_TYPE_SMALLINT: str = "SMALLINT" LOCAL_TYPE_TEXT: str = "TEXT" +LOCAL_TYPE_FLOAT: str = "FLOAT" +LOCAL_TYPE_TIMETZ: str = "TIMETZ" +LOCAL_TYPE_VARIANT: str = "VARIANT" LOCAL_TYPE_TIME: str = "TIME" LOCAL_TYPE_TIMESTAMP: str = "TIMESTAMP" LOCAL_TYPE_TIMESTAMPTZ: str = "TIMESTAMPTZ" @@ -82,19 +89,29 @@ SNOWFLAKE_VECTOR_TYPE: str = "VECTOR" BIGQUERY_BIGNUMERIC_TYPE: str = "BIGNUMERIC" BIGQUERY_VARCHAR_TYPES: frozenset[str] = frozenset({"RANGE", "BYTES"}) -BIGQUERY_BYTES_TYPE: str = "BYTES" BIGQUERY_TIMESTAMP_TYPE: str = "TIMESTAMP" BIGQUERY_WIDEN_TO_DOUBLE_TYPES: frozenset[str] = frozenset({"FLOAT64"}) DATABRICKS_TIMESTAMP_TYPE: str = "TIMESTAMP" DATABRICKS_TIMESTAMP_NTZ_TYPE: str = "TIMESTAMP_NTZ" DATABRICKS_VOID_TYPE: str = "VOID" +DATABRICKS_INTERVAL_PREFIX: str = "INTERVAL" +DATABRICKS_VARIANT_TYPE: str = "VARIANT" +DATABRICKS_ARRAY_TYPE: str = "ARRAY" +DATABRICKS_MAP_TYPE: str = "MAP" +DATABRICKS_STRUCT_TYPE: str = "STRUCT" +DATABRICKS_NESTED_TYPES: frozenset[str] = frozenset({"ARRAY", "MAP", "STRUCT"}) +DATABRICKS_MAP_ARGUMENT_COUNT: int = 2 +DATABRICKS_STRUCT_FIELD_PATTERN: re.Pattern[str] = re.compile( + r"\s*(?P`[^`]+`|[A-Za-z_][A-Za-z0-9_]*)\s*(?::\s*|\s+)(?P\S.*?)\s*" +) DUCKDB_HUGEINT_TYPE: str = "HUGEINT" DUCKDB_UHUGEINT_TYPE: str = "UHUGEINT" DUCKDB_BLOB_TYPE: str = "BLOB" +DUCKDB_SPATIAL_TYPES: frozenset[str] = frozenset({"GEOMETRY", "GEOGRAPHY"}) DUCKDB_LIST_TYPE: str = "LIST" DUCKDB_INTEGER_TYPE: str = "INTEGER" -FALLBACK_POLYGLOT_BASE_TYPES: frozenset[str] = frozenset( +GENERIC_LOCAL_BASE_TYPES: frozenset[str] = frozenset( { "BOOL", "BOOLEAN", @@ -118,18 +135,54 @@ "JSON", } ) -POLYGLOT_DECIMAL_TYPES: frozenset[str] = frozenset({"BIGDECIMAL", "DECIMAL"}) -POLYGLOT_VARCHAR_TYPES: frozenset[str] = frozenset({"NULL", "RANGE", "XML"}) -POLYGLOT_OBJECT_TYPE: str = "OBJECT" -POLYGLOT_ARRAY_TYPE: str = "ARRAY" -POLYGLOT_COLLECTION_TYPES: frozenset[str] = frozenset({"ARRAY", "LIST"}) -POLYGLOT_SPATIAL_AND_MONEY_TYPES: frozenset[str] = frozenset({"GEOGRAPHY", "GEOMETRY", "MONEY"}) -POLYGLOT_CUSTOM_TYPE: str = "CUSTOM" +TYPE_TREE_KIND: str = "data_type" +TYPE_TREE_LOCAL_TYPES: dict[str, str] = { + "boolean": "BOOLEAN", + "tiny_int": "TINYINT", + "small_int": "SMALLINT", + "int": "INT", + "big_int": "BIGINT", + "int128": "HUGEINT", + "uint8": "UTINYINT", + "uint16": "USMALLINT", + "uint32": "UINTEGER", + "uint64": "UBIGINT", + "uint128": "UHUGEINT", + "double": "DOUBLE", + "date": "DATE", + "interval": "INTERVAL", + "json": "JSON", + "json_b": "JSON", + "uuid": "UUID", + "binary": "VARCHAR", + "var_binary": "VARCHAR", + "blob": "VARCHAR", + "geography": "VARCHAR", + "geometry": "VARCHAR", + "object": "JSON", +} +TYPE_TREE_TEXT_KINDS: frozenset[str] = frozenset({"char", "var_char", "string", "text"}) +TYPE_TREE_FLOAT: str = "float" +TYPE_TREE_DECIMAL: str = "decimal" +TYPE_TREE_TIME: str = "time" +TYPE_TREE_TIMESTAMP: str = "timestamp" +TYPE_TREE_COLLECTIONS: frozenset[str] = frozenset({"array", "list"}) +TYPE_TREE_STRUCT: str = "struct" +TYPE_TREE_MAP: str = "map" +STRUCT_FIELD_NAME_PATTERN: re.Pattern[str] = re.compile(r"[A-Za-z_][A-Za-z0-9_]*") +LOCAL_TYPE_DIALECTS: frozenset[TypeDialect] = frozenset( + {TypeDialect.DUCKDB, TypeDialect.MOTHERDUCK} +) FALLBACK_BOOLEAN_TYPES: frozenset[str] = frozenset({"BOOL", "BOOLEAN"}) -FALLBACK_SIGNED_INTEGER_TYPES: frozenset[str] = frozenset( - {"TINYINT", "SMALLINT", "INT", "INTEGER", "BIGINT", "HUGEINT"} -) +FALLBACK_SIGNED_INTEGER_LOCAL_TYPES: dict[str, str] = { + "TINYINT": "TINYINT", + "SMALLINT": "SMALLINT", + "INT": "INT", + "INTEGER": "INT", + "BIGINT": "BIGINT", + "HUGEINT": "HUGEINT", +} FALLBACK_UNSIGNED_INTEGER_TYPES: frozenset[str] = frozenset( {"UTINYINT", "USMALLINT", "UINTEGER", "UBIGINT", "UHUGEINT"} ) @@ -147,7 +200,7 @@ FALLBACK_JSON_TYPES: frozenset[str] = frozenset({"JSON", "JSONB"}) FALLBACK_BINARY_TYPES: frozenset[str] = frozenset({"BLOB", "BYTEA", "BINARY", "VARBINARY"}) FALLBACK_STRUCTURED_TYPES: frozenset[str] = frozenset( - {"ARRAY", "OBJECT", "VARIANT", "JSON", "JSONB"} + {"ARRAY", "OBJECT", "VARIANT", "JSON", "JSONB", "MAP", "STRUCT"} ) SNOWFLAKE_WIDEN_TO_DOUBLE_TYPES: frozenset[str] = SNOWFLAKE_DOUBLE_TYPES GENERIC_WIDEN_TO_DOUBLE_TYPES: frozenset[str] = frozenset({"DOUBLE", "DOUBLE PRECISION", "FLOAT8"}) @@ -159,8 +212,6 @@ TYPE_ARGUMENT_CLOSE_TOKENS: frozenset[str] = frozenset({")", ">"}) TYPE_ARGUMENT_SEPARATOR: str = "," TYPE_PATTERN_WILDCARD: str = "*" -MALFORMED_RENDERED_TYPE_TOKEN: str = ")(" -UNTYPED_ARRAY_TYPE: str = "[]" JSONL_BLANK_LINE: str = "" LOCAL_SCENARIO_DIALECT: str = "duckdb" diff --git a/tests/integration/src/sqlbuild/executor/scenario/test_capture_types.py b/tests/integration/src/sqlbuild/executor/scenario/test_capture_types.py index fd7dd93469..f60ab5ff05 100644 --- a/tests/integration/src/sqlbuild/executor/scenario/test_capture_types.py +++ b/tests/integration/src/sqlbuild/executor/scenario/test_capture_types.py @@ -32,7 +32,7 @@ ScenarioSnapshotCaptureTypesIntegrationTestCase( description="typed duckdb capture jsonl reloads with manifest schema", expected_local_types={ - "decimal_scale_col": "DECIMAL(12, 2)", + "decimal_scale_col": "DECIMAL(12,2)", "date_col": "DATE", "timestamp_col": "TIMESTAMP", "json_col": "JSON", diff --git a/tests/unit/src/sqlbuild/executor/scenario/_helpers/_test_types.py b/tests/unit/src/sqlbuild/executor/scenario/_helpers/_test_types.py index bd4f84044e..fb5658e8cb 100644 --- a/tests/unit/src/sqlbuild/executor/scenario/_helpers/_test_types.py +++ b/tests/unit/src/sqlbuild/executor/scenario/_helpers/_test_types.py @@ -145,6 +145,14 @@ class ScenarioSnapshotLocalTypeMappingTestCase: local_type_overrides: dict[str, str] | None = None +@dataclass(frozen=True) +class ScenarioSnapshotLocalTypeRefusalTestCase: + description: str + sql_analysis_dialect: str | None + warehouse_type: str + expected_message: str + + @dataclass(frozen=True) class ExecuteScenarioSnapshotCaptureStepsTestCase: description: str diff --git a/tests/unit/src/sqlbuild/executor/scenario/_helpers/test_capture_columns.py b/tests/unit/src/sqlbuild/executor/scenario/_helpers/test_capture_columns.py index 7f363d0be5..40da596225 100644 --- a/tests/unit/src/sqlbuild/executor/scenario/_helpers/test_capture_columns.py +++ b/tests/unit/src/sqlbuild/executor/scenario/_helpers/test_capture_columns.py @@ -3,9 +3,11 @@ import duckdb import pytest +from sqlbuild.errors.contracts.exceptions import ExecutorInputError from sqlbuild.executor.scenario._helpers.capture.columns import local_type_for_warehouse_type from tests.unit.src.sqlbuild.executor.scenario._helpers._test_types import ( ScenarioSnapshotLocalTypeMappingTestCase, + ScenarioSnapshotLocalTypeRefusalTestCase, ) @@ -157,10 +159,10 @@ expected_local_type="JSON", ), ScenarioSnapshotLocalTypeMappingTestCase( - description="snowflake variant remains variant", + description="snowflake variant is held as json as sqlquack converts it", sql_analysis_dialect="snowflake", warehouse_type="VARIANT", - expected_local_type="VARIANT", + expected_local_type="JSON", ), ScenarioSnapshotLocalTypeMappingTestCase( description="snowflake geography falls back to varchar", @@ -187,10 +189,10 @@ expected_local_type="BIGINT", ), ScenarioSnapshotLocalTypeMappingTestCase( - description="bigquery integer maps to int", + description="bigquery integer is int64 and maps to bigint", sql_analysis_dialect="bigquery", warehouse_type="INTEGER", - expected_local_type="INT", + expected_local_type="BIGINT", ), ScenarioSnapshotLocalTypeMappingTestCase( description="bigquery boolean maps to boolean", @@ -502,7 +504,7 @@ description="databricks map maps to duckdb map type", sql_analysis_dialect="databricks", warehouse_type="MAP", - expected_local_type="MAP(TEXT, INT)", + expected_local_type="MAP(VARCHAR, INT)", ), ScenarioSnapshotLocalTypeMappingTestCase( description="databricks tinyint maps to tinyint", @@ -514,19 +516,19 @@ description="databricks decimal precision and scale are preserved", sql_analysis_dialect="databricks", warehouse_type="DECIMAL(10,2)", - expected_local_type="DECIMAL(10, 2)", + expected_local_type="DECIMAL(10,2)", ), ScenarioSnapshotLocalTypeMappingTestCase( - description="databricks string maps to text", + description="databricks string maps to varchar", sql_analysis_dialect="databricks", warehouse_type="STRING", - expected_local_type="TEXT", + expected_local_type="VARCHAR", ), ScenarioSnapshotLocalTypeMappingTestCase( - description="databricks varchar length maps to text length", + description="databricks varchar length maps to varchar", sql_analysis_dialect="databricks", warehouse_type="VARCHAR(20)", - expected_local_type="TEXT(20)", + expected_local_type="VARCHAR", ), ScenarioSnapshotLocalTypeMappingTestCase( description="databricks timestamp maps to timestamptz", @@ -568,7 +570,13 @@ description="databricks nested map of arrays maps to duckdb map", sql_analysis_dialect="databricks", warehouse_type="MAP>", - expected_local_type="MAP(TEXT, INT[])", + expected_local_type="MAP(VARCHAR, INT[])", + ), + ScenarioSnapshotLocalTypeMappingTestCase( + description="databricks struct nests lists, maps and quoted field names", + sql_analysis_dialect="databricks", + warehouse_type="STRUCT>, `b c`: TINYINT>", + expected_local_type='STRUCT(a MAP(VARCHAR, BIGINT)[], "b c" TINYINT)', ), ScenarioSnapshotLocalTypeMappingTestCase( description="databricks interval year is preserved", @@ -619,16 +627,16 @@ expected_local_type="VARCHAR", ), ScenarioSnapshotLocalTypeMappingTestCase( - description="duckdb struct is preserved with normalized inner type", + description="duckdb struct passes through unchanged", sql_analysis_dialect="duckdb", warehouse_type="STRUCT(a INTEGER)", - expected_local_type="STRUCT(a INT)", + expected_local_type="STRUCT(a INTEGER)", ), ScenarioSnapshotLocalTypeMappingTestCase( - description="duckdb union is preserved with normalized inner types", + description="duckdb union passes through unchanged", sql_analysis_dialect="duckdb", warehouse_type="UNION(num INTEGER, str VARCHAR)", - expected_local_type="UNION(num INT, str TEXT)", + expected_local_type="UNION(num INTEGER, str VARCHAR)", ), ScenarioSnapshotLocalTypeMappingTestCase( description="duckdb list angle syntax maps to bracket list syntax", @@ -643,10 +651,10 @@ expected_local_type="ENUM('a', 'b')", ), ScenarioSnapshotLocalTypeMappingTestCase( - description="duckdb time with timezone maps to timetz", + description="duckdb time with time zone passes through unchanged", sql_analysis_dialect="duckdb", warehouse_type="TIME WITH TIME ZONE", - expected_local_type="TIMETZ", + expected_local_type="TIME WITH TIME ZONE", ), ScenarioSnapshotLocalTypeMappingTestCase( description="unknown type falls back to varchar", @@ -855,10 +863,10 @@ def test_given_warehouse_type_when_mapping_to_local_type_then_returns_expected_d expected_local_type="JSON", ), ScenarioSnapshotLocalTypeMappingTestCase( - description="snowflake variant remains variant", + description="snowflake variant is held as json as sqlquack converts it", sql_analysis_dialect="snowflake", warehouse_type="VARIANT", - expected_local_type="VARIANT", + expected_local_type="JSON", ), ScenarioSnapshotLocalTypeMappingTestCase( description="snowflake geography falls back to varchar", @@ -885,10 +893,10 @@ def test_given_warehouse_type_when_mapping_to_local_type_then_returns_expected_d expected_local_type="BIGINT", ), ScenarioSnapshotLocalTypeMappingTestCase( - description="bigquery integer maps to int", + description="bigquery integer is int64 and maps to bigint", sql_analysis_dialect="bigquery", warehouse_type="INTEGER", - expected_local_type="INT", + expected_local_type="BIGINT", ), ScenarioSnapshotLocalTypeMappingTestCase( description="bigquery boolean maps to boolean", @@ -1200,7 +1208,7 @@ def test_given_warehouse_type_when_mapping_to_local_type_then_returns_expected_d description="databricks map maps to duckdb map type", sql_analysis_dialect="databricks", warehouse_type="MAP", - expected_local_type="MAP(TEXT, INT)", + expected_local_type="MAP(VARCHAR, INT)", ), ScenarioSnapshotLocalTypeMappingTestCase( description="databricks tinyint maps to tinyint", @@ -1212,19 +1220,19 @@ def test_given_warehouse_type_when_mapping_to_local_type_then_returns_expected_d description="databricks decimal precision and scale are preserved", sql_analysis_dialect="databricks", warehouse_type="DECIMAL(10,2)", - expected_local_type="DECIMAL(10, 2)", + expected_local_type="DECIMAL(10,2)", ), ScenarioSnapshotLocalTypeMappingTestCase( - description="databricks string maps to text", + description="databricks string maps to varchar", sql_analysis_dialect="databricks", warehouse_type="STRING", - expected_local_type="TEXT", + expected_local_type="VARCHAR", ), ScenarioSnapshotLocalTypeMappingTestCase( - description="databricks varchar length maps to text length", + description="databricks varchar length maps to varchar", sql_analysis_dialect="databricks", warehouse_type="VARCHAR(20)", - expected_local_type="TEXT(20)", + expected_local_type="VARCHAR", ), ScenarioSnapshotLocalTypeMappingTestCase( description="databricks timestamp maps to timestamptz", @@ -1266,7 +1274,13 @@ def test_given_warehouse_type_when_mapping_to_local_type_then_returns_expected_d description="databricks nested map of arrays maps to duckdb map", sql_analysis_dialect="databricks", warehouse_type="MAP>", - expected_local_type="MAP(TEXT, INT[])", + expected_local_type="MAP(VARCHAR, INT[])", + ), + ScenarioSnapshotLocalTypeMappingTestCase( + description="databricks struct nests lists, maps and quoted field names", + sql_analysis_dialect="databricks", + warehouse_type="STRUCT>, `b c`: TINYINT>", + expected_local_type='STRUCT(a MAP(VARCHAR, BIGINT)[], "b c" TINYINT)', ), ScenarioSnapshotLocalTypeMappingTestCase( description="databricks interval year is preserved", @@ -1317,16 +1331,16 @@ def test_given_warehouse_type_when_mapping_to_local_type_then_returns_expected_d expected_local_type="VARCHAR", ), ScenarioSnapshotLocalTypeMappingTestCase( - description="duckdb struct is preserved with normalized inner type", + description="duckdb struct passes through unchanged", sql_analysis_dialect="duckdb", warehouse_type="STRUCT(a INTEGER)", - expected_local_type="STRUCT(a INT)", + expected_local_type="STRUCT(a INTEGER)", ), ScenarioSnapshotLocalTypeMappingTestCase( - description="duckdb union is preserved with normalized inner types", + description="duckdb union passes through unchanged", sql_analysis_dialect="duckdb", warehouse_type="UNION(num INTEGER, str VARCHAR)", - expected_local_type="UNION(num INT, str TEXT)", + expected_local_type="UNION(num INTEGER, str VARCHAR)", ), ScenarioSnapshotLocalTypeMappingTestCase( description="duckdb list angle syntax maps to bracket list syntax", @@ -1341,10 +1355,10 @@ def test_given_warehouse_type_when_mapping_to_local_type_then_returns_expected_d expected_local_type="ENUM('a', 'b')", ), ScenarioSnapshotLocalTypeMappingTestCase( - description="duckdb time with timezone maps to timetz", + description="duckdb time with time zone passes through unchanged", sql_analysis_dialect="duckdb", warehouse_type="TIME WITH TIME ZONE", - expected_local_type="TIMETZ", + expected_local_type="TIME WITH TIME ZONE", ), ScenarioSnapshotLocalTypeMappingTestCase( description="unknown type falls back to varchar", @@ -1405,3 +1419,33 @@ def test_given_mapped_local_type_when_creating_duckdb_table_then_type_is_accepte assert local_type == test_case.expected_local_type connection.execute(f'CREATE TABLE local_type_probe ("value" {local_type})') + + +@pytest.mark.parametrize( + "test_case", + [ + ScenarioSnapshotLocalTypeRefusalTestCase( + description="databricks array of an unmapped element type is refused", + sql_analysis_dialect="databricks", + warehouse_type="ARRAY", + expected_message="SQLBuild cannot map 'GEOGRAPHY' inside it", + ), + ScenarioSnapshotLocalTypeRefusalTestCase( + description="databricks map missing its value type is refused", + sql_analysis_dialect="databricks", + warehouse_type="MAP", + expected_message="SQLBuild cannot map 'MAP' inside it", + ), + ], + ids=lambda case: case.description, +) +def test_given_unmapped_nested_type_when_mapping_to_local_type_then_it_is_refused( + test_case: ScenarioSnapshotLocalTypeRefusalTestCase, +) -> None: + with pytest.raises(ExecutorInputError) as raised: + local_type_for_warehouse_type( + warehouse_type=test_case.warehouse_type, + sql_analysis_dialect=test_case.sql_analysis_dialect, + ) + + assert test_case.expected_message in raised.value.message diff --git a/tests/unit/src/sqlbuild/executor/scenario/main/test_capture.py b/tests/unit/src/sqlbuild/executor/scenario/main/test_capture.py index da2be93081..2e450b4c68 100644 --- a/tests/unit/src/sqlbuild/executor/scenario/main/test_capture.py +++ b/tests/unit/src/sqlbuild/executor/scenario/main/test_capture.py @@ -137,7 +137,7 @@ def render_qualified_name( ScenarioSnapshotColumn( name="customer_id", warehouse_type="DECIMAL(38,0)", - local_type="DECIMAL(38, 0)", + local_type="DECIMAL(38,0)", ), ), ), @@ -151,7 +151,7 @@ def render_qualified_name( ScenarioSnapshotColumn( name="country_code", warehouse_type="VARCHAR", - local_type="TEXT", + local_type="VARCHAR", ), ), ), @@ -170,7 +170,7 @@ def render_qualified_name( ScenarioSnapshotColumn( name="amount", warehouse_type="DECIMAL(10,2)", - local_type="DECIMAL(10, 2)", + local_type="DECIMAL(10,2)", ), ), ), From 7655ef379af8975b24b63fc4feea82adcb555358 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 04:47:19 +0000 Subject: [PATCH 022/106] fix(scenario): map SQL Server date, time, uuid and money types locally SQL Server has no SQLQuack type parser, so its information_schema names reached the generic fallback, which held datetime2, smalldatetime, datetimeoffset and uniqueidentifier as VARCHAR. A SQL Server pre-mapper now maps them to TIMESTAMP, TIMESTAMPTZ and UUID, bit to BOOLEAN, money and smallmoney to exact decimals, and SQL Server's 8-byte float to DOUBLE. The fallback keeps 4-byte floats (FLOAT, FLOAT4, REAL) as FLOAT instead of widening them to DOUBLE. A panic in the warehouse type parser now surfaces as the ValueError capture already falls back on. --- .../src/bindings/_helpers/analysis/queries.rs | 11 +- .../scenario/_helpers/capture/columns.py | 6 + src/sqlbuild/executor/scenario/constants.py | 16 ++- .../scenario/_helpers/test_capture_columns.py | 120 ++++++++++++++++++ 4 files changed, 147 insertions(+), 6 deletions(-) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/analysis/queries.rs b/crates/sqlbuild-python/src/bindings/_helpers/analysis/queries.rs index cf5e3d7ae4..0bc7eeaa94 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/analysis/queries.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/analysis/queries.rs @@ -15,14 +15,17 @@ fn query_fingerprint(py: Python<'_>, sql: &str, dialect: &str) -> PyResult PyResult> { - let tree: Option = +fn parse_warehouse_type(py: Python<'_>, spelling: &str, dialect: &str) -> PyResult> { + py.compiler_detach(|| { sqlbuild_analysis::warehouse_types::main::parse_warehouse_type::parse_warehouse_type( spelling, dialect, ) - .map_err(value_error)?; - Ok(tree.as_ref().map(serde_json::Value::to_string)) + .map(|tree| tree.as_ref().map(serde_json::Value::to_string)) + }) + .map_err(value_error) } pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> { diff --git a/src/sqlbuild/executor/scenario/_helpers/capture/columns.py b/src/sqlbuild/executor/scenario/_helpers/capture/columns.py index d389a37bf7..add2b8fe3f 100644 --- a/src/sqlbuild/executor/scenario/_helpers/capture/columns.py +++ b/src/sqlbuild/executor/scenario/_helpers/capture/columns.py @@ -40,6 +40,7 @@ FALLBACK_BOOLEAN_TYPES, FALLBACK_DATE_TYPE, FALLBACK_DECIMAL_TYPES, + FALLBACK_DOUBLE_TYPES, FALLBACK_FLOAT_TYPES, FALLBACK_JSON_TYPES, FALLBACK_SIGNED_INTEGER_LOCAL_TYPES, @@ -86,6 +87,7 @@ SNOWFLAKE_VECTOR_TYPE, SNOWFLAKE_WIDEN_TO_DOUBLE_TYPES, STRUCT_FIELD_NAME_PATTERN, + TSQL_LOCAL_TYPES, TYPE_ARGUMENT_CLOSE_ANGLE, TYPE_ARGUMENT_CLOSE_PAREN, TYPE_ARGUMENT_CLOSE_TOKENS, @@ -296,6 +298,8 @@ def _dialect_pre_local_type(*, warehouse_type: str, sql_analysis_dialect: str | return _databricks_local_type(warehouse_type) if dialect in LOCAL_TYPE_DIALECTS: return _duckdb_pre_local_type(base=base, args=args) + if dialect == TypeDialect.TSQL: + return TSQL_LOCAL_TYPES.get(base) if dialect is None and base not in GENERIC_LOCAL_BASE_TYPES: return LOCAL_TYPE_VARCHAR return None @@ -509,6 +513,8 @@ def _known_local_type(warehouse_type: str) -> str | None: if base_type in FALLBACK_UNSIGNED_INTEGER_TYPES: return LOCAL_TYPE_BIGINT if base_type in FALLBACK_FLOAT_TYPES: + return LOCAL_TYPE_FLOAT + if base_type in FALLBACK_DOUBLE_TYPES: return LOCAL_TYPE_DOUBLE if base_type in FALLBACK_DECIMAL_TYPES: return _decimal_local_type(normalized_type) diff --git a/src/sqlbuild/executor/scenario/constants.py b/src/sqlbuild/executor/scenario/constants.py index 53ac952975..416287896c 100644 --- a/src/sqlbuild/executor/scenario/constants.py +++ b/src/sqlbuild/executor/scenario/constants.py @@ -186,9 +186,21 @@ FALLBACK_UNSIGNED_INTEGER_TYPES: frozenset[str] = frozenset( {"UTINYINT", "USMALLINT", "UINTEGER", "UBIGINT", "UHUGEINT"} ) -FALLBACK_FLOAT_TYPES: frozenset[str] = frozenset( - {"FLOAT", "FLOAT4", "REAL", "DOUBLE", "DOUBLE PRECISION", "FLOAT8"} +FALLBACK_FLOAT_TYPES: frozenset[str] = frozenset({"FLOAT", "FLOAT4", "REAL"}) +FALLBACK_DOUBLE_TYPES: frozenset[str] = frozenset( + {"DOUBLE", "DOUBLE PRECISION", "FLOAT8", "FLOAT64"} ) +TSQL_LOCAL_TYPES: dict[str, str] = { + "BIT": "BOOLEAN", + "DATETIME2": "TIMESTAMP", + "SMALLDATETIME": "TIMESTAMP", + "DATETIMEOFFSET": "TIMESTAMPTZ", + "UNIQUEIDENTIFIER": "UUID", + "FLOAT": "DOUBLE", + "REAL": "FLOAT", + "MONEY": "DECIMAL(19,4)", + "SMALLMONEY": "DECIMAL(10,4)", +} FALLBACK_DECIMAL_TYPES: frozenset[str] = frozenset({"DECIMAL", "DEC", "NUMERIC", "NUMBER"}) FALLBACK_DATE_TYPE: str = "DATE" FALLBACK_TIME_TYPES: frozenset[str] = frozenset({"TIME", "TIME WITH TIME ZONE", "TIMETZ"}) diff --git a/tests/unit/src/sqlbuild/executor/scenario/_helpers/test_capture_columns.py b/tests/unit/src/sqlbuild/executor/scenario/_helpers/test_capture_columns.py index 40da596225..f39bcb92b7 100644 --- a/tests/unit/src/sqlbuild/executor/scenario/_helpers/test_capture_columns.py +++ b/tests/unit/src/sqlbuild/executor/scenario/_helpers/test_capture_columns.py @@ -578,6 +578,66 @@ warehouse_type="STRUCT>, `b c`: TINYINT>", expected_local_type='STRUCT(a MAP(VARCHAR, BIGINT)[], "b c" TINYINT)', ), + ScenarioSnapshotLocalTypeMappingTestCase( + description="sql server datetime2 is a timestamp", + sql_analysis_dialect="tsql", + warehouse_type="datetime2", + expected_local_type="TIMESTAMP", + ), + ScenarioSnapshotLocalTypeMappingTestCase( + description="sql server smalldatetime is a timestamp", + sql_analysis_dialect="tsql", + warehouse_type="smalldatetime", + expected_local_type="TIMESTAMP", + ), + ScenarioSnapshotLocalTypeMappingTestCase( + description="sql server datetimeoffset is a timestamp with time zone", + sql_analysis_dialect="tsql", + warehouse_type="datetimeoffset", + expected_local_type="TIMESTAMPTZ", + ), + ScenarioSnapshotLocalTypeMappingTestCase( + description="sql server uniqueidentifier is a uuid", + sql_analysis_dialect="tsql", + warehouse_type="uniqueidentifier", + expected_local_type="UUID", + ), + ScenarioSnapshotLocalTypeMappingTestCase( + description="sql server bit is a boolean", + sql_analysis_dialect="tsql", + warehouse_type="bit", + expected_local_type="BOOLEAN", + ), + ScenarioSnapshotLocalTypeMappingTestCase( + description="sql server float is eight bytes", + sql_analysis_dialect="tsql", + warehouse_type="float", + expected_local_type="DOUBLE", + ), + ScenarioSnapshotLocalTypeMappingTestCase( + description="sql server real is four bytes", + sql_analysis_dialect="tsql", + warehouse_type="real", + expected_local_type="FLOAT", + ), + ScenarioSnapshotLocalTypeMappingTestCase( + description="sql server money is an exact four-place decimal", + sql_analysis_dialect="tsql", + warehouse_type="money", + expected_local_type="DECIMAL(19,4)", + ), + ScenarioSnapshotLocalTypeMappingTestCase( + description="databricks float keeps four bytes, nested too", + sql_analysis_dialect="databricks", + warehouse_type="ARRAY", + expected_local_type="FLOAT[]", + ), + ScenarioSnapshotLocalTypeMappingTestCase( + description="generic float keeps four bytes", + sql_analysis_dialect="generic", + warehouse_type="FLOAT", + expected_local_type="FLOAT", + ), ScenarioSnapshotLocalTypeMappingTestCase( description="databricks interval year is preserved", sql_analysis_dialect="databricks", @@ -1282,6 +1342,66 @@ def test_given_warehouse_type_when_mapping_to_local_type_then_returns_expected_d warehouse_type="STRUCT>, `b c`: TINYINT>", expected_local_type='STRUCT(a MAP(VARCHAR, BIGINT)[], "b c" TINYINT)', ), + ScenarioSnapshotLocalTypeMappingTestCase( + description="sql server datetime2 is a timestamp", + sql_analysis_dialect="tsql", + warehouse_type="datetime2", + expected_local_type="TIMESTAMP", + ), + ScenarioSnapshotLocalTypeMappingTestCase( + description="sql server smalldatetime is a timestamp", + sql_analysis_dialect="tsql", + warehouse_type="smalldatetime", + expected_local_type="TIMESTAMP", + ), + ScenarioSnapshotLocalTypeMappingTestCase( + description="sql server datetimeoffset is a timestamp with time zone", + sql_analysis_dialect="tsql", + warehouse_type="datetimeoffset", + expected_local_type="TIMESTAMPTZ", + ), + ScenarioSnapshotLocalTypeMappingTestCase( + description="sql server uniqueidentifier is a uuid", + sql_analysis_dialect="tsql", + warehouse_type="uniqueidentifier", + expected_local_type="UUID", + ), + ScenarioSnapshotLocalTypeMappingTestCase( + description="sql server bit is a boolean", + sql_analysis_dialect="tsql", + warehouse_type="bit", + expected_local_type="BOOLEAN", + ), + ScenarioSnapshotLocalTypeMappingTestCase( + description="sql server float is eight bytes", + sql_analysis_dialect="tsql", + warehouse_type="float", + expected_local_type="DOUBLE", + ), + ScenarioSnapshotLocalTypeMappingTestCase( + description="sql server real is four bytes", + sql_analysis_dialect="tsql", + warehouse_type="real", + expected_local_type="FLOAT", + ), + ScenarioSnapshotLocalTypeMappingTestCase( + description="sql server money is an exact four-place decimal", + sql_analysis_dialect="tsql", + warehouse_type="money", + expected_local_type="DECIMAL(19,4)", + ), + ScenarioSnapshotLocalTypeMappingTestCase( + description="databricks float keeps four bytes, nested too", + sql_analysis_dialect="databricks", + warehouse_type="ARRAY", + expected_local_type="FLOAT[]", + ), + ScenarioSnapshotLocalTypeMappingTestCase( + description="generic float keeps four bytes", + sql_analysis_dialect="generic", + warehouse_type="FLOAT", + expected_local_type="FLOAT", + ), ScenarioSnapshotLocalTypeMappingTestCase( description="databricks interval year is preserved", sql_analysis_dialect="databricks", From 4637fb4478c815c3bf20343823b18cc10dccff98 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 05:56:49 +0000 Subject: [PATCH 023/106] chore(fensu): allow the cast-target walk next to the other JSON tree walks --- fensu.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fensu.toml b/fensu.toml index bee7f18a71..bf5365df88 100644 --- a/fensu.toml +++ b/fensu.toml @@ -2932,3 +2932,7 @@ reason = "The override keeps queue.Queue.put's positional protocol, which every rule = "FFS201" path = "src/sqlbuild/orchestration_declarations/models/sensor_context.py" reason = "Only SensorContext lives here: invocation-local ctx.cursor assignment proposes progress, committed solely by native fenced completion. The package regression guards this declaration-only scope; class-symbol exceptions are unsupported." + +[[dupes.allowlist]] +paths = ["crates/sqlbuild-analysis/src/warehouse_types/main/parse_warehouse_type.rs", "crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dict_walk.rs", "crates/sqlbuild-analysis/src/relation_names/_helpers/walk.rs"] +reason = "Each is a short stack walk over a JSON syntax tree with its own stop and collect rule: cast_targets collects every cast's target type, branch_selects stops at each set-operation branch, and walked lists every object. A shared visitor would need a callback for each rule and save no logic." From 0352b445946cc59551ed5703b412d48a630a81c8 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 06:29:37 +0000 Subject: [PATCH 024/106] refactor(rules): name the date-part and unavailable-rule loops --- .../src/query_facts/_helpers/facts.rs | 34 ++++++++++--------- .../src/query_facts/_helpers/tokens.rs | 2 +- .../src/rules/_helpers/sql_test_rules.rs | 14 ++++---- 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs b/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs index b1ee3aa54f..959cf65122 100644 --- a/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs +++ b/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs @@ -520,22 +520,24 @@ fn date_part_arguments(root: &Expression, dialect: Dialect) -> HashSet<*const Ex if dialect != Dialect::Snowflake { return HashSet::new(); } - root.dfs() - .filter_map(|node| match node { - Expression::Function(function) => { - let name = function.name.to_ascii_uppercase(); - let (_, _, index) = SNOWFLAKE_DATE_PART_ARGUMENTS - .iter() - .find(|(call, count, _)| *call == name && *count == function.args.len())?; - function - .args - .get(*index) - .filter(|argument| direct_string_literal(argument).is_some()) - .map(std::ptr::from_ref) - } - _ => None, - }) - .collect() + root.dfs().filter_map(date_part_argument).collect() +} + +/// The quoted date-part argument of `node` when it is a Snowflake date-part call. +fn date_part_argument(node: &Expression) -> Option<*const Expression> { + let Expression::Function(function) = node else { + return None; + }; + let name = function.name.to_ascii_uppercase(); + for (call, count, index) in SNOWFLAKE_DATE_PART_ARGUMENTS { + if call == name && count == function.args.len() { + let argument = function.args.get(index)?; + return direct_string_literal(argument) + .is_some() + .then(|| std::ptr::from_ref(argument)); + } + } + None } fn comparison_parts(node: &Expression) -> Option> { diff --git a/crates/sqlbuild-rules/src/query_facts/_helpers/tokens.rs b/crates/sqlbuild-rules/src/query_facts/_helpers/tokens.rs index 18a1b33bb7..b847237997 100644 --- a/crates/sqlbuild-rules/src/query_facts/_helpers/tokens.rs +++ b/crates/sqlbuild-rules/src/query_facts/_helpers/tokens.rs @@ -164,7 +164,7 @@ impl<'a> QueryTokens<'a> { let mut bytes: Vec = sql.char_indices().map(|(byte, _)| byte).collect(); bytes.push(sql.len()); let character = |byte: usize| bytes.partition_point(|offset| *offset < byte); - let tokens = tokens + let tokens: Vec> = tokens .into_iter() .filter_map(|token| match token.kind { TokenKind::Significant(token_type) => Some(CodeToken { diff --git a/crates/sqlbuild-rules/src/rules/_helpers/sql_test_rules.rs b/crates/sqlbuild-rules/src/rules/_helpers/sql_test_rules.rs index 9eb7006c5d..f53e86786c 100644 --- a/crates/sqlbuild-rules/src/rules/_helpers/sql_test_rules.rs +++ b/crates/sqlbuild-rules/src/rules/_helpers/sql_test_rules.rs @@ -62,13 +62,13 @@ fn evaluated_project(evaluation: ProjectEvaluationRequest<'_>) -> Result = fixture_rules + .iter() + .map(|rule| rule.code.as_str()) + .collect(); + for path in paths { + faults.push(rules_unavailable(path, codes.iter().copied(), dialect)); + } } for rule in fixture_rules.iter().filter(|_| facts_available(dialect)) { for test in &tests { From 5f253c1a790506aad9aeae17dabe5c354c98ba76 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 06:38:15 +0000 Subject: [PATCH 025/106] docs(analysis): keep the warehouse type binding's doc comment to one line --- .../sqlbuild-python/src/bindings/_helpers/analysis/queries.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/analysis/queries.rs b/crates/sqlbuild-python/src/bindings/_helpers/analysis/queries.rs index 0bc7eeaa94..b719e29b6b 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/analysis/queries.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/analysis/queries.rs @@ -14,9 +14,7 @@ fn query_fingerprint(py: Python<'_>, sql: &str, dialect: &str) -> PyResult, spelling: &str, dialect: &str) -> PyResult> { py.compiler_detach(|| { From 2d146f511a15e7a5b2b9fbf70e9ad4c759f25466 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 07:22:56 +0000 Subject: [PATCH 026/106] feat(types): normalize adapter types without Polyglot Read each type with SQLQuack's parser for the dialect, or SQLBuild's own type reader where a dialect has none, and spell it the way the Polyglot-based normalization did so the same aliases stay equal. normalize_type now raises only ValueError, for an unknown dialect. --- Cargo.lock | 1 + crates/sqlbuild-analysis/Cargo.toml | 1 + .../main/classify_cursor_sql_type.rs | 6 +- .../src/type_system/_helpers/mod.rs | 3 +- .../src/type_system/_helpers/parsed.rs | 234 +- .../src/type_system/_helpers/shallow_args.rs | 442 -- .../src/type_system/_helpers/spelling.rs | 510 ++ .../src/type_system/_helpers/type_reader.rs | 440 ++ .../src/type_system/constants.rs | 42 +- .../src/type_system/errors.rs | 7 +- .../src/type_system/main/mod.rs | 2 +- .../src/type_system/main/normalize_type.rs | 40 +- .../type_system/main/polyglot_type_args.rs | 17 - .../src/type_system/main/type_args.rs | 13 + .../src/type_system/models.rs | 2 +- .../src/type_system/tests/alias_groups.json | 327 ++ .../src/type_system/tests/helpers.rs | 10 +- .../type_system/tests/test_normalize_type.rs | 12 +- .../src/type_system/tests/test_same_type.rs | 59 +- .../src/type_system/tests/test_types.rs | 7 + .../src/warehouse_types/_helpers/mod.rs | 1 + .../warehouse_types/_helpers/type_trees.rs | 67 + .../src/warehouse_types/main/mod.rs | 1 + .../main/parse_warehouse_type.rs | 57 +- .../src/warehouse_types/main/type_tree.rs | 15 + .../src/warehouse_types/mod.rs | 2 + .../src/warehouse_types/models.rs | 14 + .../_helpers/contracts/model_contracts.rs | 2 +- .../_helpers/semantic_checks/completion.rs | 2 +- .../_helpers/semantic_checks/failures.rs | 6 +- .../_helpers/semantic_checks/metadata.rs | 2 +- .../_helpers/semantic_checks/type_recovery.rs | 2 +- .../bindings/_helpers/type_system/equality.rs | 5 +- .../_helpers/type_system/normalization.rs | 21 +- fensu.toml | 4 +- src/sqlbuild/adapter/type_system/constants.py | 2 +- .../main/_native_normalize_type.py | 2 +- ...ped_nullability_and_dynamic_contracts.json | 4 +- ...rized_nested_padded_and_invalid_types.json | 4228 ++++++++--------- .../adapter/type_system/_test_types.py | 2 +- .../sqlbuild/adapter/type_system/helpers.py | 2 +- .../test_native_type_normalization.py | 2 +- 42 files changed, 3806 insertions(+), 2812 deletions(-) delete mode 100644 crates/sqlbuild-analysis/src/type_system/_helpers/shallow_args.rs create mode 100644 crates/sqlbuild-analysis/src/type_system/_helpers/spelling.rs create mode 100644 crates/sqlbuild-analysis/src/type_system/_helpers/type_reader.rs delete mode 100644 crates/sqlbuild-analysis/src/type_system/main/polyglot_type_args.rs create mode 100644 crates/sqlbuild-analysis/src/type_system/main/type_args.rs create mode 100644 crates/sqlbuild-analysis/src/type_system/tests/alias_groups.json create mode 100644 crates/sqlbuild-analysis/src/warehouse_types/_helpers/mod.rs create mode 100644 crates/sqlbuild-analysis/src/warehouse_types/_helpers/type_trees.rs create mode 100644 crates/sqlbuild-analysis/src/warehouse_types/main/type_tree.rs create mode 100644 crates/sqlbuild-analysis/src/warehouse_types/models.rs diff --git a/Cargo.lock b/Cargo.lock index 4d81a5009e..fdfb5d93bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4145,6 +4145,7 @@ dependencies = [ "sqlquack-syntax", "sqlquack-syntax-bigquery", "sqlquack-syntax-postgres", + "stacker", ] [[package]] diff --git a/crates/sqlbuild-analysis/Cargo.toml b/crates/sqlbuild-analysis/Cargo.toml index 9a7d4b5a89..3bae24f07f 100644 --- a/crates/sqlbuild-analysis/Cargo.toml +++ b/crates/sqlbuild-analysis/Cargo.toml @@ -29,6 +29,7 @@ sqlclean.workspace = true sqlquack-syntax.workspace = true sqlquack-syntax-bigquery.workspace = true sqlquack-syntax-postgres.workspace = true +stacker.workspace = true [lints] workspace = true diff --git a/crates/sqlbuild-analysis/src/planner_strategy/main/classify_cursor_sql_type.rs b/crates/sqlbuild-analysis/src/planner_strategy/main/classify_cursor_sql_type.rs index 22dedf16bd..1636707d0d 100644 --- a/crates/sqlbuild-analysis/src/planner_strategy/main/classify_cursor_sql_type.rs +++ b/crates/sqlbuild-analysis/src/planner_strategy/main/classify_cursor_sql_type.rs @@ -7,9 +7,9 @@ use crate::planner_strategy::_helpers::type_names::{argument, named_type}; use crate::planner_strategy::constants::{GENERIC_DIALECT, POLYGLOT_CUSTOM_TYPE_NAME}; use crate::planner_strategy::errors::TypeParseFailure; use crate::planner_strategy::models::CursorType; -use crate::type_system::main::polyglot_type_args::polyglot_type_args; +use crate::type_system::main::type_args::type_args; -/// Timestamp or integer for supported cross-adapter types; `Err` when Polyglot cannot parse an unknown type. +/// Timestamp or integer for supported cross-adapter types; `Err` when an unknown type does not parse. pub fn classify_cursor_sql_type(sql_type: &str) -> Result, TypeParseFailure> { let normalized: String = sql_type .to_uppercase() @@ -29,7 +29,7 @@ pub fn classify_cursor_sql_type(sql_type: &str) -> Result, Ty return Ok(Some(cursor_type)); } let args: Map = - polyglot_type_args(&normalized, GENERIC_DIALECT).map_err(|message| TypeParseFailure { + type_args(&normalized, GENERIC_DIALECT).map_err(|message| TypeParseFailure { normalized_type: normalized.clone(), message, })?; diff --git a/crates/sqlbuild-analysis/src/type_system/_helpers/mod.rs b/crates/sqlbuild-analysis/src/type_system/_helpers/mod.rs index 70cbe23166..542e409872 100644 --- a/crates/sqlbuild-analysis/src/type_system/_helpers/mod.rs +++ b/crates/sqlbuild-analysis/src/type_system/_helpers/mod.rs @@ -2,4 +2,5 @@ pub(crate) mod dialects; pub(crate) mod fallback; pub(crate) mod parsed; pub(crate) mod python_text; -pub(crate) mod shallow_args; +pub(crate) mod spelling; +pub(crate) mod type_reader; diff --git a/crates/sqlbuild-analysis/src/type_system/_helpers/parsed.rs b/crates/sqlbuild-analysis/src/type_system/_helpers/parsed.rs index 1ee482d2c8..e09d002fee 100644 --- a/crates/sqlbuild-analysis/src/type_system/_helpers/parsed.rs +++ b/crates/sqlbuild-analysis/src/type_system/_helpers/parsed.rs @@ -1,97 +1,122 @@ -//! Python's `_normalize_with_polyglot`: the parsed type's SQL and arguments. +//! A parsed type's normalization: its tag, parameters and dialect spelling. -use polyglot_sql::{ - ComplexityGuardOptions, DataType, Dialect, DialectType, Expression, ParseOptions, -}; -use serde_json::{Map, Value}; +use serde_json::Value; +use sqlquack_syntax::tokens::models::Dialect; use crate::type_system::_helpers::fallback::{ normalize_with_fallback, python_or_default, simple, snowflake_integer, timestamp_normalized_name, }; use crate::type_system::_helpers::python_text::{python_int, split_type_and_params, upper}; -use crate::type_system::_helpers::shallow_args::shallow_args; +use crate::type_system::_helpers::spelling::{SpellingDialect, spell}; +use crate::type_system::_helpers::type_reader::read_type; use crate::type_system::constants::{ BIGNUMERIC_TYPE_NAME, BIGQUERY_STRING_TYPE_NAME, BOOLEAN_TYPE_NAMES, - CUSTOM_NORMALIZATION_TYPE_NAMES, DATE_TYPE_NAME, DATETIME_TYPE_NAME, DECIMAL_TYPE_NAMES, - FLOAT_TYPE_NAMES, FLOAT_WIRE_TYPE_NAME, INTEGER_PARSE_TYPE_NAMES, INTEGER_PRECISION, - INTEGER_SCALE, INTEGER_TYPE_NAMES, MAX_FUNCTION_CALL_DEPTH, POLYGLOT_CUSTOM_TYPE_NAME, - POLYGLOT_TYPE_NAME_ALIASES, SIZED_TEXT_TYPE_NAMES, STRING_TYPE_NAMES, TIMESTAMP_TYPE_NAMES, - UNBOUNDED_TEXT_TYPE_NAMES, VARCHAR_TYPE_NAME, + CUSTOM_NORMALIZATION_TYPE_NAMES, CUSTOM_TYPE_TAG, DATE_TYPE_NAME, DATETIME_TYPE_NAME, + DECIMAL_TYPE_NAMES, FLOAT_TYPE_NAMES, FLOAT_WIRE_TYPE_NAME, INTEGER_PARSE_TYPE_NAMES, + INTEGER_PRECISION, INTEGER_SCALE, INTEGER_TYPE_NAMES, MAX_TYPE_NESTING, SIZED_TEXT_TYPE_NAMES, + STRING_TYPE_NAMES, TIMESTAMP_TYPE_NAMES, TYPE_TAG_ALIASES, UNBOUNDED_TEXT_TYPE_NAMES, + VARCHAR_TYPE_NAME, }; -use crate::type_system::errors::TypeNormalizationError; use crate::type_system::models::{NormalizedType, PythonInteger, TypeDialect, TypeFamily}; +use crate::warehouse_types::main::type_tree::type_tree; +use crate::warehouse_types::models::TypeParser; -/// Parse one type with SQLBuild's Polyglot guard; the error text is what Python logs. -pub(crate) fn parse_type(type_sql: &str, dialect: &Dialect) -> Result { - dialect - .parse_data_type_with_options( - type_sql, - &ParseOptions { - complexity_guard: Some(ComplexityGuardOptions { - max_function_call_depth: Some(MAX_FUNCTION_CALL_DEPTH), - ..Default::default() - }), - }, - ) - .map_err(|error| error.to_string()) +/// The type tree of `type_sql` from SQLQuack's parser, else SQLBuild's reader, or the error. +pub(crate) fn parse_type(type_sql: &str, dialect: SpellingDialect) -> Result { + if nesting_bound(type_sql) > MAX_TYPE_NESTING { + return Err("the type nests too deeply".to_owned()); + } + let parser: TypeParser = match dialect { + SpellingDialect::Snowflake => TypeParser::Core(Dialect::Snowflake), + SpellingDialect::DuckDb => TypeParser::Core(Dialect::DuckDB), + SpellingDialect::Sqlite => TypeParser::Core(Dialect::SQLite), + SpellingDialect::Postgres => TypeParser::Postgres, + SpellingDialect::BigQuery => TypeParser::BigQuery, + SpellingDialect::Generic + | SpellingDialect::MySql + | SpellingDialect::Databricks + | SpellingDialect::Tsql + | SpellingDialect::ClickHouse => return read_type(type_sql, dialect), + }; + let native: Result = type_tree(type_sql, parser); + match native { + Ok(tree) if !has_custom(&tree) => Ok(tree), + Ok(tree) => Ok(read_type(type_sql, dialect).unwrap_or(tree)), + Err(error) => read_type(type_sql, dialect).map_err(|_| error), + } } -/// The parsed type's SQL, in generic spelling when the dialect refuses a type it cannot hold. -fn generated_type( - expression: &Expression, - polyglot: &Dialect, -) -> Result { - match polyglot.generate(expression) { - Ok(generated) => Ok(generated), - Err(polyglot_sql::error::Error::Unsupported { .. }) => Dialect::get(DialectType::Generic) - .generate(expression) - .map_err(|error| TypeNormalizationError::Generation(error.to_string())), - Err(error) => Err(TypeNormalizationError::Generation(error.to_string())), +/// A bound on the type tree's depth: the deepest brackets plus the array suffixes. +pub(crate) fn nesting_bound(type_sql: &str) -> usize { + let (mut depth, mut deepest, mut suffixes): (usize, usize, usize) = (0, 0, 0); + for character in type_sql.chars() { + match character { + '(' | '<' => { + depth += 1; + deepest = deepest.max(depth); + } + ')' | '>' => depth = depth.saturating_sub(1), + '[' => suffixes += 1, + _ => {} + } + } + deepest + suffixes +} + +/// Whether `tree` or any type nested in it is an unknown type name. +fn has_custom(tree: &Value) -> bool { + match tree { + Value::Object(fields) => { + fields + .get("data_type") + .and_then(Value::as_str) + .is_some_and(|tag| tag.eq_ignore_ascii_case(CUSTOM_TYPE_TAG)) + || fields.values().any(has_custom) + } + Value::Array(items) => items.iter().any(has_custom), + _ => false, } } -/// Python's `_normalized_from_parsed_type`. +/// The normalization of a parsed type. pub(crate) fn normalize_parsed( - parsed: DataType, - polyglot: &Dialect, + tree: &Value, + spelling: SpellingDialect, dialect: Option, -) -> Result { - let args: Map = shallow_args(&parsed); - let expression: Expression = Expression::DataType(parsed); - let generated: String = generated_type(&expression, polyglot)?; - let normalized_name: String = upper(&generated).replace(' ', ""); - let dtype_name: String = polyglot_type_name(&args); - let params: Vec = polyglot_type_params(&args); +) -> NormalizedType { + let normalized_name: String = upper(&spell(tree, spelling)).replace(' ', ""); + let dtype_name: String = type_name(tree); + let params: Vec = type_params(tree); let snowflake: bool = dialect == Some(TypeDialect::Snowflake); let bigquery: bool = dialect == Some(TypeDialect::BigQuery); let dtype: &str = dtype_name.as_str(); if bigquery && INTEGER_PARSE_TYPE_NAMES.contains(&dtype) { - return Ok(simple("INT64", TypeFamily::Integer)); + return simple("INT64", TypeFamily::Integer); } if bigquery && dtype == BIGNUMERIC_TYPE_NAME { - return Ok(simple("BIGNUMERIC", TypeFamily::Decimal)); + return simple("BIGNUMERIC", TypeFamily::Decimal); } if bigquery && dtype == FLOAT_WIRE_TYPE_NAME { - return Ok(simple("FLOAT64", TypeFamily::Float)); + return simple("FLOAT64", TypeFamily::Float); } - if dtype == POLYGLOT_CUSTOM_TYPE_NAME { - let raw_name: String = upper(&match args.get("name") { - Some(value) => python_str(value), + if dtype == CUSTOM_TYPE_TAG { + let raw_name: String = upper(&match tree.get("name").and_then(Value::as_str) { + Some(name) => name.to_owned(), None => normalized_name.clone(), }) .replace(' ', ""); if snowflake && raw_name.starts_with("NUMBER") { - return Ok(snowflake_number(&raw_name)); + return snowflake_number(&raw_name); } - return Ok(normalize_with_fallback(&raw_name, dialect)); + return normalize_with_fallback(&raw_name, dialect); } if INTEGER_TYPE_NAMES.contains(&dtype) { if snowflake { - return Ok(snowflake_integer()); + return snowflake_integer(); } - return Ok(simple(&normalized_name, TypeFamily::Integer)); + return simple(&normalized_name, TypeFamily::Integer); } if DECIMAL_TYPE_NAMES.contains(&dtype) { let mut precision: Option = params.first().cloned(); @@ -102,16 +127,16 @@ pub(crate) fn normalize_parsed( scale = Some(INTEGER_SCALE.into()); name = format!("DECIMAL({INTEGER_PRECISION},{INTEGER_SCALE})"); } - return Ok(NormalizedType { + return NormalizedType { normalized_name: name, family: TypeFamily::Decimal, precision, scale, length: None, - }); + }; } if FLOAT_TYPE_NAMES.contains(&dtype) { - return Ok(simple(&normalized_name, TypeFamily::Float)); + return simple(&normalized_name, TypeFamily::Float); } if STRING_TYPE_NAMES.contains(&dtype) { let mut length: Option = params.first().cloned(); @@ -126,30 +151,44 @@ pub(crate) fn normalize_parsed( } else if length.is_some() && SIZED_TEXT_TYPE_NAMES.contains(&base_name) { name = name.replacen(base_name, VARCHAR_TYPE_NAME, 1); } - return Ok(NormalizedType { + return NormalizedType { normalized_name: name, family: TypeFamily::String, precision: None, scale: None, length, - }); + }; } if BOOLEAN_TYPE_NAMES.contains(&dtype) { - return Ok(simple(&normalized_name, TypeFamily::Boolean)); + return simple(&normalized_name, TypeFamily::Boolean); } if TIMESTAMP_TYPE_NAMES.contains(&dtype) { - return Ok(simple( + if bigquery && normalized_name == DATETIME_TYPE_NAME { + return simple(&normalized_name, TypeFamily::Datetime); + } + return simple( ×tamp_normalized_name(&normalized_name, dialect), TypeFamily::Timestamp, - )); + ); } if dtype == DATE_TYPE_NAME { - return Ok(simple(&normalized_name, TypeFamily::Date)); + return simple(&normalized_name, TypeFamily::Date); } if dtype == DATETIME_TYPE_NAME { - return Ok(simple(&normalized_name, TypeFamily::Datetime)); + return simple(&normalized_name, TypeFamily::Datetime); } - Ok(simple(&normalized_name, TypeFamily::Other)) + simple(&normalized_name, TypeFamily::Other) +} + +/// The scalar parameters of a type tree, keyed as the tree names them. +pub(crate) fn type_args(tree: &Value) -> serde_json::Map { + let mut args: serde_json::Map = serde_json::Map::new(); + for (key, value) in tree.as_object().into_iter().flatten() { + if !value.is_array() && !value.is_object() { + args.insert(key.clone(), value.clone()); + } + } + args } /// Snowflake's `NUMBER(p,s)` spelled as a custom type: Python's `DECIMAL` rewrite. @@ -172,57 +211,30 @@ fn snowflake_number(raw_name: &str) -> NormalizedType { } } -/// Python's `_polyglot_type_name`. -fn polyglot_type_name(args: &Map) -> String { - let data_type: String = upper(&match args.get("data_type") { - Some(value) => python_str(value), - None => String::new(), - }); - if let Some((_, alias)) = POLYGLOT_TYPE_NAME_ALIASES - .iter() - .find(|(name, _)| *name == data_type) - { +/// The type's tag, upper-cased with its word separators as the type names spell them. +fn type_name(tree: &Value) -> String { + let tag: String = upper(tree.get("data_type").and_then(Value::as_str).unwrap_or("")); + if let Some((_, alias)) = TYPE_TAG_ALIASES.iter().find(|(name, _)| *name == tag) { return (*alias).to_owned(); } - if data_type == POLYGLOT_CUSTOM_TYPE_NAME { - let name: String = upper(&match args.get("name") { - Some(value) => python_str(value), - None => String::new(), - }) - .replace(' ', ""); + if tag == CUSTOM_TYPE_TAG { + let name: String = + upper(tree.get("name").and_then(Value::as_str).unwrap_or("")).replace(' ', ""); if CUSTOM_NORMALIZATION_TYPE_NAMES.contains(&name.as_str()) { return name; } } - data_type + tag } -/// Python's `_polyglot_type_params`: `int(value)` of each parameter, skipping what it rejects. -fn polyglot_type_params(args: &Map) -> Vec { - let mut params: Vec = Vec::new(); - for key in ["precision", "scale", "length"] { - let Some(value) = args.get(key) else { - continue; - }; - let param: Option = match value { - Value::Null | Value::Array(_) | Value::Object(_) => None, - Value::Bool(flag) => Some(i64::from(*flag).into()), +/// The type's precision, scale and length, in that order, where it has them. +fn type_params(tree: &Value) -> Vec { + ["precision", "scale", "length"] + .iter() + .filter_map(|key| tree.get(*key)) + .filter_map(|value| match value { Value::Number(number) => python_int(&number.to_string()), - Value::String(text) => python_int(text), - }; - params.extend(param); - } - params -} - -/// Python's `str(value)` for the scalar values Polyglot gives a parsed type's tag and name. -fn python_str(value: &Value) -> String { - match value { - Value::String(text) => text.clone(), - Value::Null => "None".to_owned(), - Value::Bool(true) => "True".to_owned(), - Value::Bool(false) => "False".to_owned(), - Value::Number(number) => number.to_string(), - Value::Array(_) | Value::Object(_) => value.to_string(), - } + _ => None, + }) + .collect() } diff --git a/crates/sqlbuild-analysis/src/type_system/_helpers/shallow_args.rs b/crates/sqlbuild-analysis/src/type_system/_helpers/shallow_args.rs deleted file mode 100644 index 411f17c653..0000000000 --- a/crates/sqlbuild-analysis/src/type_system/_helpers/shallow_args.rs +++ /dev/null @@ -1,442 +0,0 @@ -//! The wheel's `DataType.args` scalars, read without walking nested types. - -use std::fmt::Display; - -use serde::ser::{ - Impossible, Serialize, SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant, - SerializeTuple, SerializeTupleStruct, SerializeTupleVariant, Serializer, -}; -use serde_json::{Map, Number, Value}; - -/// The top-level scalar fields of one serialized value; nested values are absent. -pub(crate) fn shallow_args(value: &T) -> Map { - value.serialize(Fields::default()).unwrap_or_default() -} - -#[derive(Debug)] -pub(crate) struct Unsupported; - -impl Display for Unsupported { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("not a struct") - } -} - -impl std::error::Error for Unsupported {} - -impl serde::ser::Error for Unsupported { - fn custom(_message: T) -> Self { - Self - } -} - -/// Collects one struct's scalar fields. -#[derive(Default)] -struct Fields { - fields: Map, -} - -impl SerializeStruct for Fields { - type Ok = Map; - type Error = Unsupported; - - fn serialize_field( - &mut self, - key: &'static str, - value: &T, - ) -> Result<(), Unsupported> { - if let Some(scalar) = value.serialize(Scalar)? { - let _ = self.fields.insert(key.to_owned(), scalar); - } - Ok(()) - } - - fn end(self) -> Result, Unsupported> { - Ok(self.fields) - } -} - -impl SerializeStructVariant for Fields { - type Ok = Map; - type Error = Unsupported; - - fn serialize_field( - &mut self, - key: &'static str, - value: &T, - ) -> Result<(), Unsupported> { - SerializeStruct::serialize_field(self, key, value) - } - - fn end(self) -> Result, Unsupported> { - Ok(self.fields) - } -} - -impl SerializeMap for Fields { - type Ok = Map; - type Error = Unsupported; - - fn serialize_key(&mut self, _key: &T) -> Result<(), Unsupported> { - Err(Unsupported) - } - - fn serialize_value(&mut self, _value: &T) -> Result<(), Unsupported> { - Err(Unsupported) - } - - fn end(self) -> Result, Unsupported> { - Ok(self.fields) - } -} - -macro_rules! not_a_struct { - ($($method:ident($($argument:ty),*)),* $(,)?) => { - $(fn $method(self, $(_: $argument),*) -> Result { - Err(Unsupported) - })* - }; -} - -impl Serializer for Fields { - type Ok = Map; - type Error = Unsupported; - type SerializeSeq = Impossible; - type SerializeTuple = Impossible; - type SerializeTupleStruct = Impossible; - type SerializeTupleVariant = Impossible; - type SerializeMap = Self; - type SerializeStruct = Self; - type SerializeStructVariant = Self; - - not_a_struct!( - serialize_bool(bool), - serialize_i8(i8), - serialize_i16(i16), - serialize_i32(i32), - serialize_i64(i64), - serialize_u8(u8), - serialize_u16(u16), - serialize_u32(u32), - serialize_u64(u64), - serialize_f32(f32), - serialize_f64(f64), - serialize_char(char), - serialize_str(&str), - serialize_bytes(&[u8]), - serialize_none(), - serialize_unit(), - serialize_unit_struct(&'static str), - serialize_unit_variant(&'static str, u32, &'static str), - ); - - fn serialize_some(self, value: &T) -> Result { - value.serialize(self) - } - - fn serialize_newtype_struct( - self, - _name: &'static str, - value: &T, - ) -> Result { - value.serialize(self) - } - - fn serialize_newtype_variant( - self, - _name: &'static str, - _index: u32, - _variant: &'static str, - _value: &T, - ) -> Result { - Err(Unsupported) - } - - fn serialize_seq(self, _len: Option) -> Result { - Err(Unsupported) - } - - fn serialize_tuple(self, _len: usize) -> Result { - Err(Unsupported) - } - - fn serialize_tuple_struct( - self, - _name: &'static str, - _len: usize, - ) -> Result { - Err(Unsupported) - } - - fn serialize_tuple_variant( - self, - _name: &'static str, - _index: u32, - _variant: &'static str, - _len: usize, - ) -> Result { - Err(Unsupported) - } - - fn serialize_map(self, _len: Option) -> Result { - Ok(self) - } - - fn serialize_struct( - self, - _name: &'static str, - _len: usize, - ) -> Result { - Ok(self) - } - - fn serialize_struct_variant( - self, - _name: &'static str, - _index: u32, - _variant: &'static str, - _len: usize, - ) -> Result { - Ok(self) - } -} - -/// One field's value if it is a scalar; nested values are skipped without visiting them. -struct Scalar; - -/// Swallows the children of a nested value unvisited. -struct Skipped; - -macro_rules! skip_children { - ($($trait:ident :: $method:ident),* $(,)?) => { - $(impl $trait for Skipped { - type Ok = Option; - type Error = Unsupported; - - fn $method(&mut self, _value: &T) -> Result<(), Unsupported> { - Ok(()) - } - - fn end(self) -> Result, Unsupported> { - Ok(None) - } - })* - }; -} - -skip_children!( - SerializeSeq::serialize_element, - SerializeTuple::serialize_element, - SerializeTupleStruct::serialize_field, - SerializeTupleVariant::serialize_field, -); - -impl SerializeMap for Skipped { - type Ok = Option; - type Error = Unsupported; - - fn serialize_key(&mut self, _key: &T) -> Result<(), Unsupported> { - Ok(()) - } - - fn serialize_value(&mut self, _value: &T) -> Result<(), Unsupported> { - Ok(()) - } - - fn end(self) -> Result, Unsupported> { - Ok(None) - } -} - -impl SerializeStruct for Skipped { - type Ok = Option; - type Error = Unsupported; - - fn serialize_field( - &mut self, - _key: &'static str, - _value: &T, - ) -> Result<(), Unsupported> { - Ok(()) - } - - fn end(self) -> Result, Unsupported> { - Ok(None) - } -} - -impl SerializeStructVariant for Skipped { - type Ok = Option; - type Error = Unsupported; - - fn serialize_field( - &mut self, - _key: &'static str, - _value: &T, - ) -> Result<(), Unsupported> { - Ok(()) - } - - fn end(self) -> Result, Unsupported> { - Ok(None) - } -} - -fn number(value: impl Into) -> Result, Unsupported> { - Ok(Some(Value::Number(value.into()))) -} - -impl Serializer for Scalar { - type Ok = Option; - type Error = Unsupported; - type SerializeSeq = Skipped; - type SerializeTuple = Skipped; - type SerializeTupleStruct = Skipped; - type SerializeTupleVariant = Skipped; - type SerializeMap = Skipped; - type SerializeStruct = Skipped; - type SerializeStructVariant = Skipped; - - fn serialize_bool(self, value: bool) -> Result { - Ok(Some(Value::Bool(value))) - } - - fn serialize_i8(self, value: i8) -> Result { - number(value) - } - - fn serialize_i16(self, value: i16) -> Result { - number(value) - } - - fn serialize_i32(self, value: i32) -> Result { - number(value) - } - - fn serialize_i64(self, value: i64) -> Result { - number(value) - } - - fn serialize_u8(self, value: u8) -> Result { - number(value) - } - - fn serialize_u16(self, value: u16) -> Result { - number(value) - } - - fn serialize_u32(self, value: u32) -> Result { - number(value) - } - - fn serialize_u64(self, value: u64) -> Result { - number(value) - } - - fn serialize_f32(self, value: f32) -> Result { - self.serialize_f64(f64::from(value)) - } - - fn serialize_f64(self, value: f64) -> Result { - Ok(Number::from_f64(value).map(Value::Number)) - } - - fn serialize_char(self, value: char) -> Result { - Ok(Some(Value::String(value.to_string()))) - } - - fn serialize_str(self, value: &str) -> Result { - Ok(Some(Value::String(value.to_owned()))) - } - - fn serialize_bytes(self, _value: &[u8]) -> Result { - Ok(None) - } - - fn serialize_none(self) -> Result { - Ok(Some(Value::Null)) - } - - fn serialize_some(self, value: &T) -> Result { - value.serialize(self) - } - - fn serialize_unit(self) -> Result { - Ok(Some(Value::Null)) - } - - fn serialize_unit_struct(self, _name: &'static str) -> Result { - Ok(Some(Value::Null)) - } - - fn serialize_unit_variant( - self, - _name: &'static str, - _index: u32, - variant: &'static str, - ) -> Result { - Ok(Some(Value::String(variant.to_owned()))) - } - - fn serialize_newtype_struct( - self, - _name: &'static str, - value: &T, - ) -> Result { - value.serialize(self) - } - - fn serialize_newtype_variant( - self, - _name: &'static str, - _index: u32, - _variant: &'static str, - _value: &T, - ) -> Result { - Ok(None) - } - - fn serialize_seq(self, _len: Option) -> Result { - Ok(Skipped) - } - - fn serialize_tuple(self, _len: usize) -> Result { - Ok(Skipped) - } - - fn serialize_tuple_struct( - self, - _name: &'static str, - _len: usize, - ) -> Result { - Ok(Skipped) - } - - fn serialize_tuple_variant( - self, - _name: &'static str, - _index: u32, - _variant: &'static str, - _len: usize, - ) -> Result { - Ok(Skipped) - } - - fn serialize_map(self, _len: Option) -> Result { - Ok(Skipped) - } - - fn serialize_struct(self, _name: &'static str, _len: usize) -> Result { - Ok(Skipped) - } - - fn serialize_struct_variant( - self, - _name: &'static str, - _index: u32, - _variant: &'static str, - _len: usize, - ) -> Result { - Ok(Skipped) - } -} diff --git a/crates/sqlbuild-analysis/src/type_system/_helpers/spelling.rs b/crates/sqlbuild-analysis/src/type_system/_helpers/spelling.rs new file mode 100644 index 0000000000..7465e89f2f --- /dev/null +++ b/crates/sqlbuild-analysis/src/type_system/_helpers/spelling.rs @@ -0,0 +1,510 @@ +//! Each dialect's spelling of a type tree: the canonical text `same_type` compares. + +use serde_json::Value; + +use crate::type_system::constants::GENERIC_TYPE_DIALECTS; + +/// A dialect whose type spellings SQLBuild knows. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum SpellingDialect { + Generic, + Snowflake, + BigQuery, + DuckDb, + Databricks, + Postgres, + Tsql, + Sqlite, + ClickHouse, + MySql, +} + +impl SpellingDialect { + /// The dialect a name refers to, in any letter case; None for a dialect SQLBuild lacks. + pub(crate) fn named(name: &str) -> Option { + Some(match name.to_ascii_lowercase().as_str() { + "" | "generic" => Self::Generic, + "snowflake" => Self::Snowflake, + "bigquery" => Self::BigQuery, + "duckdb" => Self::DuckDb, + "databricks" => Self::Databricks, + "postgres" | "postgresql" => Self::Postgres, + "tsql" | "mssql" | "sqlserver" => Self::Tsql, + "sqlite" => Self::Sqlite, + "clickhouse" => Self::ClickHouse, + "mysql" => Self::MySql, + other if GENERIC_TYPE_DIALECTS.contains(&other) => Self::Generic, + _ => return None, + }) + } +} + +/// `tree` written as `dialect` spells it. +pub(crate) fn spell(tree: &Value, dialect: SpellingDialect) -> String { + let mut text: String = String::new(); + Speller { + dialect, + text: &mut text, + } + .data_type(tree, false); + text +} + +struct Speller<'a> { + dialect: SpellingDialect, + text: &'a mut String, +} + +impl Speller<'_> { + fn write(&mut self, text: &str) { + self.text.push_str(text); + } + + /// `name`, then `(length)` when there is one. + fn sized(&mut self, name: &str, length: Option) { + self.write(name); + if let Some(length) = length { + self.write(&format!("({length})")); + } + } + + /// `name`, then `(precision)` or `(precision, scale)` when there is a precision. + fn scaled(&mut self, name: &str, precision: Option, scale: Option) { + self.write(name); + if let Some(precision) = precision { + match scale { + Some(scale) => self.write(&format!("({precision}, {scale})")), + None => self.write(&format!("({precision})")), + } + } + } + + /// A ClickHouse scalar, nullable except as a map key. + fn clickhouse(&mut self, name: &str, map_key: bool) { + if map_key { + self.write(name); + } else { + self.write(&format!("Nullable({name})")); + } + } + + fn data_type(&mut self, tree: &Value, map_key: bool) { + let tag: &str = tree.get("data_type").and_then(Value::as_str).unwrap_or(""); + if self.number_type(tree, tag, map_key) + || self.text_type(tree, tag, map_key) + || self.time_type(tree, tag) + { + return; + } + self.other_type(tree, tag); + } + + /// A boolean or numeric type; false for any other tag. + fn number_type(&mut self, tree: &Value, tag: &str, map_key: bool) -> bool { + use SpellingDialect::{ + BigQuery, ClickHouse, Databricks, MySql, Postgres, Snowflake, Sqlite, Tsql, + }; + let dialect: SpellingDialect = self.dialect; + let number = |key: &str| tree.get(key).and_then(Value::as_u64); + let flag = |key: &str| tree.get(key).and_then(Value::as_bool).unwrap_or(false); + let length: Option = number("length"); + let precision: Option = number("precision"); + let scale: Option = number("scale"); + match tag { + "boolean" => match dialect { + Tsql => self.write("BIT"), + ClickHouse => self.write("Bool"), + _ => self.write("BOOLEAN"), + }, + "tiny_int" => match dialect { + Postgres => self.sized("SMALLINT", length), + ClickHouse => self.clickhouse("Int8", map_key), + _ => self.sized("TINYINT", length), + }, + "small_int" => match dialect { + Sqlite => self.write("INTEGER"), + BigQuery => self.write("INT64"), + ClickHouse => self.clickhouse("Int16", map_key), + _ => self.sized("SMALLINT", length), + }, + "int" => match dialect { + BigQuery => self.write("INT64"), + ClickHouse => self.clickhouse("Int32", map_key), + Tsql | Sqlite => self.sized("INTEGER", length), + _ => self.sized("INT", length), + }, + "big_int" => match dialect { + BigQuery => self.write("INT64"), + ClickHouse => self.clickhouse("Int64", map_key), + _ => self.sized("BIGINT", length), + }, + "int128" => match dialect { + ClickHouse => self.write("Int128"), + _ => self.write("INT128"), + }, + "uint8" | "uint16" | "uint32" | "uint64" | "uint128" => { + let (canonical, clickhouse) = match tag { + "uint8" => ("UTINYINT", "UInt8"), + "uint16" => ("USMALLINT", "UInt16"), + "uint32" => ("UINTEGER", "UInt32"), + "uint64" => ("UBIGINT", "UInt64"), + _ => ("UINT128", "UInt128"), + }; + self.write(if dialect == ClickHouse { + clickhouse + } else { + canonical + }); + } + "float" if dialect == ClickHouse => { + self.clickhouse("Float32", map_key); + self.scaled("", precision, scale); + } + "double" if dialect == ClickHouse => { + self.clickhouse("Float64", map_key); + self.scaled("", precision, scale); + } + "float" => { + let name: &str = if flag("real_spelling") + && !matches!(dialect, Databricks | Snowflake | BigQuery | MySql) + { + "REAL" + } else { + match dialect { + Postgres => "REAL", + BigQuery => "FLOAT64", + _ => "FLOAT", + } + }; + if dialect == Databricks { + self.write(name); + } else { + self.scaled(name, precision, scale); + } + } + "double" => { + let name: &str = match dialect { + Tsql => "FLOAT", + BigQuery => "FLOAT64", + Sqlite => "REAL", + Postgres => "DOUBLE PRECISION", + _ => "DOUBLE", + }; + self.scaled(name, precision, scale); + } + "decimal" => match dialect { + ClickHouse => self.scaled("Decimal", precision, scale), + BigQuery => self.scaled("NUMERIC", precision, scale), + _ => self.scaled("DECIMAL", precision, scale), + }, + _ => return false, + } + true + } + + /// A text, binary, JSON or UUID type; false for any other tag. + fn text_type(&mut self, tree: &Value, tag: &str, map_key: bool) -> bool { + use SpellingDialect::{ + BigQuery, ClickHouse, Databricks, DuckDb, MySql, Postgres, Snowflake, Sqlite, Tsql, + }; + let dialect: SpellingDialect = self.dialect; + let number = |key: &str| tree.get(key).and_then(Value::as_u64); + let length: Option = number("length"); + match tag { + "text_with_length" if dialect == ClickHouse => self.write("Nullable(String)"), + "char" | "var_char" | "string" | "text" if dialect == ClickHouse => { + self.write("String"); + } + "char" => match dialect { + DuckDb | Sqlite => self.write("TEXT"), + Databricks if length.is_none() => self.write("STRING"), + _ => self.sized("CHAR", length), + }, + "var_char" => match dialect { + DuckDb | Sqlite => self.sized("TEXT", length), + MySql if length.is_none() => self.write("TEXT"), + Databricks if length.is_none() => self.write("STRING"), + _ => self.sized("VARCHAR", length), + }, + "text" => match dialect { + Tsql => self.write("VARCHAR(MAX)"), + BigQuery | Databricks => self.write("STRING"), + Snowflake => self.write("VARCHAR"), + _ => self.write("TEXT"), + }, + "text_with_length" => match dialect { + Databricks | Snowflake | Tsql => self.sized("VARCHAR", length), + BigQuery => self.sized("STRING", length), + _ => self.sized("TEXT", length), + }, + "string" => match dialect { + Postgres | MySql if length.is_none() => self.write("TEXT"), + Postgres => self.sized("VARCHAR", length), + Tsql if length.is_none() => self.write("VARCHAR(MAX)"), + Tsql => self.sized("VARCHAR", length), + DuckDb => self.sized("TEXT", length), + _ => self.sized("STRING", length), + }, + "binary" | "var_binary" if dialect == BigQuery => self.sized("BYTES", length), + "binary" => match dialect { + Postgres => self.sized("BYTEA", length), + DuckDb | Sqlite => self.sized("BLOB", length), + _ => self.sized("BINARY", length), + }, + "var_binary" => match dialect { + Postgres => self.sized("BYTEA", length), + DuckDb | Sqlite => self.sized("BLOB", length), + Databricks => self.sized("BINARY", length), + ClickHouse => self.clickhouse("String", map_key), + _ => self.sized("VARBINARY", length), + }, + "blob" => match dialect { + Postgres => self.write("BYTEA"), + Tsql | DuckDb => self.write("VARBINARY"), + BigQuery => self.write("BYTES"), + Databricks => self.write("BINARY"), + ClickHouse => self.write("Nullable(String)"), + _ => self.write("BLOB"), + }, + "bit" => match dialect { + Databricks | Snowflake | BigQuery | ClickHouse => self.write("BOOLEAN"), + _ => self.sized("BIT", length), + }, + "var_bit" => self.sized("VARBIT", length), + "json_b" if dialect == Postgres => self.write("JSONB"), + "json" | "json_b" => match dialect { + Tsql => self.write("NVARCHAR(MAX)"), + Snowflake => self.write("VARIANT"), + _ => self.write("JSON"), + }, + "uuid" => match dialect { + Tsql => self.write("UNIQUEIDENTIFIER"), + MySql => self.write("CHAR(36)"), + BigQuery | Databricks => self.write("STRING"), + _ => self.write("UUID"), + }, + _ => return false, + } + true + } + + /// A date, time, timestamp or interval type; false for any other tag. + fn time_type(&mut self, tree: &Value, tag: &str) -> bool { + use SpellingDialect::{BigQuery, ClickHouse, Databricks, DuckDb, MySql, Postgres, Tsql}; + let dialect: SpellingDialect = self.dialect; + let number = |key: &str| tree.get(key).and_then(Value::as_u64); + let flag = |key: &str| tree.get(key).and_then(Value::as_bool).unwrap_or(false); + let precision: Option = number("precision"); + match tag { + "date" => self.write("DATE"), + "time" if flag("timezone") => match dialect { + DuckDb => self.write("TIMETZ"), + Postgres => self.sized("TIMETZ", precision), + _ => { + self.sized("TIME", precision); + self.write(" WITH TIME ZONE"); + } + }, + "time" => match dialect { + Databricks => self.write("TIMESTAMP"), + _ => self.sized("TIME", precision), + }, + "timestamp" => { + let timezone: bool = flag("timezone"); + match dialect { + ClickHouse => self.sized("DateTime", precision), + Tsql if timezone => self.sized("DATETIMEOFFSET", precision), + Tsql => self.sized("DATETIME2", precision), + BigQuery if timezone => self.write("TIMESTAMP"), + BigQuery => self.write("DATETIME"), + DuckDb if timezone => self.write("TIMESTAMPTZ"), + MySql => self.sized("TIMESTAMP", precision), + _ if timezone => self.sized("TIMESTAMPTZ", precision), + _ => self.sized("TIMESTAMP", precision), + } + } + "interval" => { + self.write("INTERVAL"); + for (key, prefix) in [("unit", " "), ("to", " TO ")] { + if let Some(unit) = tree.get(key).and_then(Value::as_str) { + self.write(prefix); + self.write(&unit.to_ascii_uppercase()); + } + } + } + _ => return false, + } + true + } + + /// A nested, custom or other type. + fn other_type(&mut self, tree: &Value, tag: &str) { + use SpellingDialect::{ClickHouse, DuckDb, Generic, MySql, Postgres, Snowflake, Tsql}; + let dialect: SpellingDialect = self.dialect; + let number = |key: &str| tree.get(key).and_then(Value::as_u64); + match tag { + "array" => { + let element: &Value = tree.get("element_type").unwrap_or(&Value::Null); + match dialect { + Postgres | DuckDb => { + self.data_type(element, false); + match number("dimension") { + Some(dimension) => self.write(&format!("[{dimension}]")), + None => self.write("[]"), + } + } + Snowflake | ClickHouse => { + self.write(if dialect == ClickHouse { + "Array(" + } else { + "ARRAY(" + }); + self.data_type(element, false); + self.write(")"); + } + Tsql => self.write("NVARCHAR(MAX)"), + MySql => self.write("JSON"), + _ => { + self.write("ARRAY<"); + self.data_type(element, false); + self.write(">"); + } + } + } + "list" => { + self.data_type(tree.get("element_type").unwrap_or(&Value::Null), false); + self.write(" LIST"); + } + "map" => { + let key: &Value = tree.get("key_type").unwrap_or(&Value::Null); + let value: &Value = tree.get("value_type").unwrap_or(&Value::Null); + let (open, close) = match dialect { + Snowflake | DuckDb => ("MAP(", ")"), + ClickHouse => ("Map(", ")"), + _ => ("MAP<", ">"), + }; + self.write(open); + self.data_type(key, dialect == ClickHouse); + self.write(", "); + self.data_type(value, false); + self.write(close); + } + "struct" => self.struct_type(tree), + "object" => self.field_list("OBJECT(", tree), + "union" => self.field_list("UNION(", tree), + "vector" => { + self.write("VECTOR"); + let element: Option<&Value> = tree.get("element_type").filter(|v| !v.is_null()); + let dimension: Option = number("dimension"); + if element.is_some() || dimension.is_some() { + self.write("("); + if let Some(element) = element { + self.data_type(element, false); + if dimension.is_some() { + self.write(", "); + } + } + if let Some(dimension) = dimension { + self.write(&dimension.to_string()); + } + self.write(")"); + } + } + "enum" => { + let values: Vec = tree + .get("values") + .and_then(Value::as_array) + .map_or(&[][..], Vec::as_slice) + .iter() + .map(|value| format!("'{}'", value.as_str().unwrap_or("").replace('\'', "''"))) + .collect(); + self.write(&format!("ENUM({})", values.join(", "))); + } + "custom" => { + let name: &str = tree.get("name").and_then(Value::as_str).unwrap_or(""); + self.write(&custom_name(name, dialect)); + } + _ if matches!(dialect, Generic | MySql) => self.write(&tag.to_ascii_uppercase()), + _ => self.write(&tag.to_ascii_uppercase().replace('_', "")), + } + } + + /// An `OBJECT` or `UNION` of named fields, opened by `open`. + fn field_list(&mut self, open: &str, tree: &Value) { + self.write(open); + let fields: &[Value] = tree + .get("fields") + .and_then(Value::as_array) + .map_or(&[], Vec::as_slice); + for (index, field) in fields.iter().enumerate() { + if index > 0 { + self.write(", "); + } + self.write(field.get(0).and_then(Value::as_str).unwrap_or("")); + self.write(" "); + self.data_type(field.get(1).unwrap_or(&Value::Null), false); + } + self.write(")"); + } + + fn struct_type(&mut self, tree: &Value) { + use SpellingDialect::{BigQuery, ClickHouse, Databricks, DuckDb, Snowflake}; + let dialect: SpellingDialect = self.dialect; + let nested: bool = tree.get("nested").and_then(Value::as_bool).unwrap_or(false); + let (open, close, separator): (&str, &str, &str) = match dialect { + Snowflake => ("OBJECT(", ")", " "), + DuckDb => ("STRUCT(", ")", " "), + ClickHouse => ("Tuple(", ")", " "), + Databricks | BigQuery => ( + "STRUCT<", + ">", + if dialect == Databricks { ": " } else { " " }, + ), + _ if nested => ("STRUCT(", ")", " "), + _ => ("STRUCT<", ">", " "), + }; + self.write(open); + let fields: &[Value] = tree + .get("fields") + .and_then(Value::as_array) + .map_or(&[], Vec::as_slice); + for (index, field) in fields.iter().enumerate() { + if index > 0 { + self.write(", "); + } + let name: &str = field.get("name").and_then(Value::as_str).unwrap_or(""); + if !name.is_empty() { + self.write(name); + self.write(separator); + } + self.data_type(field.get("data_type").unwrap_or(&Value::Null), false); + } + self.write(close); + } +} + +/// A type name no dialect gives its own shape, as `dialect` spells it. +fn custom_name(name: &str, dialect: SpellingDialect) -> String { + let (base, arguments): (&str, &str) = name.find('(').map_or((name, ""), |at| name.split_at(at)); + let base_upper: String = base.trim().to_ascii_uppercase(); + match (dialect, base_upper.as_str()) { + (SpellingDialect::Snowflake, "TIMESTAMP_NTZ" | "TIMESTAMP_LTZ" | "TIMESTAMP_TZ") => { + format!("{}{arguments}", base_upper.replace('_', "")) + } + (SpellingDialect::Tsql, "VARIANT") => "SQL_VARIANT".to_owned(), + (SpellingDialect::MySql, "GEOGRAPHY") => { + let srid: &str = arguments.trim_matches(['(', ')']); + format!( + "GEOMETRY SRID {}", + if srid.is_empty() { "4326" } else { srid } + ) + } + ( + SpellingDialect::BigQuery | SpellingDialect::ClickHouse | SpellingDialect::Snowflake, + _, + ) => name.to_owned(), + (_, "INT64") if arguments.is_empty() => "BIGINT".to_owned(), + (_, "FLOAT64") if arguments.is_empty() => "DOUBLE".to_owned(), + _ => name.to_owned(), + } +} diff --git a/crates/sqlbuild-analysis/src/type_system/_helpers/type_reader.rs b/crates/sqlbuild-analysis/src/type_system/_helpers/type_reader.rs new file mode 100644 index 0000000000..b04611dc70 --- /dev/null +++ b/crates/sqlbuild-analysis/src/type_system/_helpers/type_reader.rs @@ -0,0 +1,440 @@ +//! SQLBuild's own type reader, which builds the tree SQLQuack's type parsers build. + +use serde_json::{Map, Value, json}; + +use crate::type_system::_helpers::spelling::SpellingDialect; + +/// The type tree of `type_sql`, or why the reader rejects it. +pub(crate) fn read_type(type_sql: &str, dialect: SpellingDialect) -> Result { + let tokens: Vec = tokens(type_sql, dialect)?; + let mut reader = Reader { + tokens: &tokens, + position: 0, + dialect, + }; + let tree: Value = reader.data_type()?; + if reader.peek() == Some(&Token::Punct(';')) { + reader.position += 1; + } + match reader.peek() { + None => Ok(tree), + Some(token) => Err(format!("unexpected {} after the type", token.text())), + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum Token { + Word(String), + Quoted(String), + Number(String), + Punct(char), +} + +impl Token { + fn text(&self) -> String { + match self { + Self::Word(text) | Self::Quoted(text) | Self::Number(text) => format!("'{text}'"), + Self::Punct(character) => format!("'{character}'"), + } + } +} + +fn tokens(type_sql: &str, dialect: SpellingDialect) -> Result, String> { + let characters: Vec = type_sql.chars().collect(); + let mut tokens: Vec = Vec::new(); + let mut index: usize = 0; + while let Some(&character) = characters.get(index) { + if character.is_whitespace() { + index += 1; + } else if character.is_alphabetic() || character == '_' { + let start: usize = index; + while characters + .get(index) + .is_some_and(|next| next.is_alphanumeric() || *next == '_' || *next == '$') + { + index += 1; + } + tokens.push(Token::Word(characters[start..index].iter().collect())); + } else if character.is_ascii_digit() { + let start: usize = index; + while characters.get(index).is_some_and(char::is_ascii_digit) { + index += 1; + } + if characters + .get(index) + .is_some_and(|next| next.is_alphabetic() || *next == '_') + { + return Err(format!("invalid identifier at offset {start}")); + } + tokens.push(Token::Number(characters[start..index].iter().collect())); + } else if let Some(close) = quote_close(character, dialect) { + let start: usize = index + 1; + index = start; + while characters.get(index).is_some_and(|next| *next != close) { + index += 1; + } + if index >= characters.len() { + return Err("unterminated quoted name".to_owned()); + } + tokens.push(Token::Quoted(characters[start..index].iter().collect())); + index += 1; + } else if "()<>,[]:;".contains(character) { + tokens.push(Token::Punct(character)); + index += 1; + } else { + return Err(format!("unexpected character '{character}'")); + } + } + Ok(tokens) +} + +/// The closing quote of a quoted name `open` starts in `dialect`. +fn quote_close(open: char, dialect: SpellingDialect) -> Option { + let backtick: bool = matches!( + dialect, + SpellingDialect::Databricks | SpellingDialect::BigQuery | SpellingDialect::ClickHouse + ); + match open { + '`' if backtick => Some('`'), + '"' if !matches!( + dialect, + SpellingDialect::Databricks | SpellingDialect::BigQuery + ) => + { + Some('"') + } + '[' if dialect == SpellingDialect::Tsql => Some(']'), + _ => None, + } +} + +struct Reader<'a> { + tokens: &'a [Token], + position: usize, + dialect: SpellingDialect, +} + +impl Reader<'_> { + fn peek(&self) -> Option<&Token> { + self.tokens.get(self.position) + } + + fn next(&mut self) -> Option { + let token: Option = self.peek().cloned(); + self.position += usize::from(token.is_some()); + token + } + + fn eat_punct(&mut self, character: char) -> bool { + let found: bool = self.peek() == Some(&Token::Punct(character)); + self.position += usize::from(found); + found + } + + fn expect_punct(&mut self, character: char) -> Result<(), String> { + if self.eat_punct(character) { + return Ok(()); + } + Err(match self.peek() { + Some(token) => format!("expected '{character}', found {}", token.text()), + None => format!("expected '{character}' at the end of the type"), + }) + } + + fn eat_word(&mut self, word: &str) -> bool { + let found: bool = + matches!(self.peek(), Some(Token::Word(text)) if text.eq_ignore_ascii_case(word)); + self.position += usize::from(found); + found + } + + fn data_type(&mut self) -> Result { + let mut tree: Value = self.base_type()?; + while self.eat_punct('[') { + let dimension: Option = match self.peek() { + Some(Token::Number(_)) => Some(self.number()?), + _ => None, + }; + self.expect_punct(']')?; + let mut array: Map = Map::new(); + array.insert("data_type".to_owned(), json!("array")); + array.insert("element_type".to_owned(), tree); + if let Some(dimension) = dimension { + array.insert("dimension".to_owned(), json!(dimension)); + } + tree = Value::Object(array); + } + Ok(tree) + } + + fn base_type(&mut self) -> Result { + match self.next() { + Some(Token::Quoted(name)) => self.custom(name), + Some(Token::Word(word)) => self.named_type(&word), + other => Err(unexpected("a type name", other.as_ref())), + } + } + + fn named_type(&mut self, word: &str) -> Result { + let upper: String = word.to_ascii_uppercase(); + match upper.as_str() { + "ARRAY" => { + let close: char = self.open_nested()?; + let element: Value = self.data_type()?; + self.expect_punct(close)?; + Ok(json!({"data_type": "array", "element_type": element})) + } + "MAP" => { + let close: char = self.open_nested()?; + let key: Value = self.data_type()?; + self.expect_punct(',')?; + let value: Value = self.data_type()?; + self.expect_punct(close)?; + Ok(json!({"data_type": "map", "key_type": key, "value_type": value})) + } + "STRUCT" => { + let close: char = self.open_nested()?; + let mut fields: Vec = Vec::new(); + loop { + let name: String = match self.next() { + Some(Token::Word(name) | Token::Quoted(name)) => name, + _ => return Err("expected a struct field name".to_owned()), + }; + self.eat_punct(':'); + let data_type: Value = self.data_type()?; + fields.push(json!({"name": name, "data_type": data_type})); + if !self.eat_punct(',') { + break; + } + } + self.expect_punct(close)?; + Ok(json!({"data_type": "struct", "fields": fields, "nested": close == ')'})) + } + "DOUBLE" => { + self.eat_word("PRECISION"); + let (precision, scale) = self.precision_scale()?; + Ok(json!({"data_type": "double", "precision": precision, "scale": scale})) + } + "CHARACTER" | "CHAR" if self.eat_word("VARYING") => { + let length: Option = self.text_length(Self::length)?; + Ok(json!({"data_type": "var_char", "length": length})) + } + "TIMESTAMP" => self.time_type("timestamp", self.dialect == SpellingDialect::BigQuery), + "TIME" => self.time_type("time", false), + _ => self.scalar_type(word, &upper), + } + } + + /// A time or timestamp; `zoned` when the bare type has a time zone, as in BigQuery. + fn time_type(&mut self, tag: &str, zoned: bool) -> Result { + let precision: Option = self.length()?; + let timezone: bool = self.time_zone()?.unwrap_or(zoned); + Ok(json!({"data_type": tag, "precision": precision, "timezone": timezone})) + } + + /// The opening bracket of a nested type's parameters, returning its closing bracket. + fn open_nested(&mut self) -> Result { + if self.eat_punct('<') { + return Ok('>'); + } + self.expect_punct('(')?; + Ok(')') + } + + /// `WITH TIME ZONE` or `WITHOUT TIME ZONE` after a time type, None when neither follows. + fn time_zone(&mut self) -> Result, String> { + let timezone: bool = if self.eat_word("WITH") { + true + } else if self.eat_word("WITHOUT") { + false + } else { + return Ok(None); + }; + if self.eat_word("TIME") && self.eat_word("ZONE") { + return Ok(Some(timezone)); + } + Err("expected TIME ZONE".to_owned()) + } + + fn scalar_type(&mut self, word: &str, upper: &str) -> Result { + let tagged = |tag: &str, length: Option| json!({"data_type": tag, "length": length}); + Ok(match upper { + "INT" | "INTEGER" => tagged("int", self.sized_length()?), + "BIGINT" => tagged("big_int", self.sized_length()?), + "SMALLINT" => tagged("small_int", self.sized_length()?), + "TINYINT" => tagged("tiny_int", self.sized_length()?), + "VARCHAR" => { + if self.max_length() { + return Ok(json!({"data_type": "custom", "name": upper})); + } + tagged("var_char", self.text_length(Self::sized_length)?) + } + "CHAR" | "CHARACTER" => tagged("char", self.text_length(Self::sized_length)?), + "STRING" => tagged("string", self.text_length(Self::length)?), + "BINARY" => tagged("binary", self.length()?), + "GEOGRAPHY" if self.single_geography() => { + if !self.max_length() { + self.length()?; + } + json!({"data_type": "custom", "name": upper}) + } + "VARBINARY" | "NVARCHAR" | "GEOGRAPHY" if self.max_length() => { + json!({"data_type": "custom", "name": format!("{upper}(MAX)")}) + } + "VARBINARY" => tagged("var_binary", self.length()?), + "TEXT" | "NTEXT" => match self.length()? { + Some(length) => json!({"data_type": "text_with_length", "length": length}), + None => json!({"data_type": "text"}), + }, + "DECIMAL" | "NUMERIC" => { + let (precision, scale) = self.precision_scale()?; + json!({"data_type": "decimal", "precision": precision, "scale": scale}) + } + "FLOAT" => self.float_type(false)?, + "REAL" => self.float_type(true)?, + "TIMESTAMPTZ" | "TIMESTAMP_TZ" => { + let precision: Option = self.length()?; + json!({"data_type": "timestamp", "precision": precision, "timezone": true}) + } + "DATETIME" if self.max_length() => json!({"data_type": "custom", "name": upper}), + "TIMESTAMPNTZ" | "TIMESTAMP_NTZ" | "TIMESTAMPLTZ" | "TIMESTAMP_LTZ" | "DATETIME" => { + let _precision: Option = self.length()?; + json!({"data_type": "custom", "name": upper.replace('_', "")}) + } + "BOOL" | "BOOLEAN" => json!({"data_type": "boolean"}), + "DATE" => json!({"data_type": "date"}), + "INTERVAL" => json!({"data_type": "interval"}), + "JSON" => json!({"data_type": "json"}), + "JSONB" => json!({"data_type": "json_b"}), + "UUID" => json!({"data_type": "uuid"}), + "BLOB" => json!({"data_type": "blob"}), + "HUGEINT" => json!({"data_type": "int128"}), + "LONG" => json!({"data_type": "custom", "name": upper}), + "NVARCHAR" | "GEOGRAPHY" => { + let length: Option = self.length()?; + let name: String = match length { + Some(length) => format!("{upper}({length})"), + None => upper.to_owned(), + }; + json!({"data_type": "custom", "name": name}) + } + _ => return self.custom(word.to_owned()), + }) + } + + /// Whether the dialect has one GEOGRAPHY, which takes no parameters: BigQuery and Snowflake. + fn single_geography(&self) -> bool { + matches!( + self.dialect, + SpellingDialect::BigQuery | SpellingDialect::Snowflake + ) + } + + /// A FLOAT or REAL; Postgres's REAL is a 4-byte float, whatever precision follows it. + fn float_type(&mut self, real: bool) -> Result { + let (mut precision, mut scale) = self.precision_scale()?; + if real && self.dialect == SpellingDialect::Postgres { + (precision, scale) = (None, None); + } + Ok(json!({ + "data_type": "float", + "precision": precision, + "scale": scale, + "real_spelling": real, + })) + } + + /// Any other type name with any number of integer parameters, kept as written. + fn custom(&mut self, name: String) -> Result { + if !self.eat_punct('(') { + return Ok(json!({"data_type": "custom", "name": name})); + } + if self.eat_punct(')') { + return Ok(json!({"data_type": "custom", "name": format!("{name}()")})); + } + let mut parameters: Vec = Vec::new(); + loop { + match self.next() { + Some(Token::Number(number)) => parameters.push(number), + Some(Token::Word(word)) => parameters.push(word), + _ => return Err(format!("expected a parameter of {name}")), + } + if !self.eat_punct(',') { + break; + } + } + self.expect_punct(')')?; + Ok(json!({"data_type": "custom", "name": format!("{name}({})", parameters.join(", "))})) + } + + /// The optional length of an integer or character type; empty parentheses state none. + fn sized_length(&mut self) -> Result, String> { + if self.peek() == Some(&Token::Punct('(')) + && self.tokens.get(self.position + 1) == Some(&Token::Punct(')')) + { + self.position += 2; + return Ok(None); + } + self.length() + } + + /// A character type's length, which ClickHouse reads and drops: its `String` has none. + fn text_length( + &mut self, + read: fn(&mut Self) -> Result, String>, + ) -> Result, String> { + let length: Option = read(self)?; + Ok(length.filter(|_| self.dialect != SpellingDialect::ClickHouse)) + } + + /// A parenthesized `MAX`, which only the variable-length types take. + fn max_length(&mut self) -> bool { + let found: bool = self.peek() == Some(&Token::Punct('(')) + && matches!(self.tokens.get(self.position + 1), Some(Token::Word(word)) if word.eq_ignore_ascii_case("MAX")) + && self.tokens.get(self.position + 2) == Some(&Token::Punct(')')); + self.position += if found { 3 } else { 0 }; + found + } + + /// One optional parenthesized integer. + fn length(&mut self) -> Result, String> { + if !self.eat_punct('(') { + return Ok(None); + } + let length: u32 = self.number()?; + self.expect_punct(')')?; + Ok(Some(length)) + } + + /// An optional parenthesized precision and scale. + fn precision_scale(&mut self) -> Result<(Option, Option), String> { + if !self.eat_punct('(') { + return Ok((None, None)); + } + let precision: u32 = self.number()?; + let scale: Option = if self.eat_punct(',') { + Some(self.number()?) + } else { + None + }; + self.expect_punct(')')?; + Ok((Some(precision), scale)) + } + + fn number(&mut self) -> Result { + match self.next() { + Some(Token::Number(number)) => number + .parse::() + .map_err(|_| format!("invalid number: {number}")), + other => Err(unexpected("a number", other.as_ref())), + } + } +} + +/// Why `found`, or the end of the type, is not the `expected` token. +fn unexpected(expected: &str, found: Option<&Token>) -> String { + match found { + Some(token) => format!("expected {expected}, found {}", token.text()), + None => format!("expected {expected}"), + } +} diff --git a/crates/sqlbuild-analysis/src/type_system/constants.rs b/crates/sqlbuild-analysis/src/type_system/constants.rs index e5d79cc893..b432058a51 100644 --- a/crates/sqlbuild-analysis/src/type_system/constants.rs +++ b/crates/sqlbuild-analysis/src/type_system/constants.rs @@ -9,7 +9,41 @@ pub(crate) const FLOAT_TYPE_NAMES: &[&str] = &["DOUBLE", "FLOAT", "FLOAT64", "RE pub(crate) const INTEGER_TYPE_NAMES: &[&str] = &[ "BIGINT", "INT", "INT64", "INTEGER", "LONG", "SMALLINT", "TINYINT", ]; -pub(crate) const POLYGLOT_CUSTOM_TYPE_NAME: &str = "CUSTOM"; +/// The other dialects SQL analysis accepts, which have no type rules of their own. +pub(crate) const GENERIC_TYPE_DIALECTS: &[&str] = &[ + "athena", + "cockroachdb", + "datafusion", + "doris", + "dremio", + "drill", + "druid", + "dune", + "exasol", + "fabric", + "hive", + "materialize", + "oracle", + "presto", + "redshift", + "risingwave", + "singlestore", + "solr", + "spark", + "starrocks", + "tableau", + "teradata", + "tidb", + "trino", +]; +/// The deepest type nesting SQLBuild reads: SQLQuack's syntax tree depth. +pub(crate) const MAX_TYPE_NESTING: usize = 512; +/// The type nesting read on the caller's stack; deeper types get their own. +pub(crate) const SHALLOW_TYPE_NESTING: usize = 32; +/// The stack a more deeply nested type is read and normalized on. +pub(crate) const DEEP_TYPE_STACK_BYTES: usize = 32 * 1024 * 1024; +/// The tag of a type name no dialect gives its own shape. +pub(crate) const CUSTOM_TYPE_TAG: &str = "CUSTOM"; pub(crate) const STRING_TYPE_NAMES: &[&str] = &["CHAR", "CHARACTER", "STRING", "TEXT", "VARCHAR"]; pub(crate) const TIMESTAMP_TYPE_NAMES: &[&str] = &["TIMESTAMP", "TIMESTAMPLTZ", "TIMESTAMPNTZ", "TIMESTAMPTZ"]; @@ -31,16 +65,14 @@ pub(crate) const SIZED_TEXT_TYPE_NAMES: &[&str] = &["STRING", "TEXT"]; pub(crate) const VARCHAR_TYPE_NAME: &str = "VARCHAR"; /// BigQuery's one spelling of every variable-length text type. pub(crate) const BIGQUERY_STRING_TYPE_NAME: &str = "STRING"; -/// Polyglot `data_type` tags whose Python spelling drops the word separator. -pub(crate) const POLYGLOT_TYPE_NAME_ALIASES: &[(&str, &str)] = &[ +/// Type tags whose type name drops the word separator. +pub(crate) const TYPE_TAG_ALIASES: &[(&str, &str)] = &[ ("BIG_INT", "BIGINT"), ("SMALL_INT", "SMALLINT"), ("TEXT_WITH_LENGTH", "TEXT"), ("TINY_INT", "TINYINT"), ("VAR_CHAR", "VARCHAR"), ]; -/// The function call depth limit SQLBuild passes to every Polyglot parse. -pub(crate) const MAX_FUNCTION_CALL_DEPTH: usize = 512; /// Python's `str(0)`, the only integer text Python treats as false. pub(crate) const ZERO_TEXT: &str = "0"; /// Snowflake time and timestamp types, word separators removed, whose precision metadata omits. diff --git a/crates/sqlbuild-analysis/src/type_system/errors.rs b/crates/sqlbuild-analysis/src/type_system/errors.rs index 05145a1c60..d8aae3d875 100644 --- a/crates/sqlbuild-analysis/src/type_system/errors.rs +++ b/crates/sqlbuild-analysis/src/type_system/errors.rs @@ -1,12 +1,10 @@ //! Why type normalization fails. -/// Why Python's `normalize_type` raises instead of returning a type. +/// Why `normalize_type` raises instead of returning a type. #[derive(Debug, Clone, PartialEq, Eq)] pub enum TypeNormalizationError { - /// Polyglot does not know the dialect name: the wheel's `ValueError`. + /// SQLBuild has no type rules for the dialect name: Python's `ValueError`. UnknownDialect(String), - /// Polyglot cannot write the parsed type back as SQL: the wheel's `PolyglotError`. - Generation(String), } impl TypeNormalizationError { @@ -15,7 +13,6 @@ impl TypeNormalizationError { pub fn message(&self) -> String { match self { Self::UnknownDialect(name) => format!("Unknown dialect: {name}"), - Self::Generation(message) => message.clone(), } } } diff --git a/crates/sqlbuild-analysis/src/type_system/main/mod.rs b/crates/sqlbuild-analysis/src/type_system/main/mod.rs index 7434eded53..23df8a93ee 100644 --- a/crates/sqlbuild-analysis/src/type_system/main/mod.rs +++ b/crates/sqlbuild-analysis/src/type_system/main/mod.rs @@ -1,3 +1,3 @@ pub mod normalize_type; -pub mod polyglot_type_args; pub mod same_type; +pub mod type_args; diff --git a/crates/sqlbuild-analysis/src/type_system/main/normalize_type.rs b/crates/sqlbuild-analysis/src/type_system/main/normalize_type.rs index ef9005288c..cf32a9cd8f 100644 --- a/crates/sqlbuild-analysis/src/type_system/main/normalize_type.rs +++ b/crates/sqlbuild-analysis/src/type_system/main/normalize_type.rs @@ -1,36 +1,34 @@ -//! Python's `normalize_type`: Polyglot's parsed type first, then the text fallback. - -use polyglot_sql::{Dialect, DialectType}; +//! Python's `normalize_type`: the parsed type first, then the text fallback. use crate::type_system::_helpers::dialects::coerce_type_dialect; use crate::type_system::_helpers::fallback::normalize_with_fallback; -use crate::type_system::_helpers::parsed::{normalize_parsed, parse_type}; +use crate::type_system::_helpers::parsed::{nesting_bound, normalize_parsed, parse_type}; +use crate::type_system::_helpers::spelling::SpellingDialect; +use crate::type_system::constants::{DEEP_TYPE_STACK_BYTES, SHALLOW_TYPE_NESTING}; use crate::type_system::errors::TypeNormalizationError; use crate::type_system::models::{TypeDialect, TypeNormalization}; -/// Normalize one type as Python does for the dialect name it hands Polyglot. +/// Normalize one type in the named dialect. pub fn normalize_type( type_sql: &str, dialect: &str, ) -> Result { - let name: &str = if dialect.is_empty() { - "generic" - } else { - dialect - }; - let polyglot: Dialect = name - .parse::() - .map(Dialect::get) - .map_err(|_| TypeNormalizationError::UnknownDialect(name.to_owned()))?; + let spelling: SpellingDialect = SpellingDialect::named(dialect) + .ok_or_else(|| TypeNormalizationError::UnknownDialect(dialect.to_owned()))?; let type_dialect: Option = coerce_type_dialect(dialect); - match parse_type(type_sql, &polyglot) { - Ok(parsed) => Ok(TypeNormalization { - normalized: normalize_parsed(parsed, &polyglot, type_dialect)?, + let normalize = || match parse_type(type_sql, spelling) { + Ok(tree) => TypeNormalization { + normalized: normalize_parsed(&tree, spelling, type_dialect), parse_error: None, - }), - Err(error) => Ok(TypeNormalization { + }, + Err(error) => TypeNormalization { normalized: normalize_with_fallback(type_sql, type_dialect), parse_error: Some(error), - }), - } + }, + }; + Ok(if nesting_bound(type_sql) > SHALLOW_TYPE_NESTING { + stacker::grow(DEEP_TYPE_STACK_BYTES, normalize) + } else { + normalize() + }) } diff --git a/crates/sqlbuild-analysis/src/type_system/main/polyglot_type_args.rs b/crates/sqlbuild-analysis/src/type_system/main/polyglot_type_args.rs deleted file mode 100644 index 972ac1f6c0..0000000000 --- a/crates/sqlbuild-analysis/src/type_system/main/polyglot_type_args.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! The scalar arguments of a type as Polyglot parses it. - -use polyglot_sql::{DataType, Dialect, DialectType}; -use serde_json::{Map, Value}; - -use crate::type_system::_helpers::parsed::parse_type; -use crate::type_system::_helpers::shallow_args::shallow_args; - -/// `parse_data_type(type_sql, dialect=...).args` scalars, or the parse error text. -pub fn polyglot_type_args(type_sql: &str, dialect: &str) -> Result, String> { - let polyglot: Dialect = dialect - .parse::() - .map(Dialect::get) - .map_err(|_| format!("unknown dialect '{dialect}'"))?; - let parsed: DataType = parse_type(type_sql, &polyglot)?; - Ok(shallow_args(&parsed)) -} diff --git a/crates/sqlbuild-analysis/src/type_system/main/type_args.rs b/crates/sqlbuild-analysis/src/type_system/main/type_args.rs new file mode 100644 index 0000000000..6d92cf07cc --- /dev/null +++ b/crates/sqlbuild-analysis/src/type_system/main/type_args.rs @@ -0,0 +1,13 @@ +//! The scalar arguments of a parsed type. + +use serde_json::{Map, Value}; + +use crate::type_system::_helpers::parsed::{parse_type, type_args as tree_args}; +use crate::type_system::_helpers::spelling::SpellingDialect; + +/// The tag, name, length, precision and scale of `type_sql` in `dialect`, or the parse error. +pub fn type_args(type_sql: &str, dialect: &str) -> Result, String> { + let spelling: SpellingDialect = + SpellingDialect::named(dialect).ok_or_else(|| format!("unknown dialect '{dialect}'"))?; + parse_type(type_sql, spelling).map(|tree| tree_args(&tree)) +} diff --git a/crates/sqlbuild-analysis/src/type_system/models.rs b/crates/sqlbuild-analysis/src/type_system/models.rs index 9497d5bf9f..28323983d0 100644 --- a/crates/sqlbuild-analysis/src/type_system/models.rs +++ b/crates/sqlbuild-analysis/src/type_system/models.rs @@ -101,7 +101,7 @@ pub struct NormalizedType { pub length: Option, } -/// A normalized type and the Polyglot parse error Python logs before its text fallback. +/// A normalized type and the parse error logged before its text fallback. #[derive(Debug, Clone, PartialEq, Eq)] pub struct TypeNormalization { pub normalized: NormalizedType, diff --git a/crates/sqlbuild-analysis/src/type_system/tests/alias_groups.json b/crates/sqlbuild-analysis/src/type_system/tests/alias_groups.json new file mode 100644 index 0000000000..b0308fc473 --- /dev/null +++ b/crates/sqlbuild-analysis/src/type_system/tests/alias_groups.json @@ -0,0 +1,327 @@ +{ + "bigquery": [ + ["ARRAY< FLOAT64(10,2) >", "FLOAT64(10,2) []"], + ["ARRAY< TIMESTAMP_NTZ(0)>", "TIMESTAMPNTZ(0) []"], + ["ARRAY", "DECIMAL(10,2)[]"], + ["ARRAY", "ARRAY", "INT[]", "SMALLINT(10) []"], + ["ARRAY", "ARRAY"], + ["BIGDECIMAL( 38 , 0 )", "DECIMAL( 38 , 0 )", "number( 38 , 0 )", "NUMERIC( 38 , 0 )"], + ["BIGDECIMAL(10,2)", "DECIMAL(10, 2)", "NUMBER(10,2)", "NUMERIC(10,2)"], + ["BIGINT", "BIGINT(10)", "INT", "INT(10)", "INT64", "INT64( 38 , 0 )", "INT64(10)", "INT64(10,2)", "INT64(MAX)", "integer", "integer(10)", "LONG", "LONG(10)", "SMALLINT", "SMALLINT(10)"], + ["BLOB", "bytes"], + ["BOOL", "BOOLEAN"], + ["CHAR", "CHARACTER"], + ["CHAR(10)", "CHARACTER(10)"], + ["CHARACTER VARYING(10)", "string(10)", "TEXT(10)", "VARCHAR(10)"], + ["DATETIME", "DATETIME(0)", "DATETIME(10)"], + ["DOUBLE", "DOUBLE PRECISION", "FLOAT", "FLOAT64", "FLOAT64( 38 , 0 )", "FLOAT64(10)", "FLOAT64(10, 2)", "FLOAT64(MAX)", "REAL"], + ["DOUBLE PRECISION(10)", "DOUBLE(10)", "FLOAT(10)", "REAL(10)"], + ["GEOGRAPHY", "GEOGRAPHY(10)"], + ["STRUCT(a INT)", "STRUCT(a INTEGER(10) )"], + ["TIMESTAMP", "TIMESTAMP(0)", "timestamp(10)"], + ["TIMESTAMP WITH TIME ZONE", "TIMESTAMPTZ", "timestamptz(0)", "TIMESTAMPTZ(10)", "TIMESTAMP_TZ", "TIMESTAMP_TZ(0)", "TIMESTAMP_TZ(10)"], + ["TIMESTAMPNTZ", "TIMESTAMPNTZ(0)", "TIMESTAMPNTZ(10)", "TIMESTAMP_NTZ", "TIMESTAMP_NTZ(0)", "TIMESTAMP_NTZ(10)"], + ["TIMESTAMP_LTZ", "TIMESTAMP_LTZ(0)", "TIMESTAMP_LTZ(10)"], + ["bigdecimal", "decimal", "NUMBER", "NUMERIC"], + ["bigdecimal(10)", "decimal(10)", "NUMBER(10)", "NUMERIC(10)"], + ["bool(10)", "BOOLEAN(10)"], + ["character varying", "STRING", "TEXT", "VARCHAR", "VARCHAR(MAX)"], + ["date", "DATE(10)"] + ], + "clickhouse": [ + ["BIGINT", "BIGINT(10)"], + ["BLOB", "Nullable(String)"], + ["Bool", "BOOL(10)", "BOOLEAN"], + ["CHAR", "CHAR(10)", "CHARACTER", "CHARACTER VARYING", "CHARACTER VARYING(10)", "CHARACTER(10)", "String", "STRING(10)", "TEXT", "VARCHAR", "VARCHAR(10)"], + ["DECIMAL", "NUMERIC"], + ["DECIMAL(10)", "NUMERIC(10)"], + ["DECIMAL(10,2)", "NUMERIC(10,2)"], + ["DOUBLE", "DOUBLE PRECISION"], + ["DOUBLE PRECISION(10)", "DOUBLE(10)"], + ["Date", "DATE(10)"], + ["DateTime", "DateTime('UTC')", "DATETIME(10)"], + ["FLOAT", "REAL"], + ["FLOAT(10)", "REAL(10)"], + ["Float64", "FLOAT64(10)", "FLOAT64(10,2)"], + ["INT", "INT(10)", "INTEGER", "INTEGER(10)"], + ["Int64", "INT64(10)", "INT64(10,2)"], + ["LONG", "LONG(10)"], + ["SMALLINT", "SMALLINT(10)"], + ["TIMESTAMP", "TIMESTAMP WITH TIME ZONE", "TIMESTAMP WITHOUT TIME ZONE", "TIMESTAMPTZ", "TIMESTAMP_TZ"], + ["TIMESTAMP(10)", "TIMESTAMPTZ(10)", "TIMESTAMP_TZ(10)"], + ["TIMESTAMPNTZ", "TIMESTAMPNTZ(10)", "TIMESTAMP_NTZ", "TIMESTAMP_NTZ(10)"], + ["TIMESTAMP_LTZ", "TIMESTAMP_LTZ(10)"], + ["TINYINT", "TINYINT(10)"] + ], + "databricks": [ + ["ARRAY< FLOAT64(10,2) >", "FLOAT64(10,2) []"], + ["ARRAY< TIMESTAMP_NTZ(0)>", "TIMESTAMPNTZ(0) []"], + ["ARRAY", "DECIMAL(10,2)[]"], + ["ARRAY", "INT[]"], + ["ARRAY", "ARRAY"], + ["BIGINT", "INT64"], + ["BOOL", "BOOLEAN", "BOOLEAN(10)"], + ["CHAR", "CHARACTER", "character varying", "STRING", "TEXT", "VARCHAR"], + ["CHAR(10)", "CHARACTER(10)"], + ["CHARACTER VARYING(10)", "string(10)", "TEXT(10)", "VARCHAR(10)"], + ["DATETIME", "DATETIME(0)", "DATETIME(10)"], + ["DECIMAL( 38 , 0 )", "NUMERIC( 38 , 0 )"], + ["DECIMAL(10, 2)", "NUMERIC(10,2)"], + ["DOUBLE", "DOUBLE PRECISION", "FLOAT64"], + ["DOUBLE PRECISION(10)", "DOUBLE(10)"], + ["FLOAT", "FLOAT(10)", "REAL", "REAL(10)"], + ["FLOAT64( 38 , 0 )", "FLOAT64(10)", "FLOAT64(10, 2)", "FLOAT64(MAX)"], + ["INT", "integer"], + ["INT(10)", "integer(10)"], + ["INT64( 38 , 0 )", "INT64(10)", "INT64(10,2)", "INT64(MAX)"], + ["LONG", "LONG(10)"], + ["TIMESTAMP", "TIMESTAMP WITHOUT TIME ZONE"], + ["TIMESTAMP WITH TIME ZONE", "TIMESTAMPTZ", "TIMESTAMP_TZ"], + ["TIMESTAMPNTZ", "TIMESTAMPNTZ(0)", "TIMESTAMPNTZ(10)", "TIMESTAMP_NTZ", "TIMESTAMP_NTZ(0)", "TIMESTAMP_NTZ(10)"], + ["TIMESTAMPTZ(10)", "TIMESTAMP_TZ(10)"], + ["TIMESTAMP_LTZ", "TIMESTAMP_LTZ(0)", "TIMESTAMP_LTZ(10)"], + ["binary", "BLOB"], + ["date", "DATE(10)"], + ["decimal", "NUMERIC"], + ["decimal(10)", "NUMERIC(10)"], + ["time", "TIME(0)", "TIME(10)"], + ["timestamptz(0)", "TIMESTAMP_TZ(0)"] + ], + "duckdb": [ + ["ARRAY< FLOAT64(10,2) >", "FLOAT64(10,2) []"], + ["ARRAY< TIMESTAMP_NTZ(0)>", "TIMESTAMPNTZ(0) []"], + ["ARRAY", "DECIMAL(10,2)[]"], + ["ARRAY", "INTEGER[]", "INT[]"], + ["ARRAY", "ARRAY"], + ["BIGINT", "INT64"], + ["BOOL", "BOOLEAN", "BOOLEAN(10)"], + ["CHAR", "CHARACTER", "character varying", "STRING", "TEXT", "VARCHAR"], + ["CHAR(10)", "CHARACTER(10)"], + ["CHARACTER VARYING(10)", "string(10)", "TEXT(10)", "VARCHAR(10)"], + ["DATETIME", "DATETIME(0)", "DATETIME(10)"], + ["DECIMAL( 38 , 0 )", "NUMERIC( 38 , 0 )"], + ["DECIMAL(10, 2)", "NUMERIC(10,2)"], + ["DOUBLE", "DOUBLE PRECISION", "FLOAT64"], + ["DOUBLE PRECISION(10)", "DOUBLE(10)"], + ["FLOAT64( 38 , 0 )", "FLOAT64(10)", "FLOAT64(10, 2)", "FLOAT64(MAX)"], + ["INT", "integer"], + ["INT(10)", "integer(10)"], + ["INT64( 38 , 0 )", "INT64(10)", "INT64(10,2)", "INT64(MAX)"], + ["LONG", "LONG(10)"], + ["TIMESTAMP", "TIMESTAMP WITHOUT TIME ZONE"], + ["TIMESTAMP WITH TIME ZONE", "TIMESTAMPTZ", "timestamptz(0)", "TIMESTAMPTZ(10)", "TIMESTAMP_TZ", "TIMESTAMP_TZ(0)", "TIMESTAMP_TZ(10)"], + ["TIMESTAMPNTZ", "TIMESTAMPNTZ(0)", "TIMESTAMPNTZ(10)", "TIMESTAMP_NTZ", "TIMESTAMP_NTZ(0)", "TIMESTAMP_NTZ(10)"], + ["TIMESTAMP_LTZ", "TIMESTAMP_LTZ(0)", "TIMESTAMP_LTZ(10)"], + ["date", "DATE(10)"], + ["decimal", "NUMERIC"], + ["decimal(10)", "NUMERIC(10)"] + ], + "generic": [ + ["ARRAY< FLOAT64(10,2) >", "FLOAT64(10,2) []"], + ["ARRAY< TIMESTAMP_NTZ(0)>", "TIMESTAMPNTZ(0) []"], + ["ARRAY", "DECIMAL(10,2)[]"], + ["ARRAY", "INT[]"], + ["ARRAY", "ARRAY"], + ["BIGINT", "INT64"], + ["BOOL", "BOOLEAN", "BOOLEAN(10)"], + ["CHAR", "CHARACTER"], + ["CHAR(10)", "CHARACTER(10)"], + ["CHARACTER VARYING(10)", "string(10)", "TEXT(10)", "VARCHAR(10)"], + ["DATETIME", "DATETIME(0)", "DATETIME(10)"], + ["DECIMAL( 38 , 0 )", "NUMERIC( 38 , 0 )"], + ["DECIMAL(10, 2)", "NUMERIC(10,2)"], + ["DOUBLE", "DOUBLE PRECISION", "FLOAT64"], + ["DOUBLE PRECISION(10)", "DOUBLE(10)"], + ["FLOAT64( 38 , 0 )", "FLOAT64(10)", "FLOAT64(10, 2)", "FLOAT64(MAX)"], + ["INT", "integer"], + ["INT(10)", "integer(10)"], + ["INT64( 38 , 0 )", "INT64(10)", "INT64(10,2)", "INT64(MAX)"], + ["LONG", "LONG(10)"], + ["TIMESTAMP", "TIMESTAMP WITHOUT TIME ZONE"], + ["TIMESTAMP WITH TIME ZONE", "TIMESTAMPTZ", "TIMESTAMP_TZ"], + ["TIMESTAMPNTZ", "TIMESTAMPNTZ(0)", "TIMESTAMPNTZ(10)", "TIMESTAMP_NTZ", "TIMESTAMP_NTZ(0)", "TIMESTAMP_NTZ(10)"], + ["TIMESTAMPTZ(10)", "TIMESTAMP_TZ(10)"], + ["TIMESTAMP_LTZ", "TIMESTAMP_LTZ(0)", "TIMESTAMP_LTZ(10)"], + ["character varying", "VARCHAR", "VARCHAR(MAX)"], + ["date", "DATE(10)"], + ["decimal", "NUMERIC"], + ["decimal(10)", "NUMERIC(10)"], + ["timestamptz(0)", "TIMESTAMP_TZ(0)"] + ], + "mssql": [ + ["BIGINT", "INT64"], + ["BOOL", "BOOLEAN"], + ["CHAR", "CHARACTER"], + ["CHAR(10)", "CHARACTER(10)"], + ["CHARACTER VARYING", "VARCHAR"], + ["CHARACTER VARYING(10)", "STRING(10)", "TEXT(10)", "VARCHAR(10)"], + ["DATE", "DATE(10)"], + ["DATETIME", "DATETIME(10)"], + ["DECIMAL", "NUMERIC"], + ["DECIMAL(10)", "NUMERIC(10)"], + ["DECIMAL(10,2)", "NUMERIC(10,2)"], + ["DOUBLE", "DOUBLE PRECISION", "FLOAT"], + ["DOUBLE PRECISION(10)", "DOUBLE(10)", "FLOAT(10)"], + ["FLOAT64(10)", "FLOAT64(10,2)"], + ["INT", "INTEGER"], + ["INT(10)", "INTEGER(10)"], + ["INT64(10)", "INT64(10,2)"], + ["LONG", "LONG(10)"], + ["STRING", "TEXT"], + ["TIMESTAMP", "TIMESTAMP WITHOUT TIME ZONE"], + ["TIMESTAMP WITH TIME ZONE", "TIMESTAMPTZ", "TIMESTAMP_TZ"], + ["TIMESTAMPNTZ", "TIMESTAMPNTZ(10)", "TIMESTAMP_NTZ", "TIMESTAMP_NTZ(10)"], + ["TIMESTAMPTZ(10)", "TIMESTAMP_TZ(10)"], + ["TIMESTAMP_LTZ", "TIMESTAMP_LTZ(10)"] + ], + "mysql": [ + ["ARRAY< DEC( 38 , 0 ) >", "ARRAY< FLOAT64(10,2) >", "ARRAY< TIMESTAMP(10) >", "ARRAY< TIMESTAMP_LTZ>", "ARRAY< TIMESTAMP_NTZ(0)>", "ARRAY< VARIANT[]>", "ARRAY>", "ARRAY", "ARRAY", "ARRAY", "ARRAY", "ARRAY", "ARRAY", "BIGINT[]", "bignumeric(10,2) []", "DECIMAL( 38 , 0 ) []", "DECIMAL(10,2)[]", "DOUBLE PRECISION(10)[]", "FLOAT64(10,2) []", "INT[]", "JSON", "JSON []", "MONEY(10)[]", "SMALLINT(10) []", "TIME(0) []", "TIMESTAMPNTZ(0) []", "TIMESTAMP_NTZ []"], + ["BIGINT", "INT64"], + ["BOOL", "BOOLEAN", "BOOLEAN(10)"], + ["CHAR", "CHARACTER"], + ["CHAR(10)", "CHARACTER(10)"], + ["CHARACTER VARYING(10)", "string(10)", "TEXT(10)", "VARCHAR(10)"], + ["DATETIME", "DATETIME(0)", "DATETIME(10)"], + ["DECIMAL( 38 , 0 )", "NUMERIC( 38 , 0 )"], + ["DECIMAL(10, 2)", "NUMERIC(10,2)"], + ["DOUBLE", "DOUBLE PRECISION", "FLOAT64"], + ["DOUBLE PRECISION(10)", "DOUBLE(10)"], + ["FLOAT", "REAL"], + ["FLOAT(10)", "REAL(10)"], + ["FLOAT64( 38 , 0 )", "FLOAT64(10)", "FLOAT64(10, 2)", "FLOAT64(MAX)"], + ["INT", "integer"], + ["INT(10)", "integer(10)"], + ["INT64( 38 , 0 )", "INT64(10)", "INT64(10,2)", "INT64(MAX)"], + ["LONG", "LONG(10)"], + ["TIMESTAMP", "TIMESTAMP WITH TIME ZONE", "TIMESTAMP WITHOUT TIME ZONE", "TIMESTAMPTZ", "TIMESTAMP_TZ"], + ["TIMESTAMP(0)", "timestamptz(0)", "TIMESTAMP_TZ(0)"], + ["TIMESTAMPNTZ", "TIMESTAMPNTZ(0)", "TIMESTAMPNTZ(10)", "TIMESTAMP_NTZ", "TIMESTAMP_NTZ(0)", "TIMESTAMP_NTZ(10)"], + ["TIMESTAMP_LTZ", "TIMESTAMP_LTZ(0)", "TIMESTAMP_LTZ(10)"], + ["character varying", "STRING", "TEXT", "VARCHAR"], + ["date", "DATE(10)"], + ["decimal", "NUMERIC"], + ["decimal(10)", "NUMERIC(10)"], + ["timestamp(10)", "TIMESTAMPTZ(10)", "TIMESTAMP_TZ(10)"] + ], + "postgres": [ + ["ARRAY< FLOAT64(10,2) >", "FLOAT64(10,2) []"], + ["ARRAY< TIMESTAMP_NTZ(0)>", "TIMESTAMPNTZ(0) []"], + ["ARRAY", "DECIMAL(10,2)[]"], + ["ARRAY", "integer[]", "INT[]"], + ["ARRAY", "ARRAY"], + ["BIGINT", "INT64"], + ["BLOB", "bytea"], + ["BOOL", "BOOLEAN", "BOOLEAN(10)"], + ["CHAR", "CHARACTER"], + ["CHAR(10)", "CHARACTER(10)"], + ["CHARACTER VARYING(10)", "string(10)", "TEXT(10)", "VARCHAR(10)"], + ["DATETIME", "DATETIME(0)", "DATETIME(10)"], + ["DECIMAL( 38 , 0 )", "NUMERIC( 38 , 0 )"], + ["DECIMAL(10, 2)", "NUMERIC(10,2)"], + ["DOUBLE", "DOUBLE PRECISION"], + ["DOUBLE PRECISION(10)", "DOUBLE(10)"], + ["FLOAT(10)", "REAL(10)"], + ["FLOAT64( 38 , 0 )", "FLOAT64(10)", "FLOAT64(10, 2)", "FLOAT64(MAX)"], + ["INT", "integer"], + ["INT(10)", "integer(10)"], + ["INT64( 38 , 0 )", "INT64(10)", "INT64(10,2)", "INT64(MAX)"], + ["LONG", "LONG(10)"], + ["SMALLINT", "TINYINT"], + ["SMALLINT(10)", "TINYINT(10)"], + ["STRING", "TEXT"], + ["TIMESTAMP", "TIMESTAMP WITHOUT TIME ZONE"], + ["TIMESTAMP WITH TIME ZONE", "TIMESTAMPTZ", "TIMESTAMP_TZ"], + ["TIMESTAMPNTZ", "TIMESTAMPNTZ(0)", "TIMESTAMPNTZ(10)", "TIMESTAMP_NTZ", "TIMESTAMP_NTZ(0)", "TIMESTAMP_NTZ(10)"], + ["TIMESTAMPTZ(10)", "TIMESTAMP_TZ(10)"], + ["TIMESTAMP_LTZ", "TIMESTAMP_LTZ(0)", "TIMESTAMP_LTZ(10)"], + ["character varying", "VARCHAR", "VARCHAR(MAX)"], + ["date", "DATE(10)"], + ["decimal", "NUMERIC"], + ["decimal(10)", "NUMERIC(10)"], + ["time", "time without time zone"], + ["timestamptz(0)", "TIMESTAMP_TZ(0)"] + ], + "snowflake": [ + ["ARRAY< FLOAT64(10,2) >", "FLOAT64(10,2) []"], + ["ARRAY< TIMESTAMP_NTZ(0)>", "TIMESTAMPNTZ(0) []"], + ["ARRAY", "DECIMAL(10,2)[]"], + ["ARRAY", "INT[]"], + ["ARRAY", "ARRAY"], + ["BIGDECIMAL(10,2)", "bignumeric(10, 2)", "DECIMAL(10, 2)", "NUMBER(10,2)", "NUMERIC(10,2)"], + ["BOOL", "BOOLEAN", "BOOLEAN(10)"], + ["CHAR", "CHARACTER"], + ["CHAR(10)", "CHARACTER(10)"], + ["CHARACTER VARYING(10)", "string(10)", "TEXT(10)", "VARCHAR(10)"], + ["DATETIME", "DATETIME(0)", "DATETIME(10)"], + ["DOUBLE", "DOUBLE PRECISION"], + ["DOUBLE PRECISION(10)", "DOUBLE(10)"], + ["FLOAT", "REAL"], + ["FLOAT(10)", "REAL(10)"], + ["FLOAT64", "FLOAT64( 38 , 0 )", "FLOAT64(10)", "FLOAT64(10, 2)", "FLOAT64(MAX)"], + ["GEOGRAPHY", "GEOGRAPHY(10)"], + ["JSON", "variant"], + ["TIMESTAMP", "TIMESTAMP WITHOUT TIME ZONE", "TIMESTAMPNTZ", "TIMESTAMPNTZ(0)", "TIMESTAMPNTZ(10)", "TIMESTAMP_NTZ", "TIMESTAMP_NTZ(0)", "TIMESTAMP_NTZ(10)", "TIMESTAMP_NTZ(9)"], + ["TIMESTAMP WITH TIME ZONE", "TIMESTAMPTZ", "TIMESTAMP_TZ"], + ["TIMESTAMPTZ(10)", "TIMESTAMP_TZ(10)"], + ["TIMESTAMP_LTZ", "TIMESTAMP_LTZ(0)", "TIMESTAMP_LTZ(10)", "TIMESTAMP_LTZ(9)"], + ["bigdecimal", "BIGDECIMAL( 38 , 0 )", "BIGINT", "BIGINT(10)", "BIGNUMERIC", "BIGNUMERIC( 38 , 0 )", "decimal", "DECIMAL( 38 , 0 )", "INT", "INT(10)", "INT64", "INT64( 38 , 0 )", "INT64(10)", "INT64(10,2)", "INT64(MAX)", "integer", "integer(10)", "LONG", "LONG(10)", "NUMBER", "number( 38 , 0 )", "NUMERIC", "NUMERIC( 38 , 0 )", "SMALLINT", "SMALLINT(10)", "TINYINT", "TINYINT(10)"], + ["bigdecimal(10)", "BIGNUMERIC(10)", "decimal(10)", "NUMBER(10)", "NUMERIC(10)"], + ["character varying", "STRING", "TEXT", "VARCHAR", "VARCHAR(16777216)", "VARCHAR(MAX)"], + ["date", "DATE(10)"], + ["timestamptz(0)", "TIMESTAMP_TZ(0)"] + ], + "sqlite": [ + ["BIGINT", "INT64"], + ["BOOL", "BOOLEAN", "BOOLEAN(10)"], + ["CHAR", "CHARACTER", "CHARACTER VARYING", "TEXT", "VARCHAR"], + ["CHAR(10)", "CHARACTER(10)"], + ["CHARACTER VARYING(10)", "STRING(10)", "TEXT(10)", "VARCHAR(10)"], + ["DATE", "DATE(10)"], + ["DATETIME", "DATETIME(10)"], + ["DECIMAL", "NUMERIC"], + ["DECIMAL(10)", "NUMERIC(10)"], + ["DECIMAL(10,2)", "NUMERIC(10,2)"], + ["DOUBLE", "DOUBLE PRECISION", "REAL"], + ["DOUBLE PRECISION(10)", "DOUBLE(10)", "REAL(10)"], + ["FLOAT64(10)", "FLOAT64(10,2)"], + ["INT", "INTEGER", "SMALLINT", "SMALLINT(10)"], + ["INT(10)", "INTEGER(10)"], + ["INT64(10)", "INT64(10,2)"], + ["LONG", "LONG(10)"], + ["TIMESTAMP", "TIMESTAMP WITHOUT TIME ZONE"], + ["TIMESTAMP WITH TIME ZONE", "TIMESTAMPTZ", "TIMESTAMP_TZ"], + ["TIMESTAMPNTZ", "TIMESTAMPNTZ(10)", "TIMESTAMP_NTZ", "TIMESTAMP_NTZ(10)"], + ["TIMESTAMPTZ(10)", "TIMESTAMP_TZ(10)"], + ["TIMESTAMP_LTZ", "TIMESTAMP_LTZ(10)"] + ], + "tsql": [ + ["ARRAY< DEC( 38 , 0 ) >", "ARRAY< FLOAT64(10,2) >", "ARRAY< TIMESTAMP(10) >", "ARRAY< TIMESTAMP_LTZ>", "ARRAY< TIMESTAMP_NTZ(0)>", "ARRAY>", "ARRAY", "ARRAY", "ARRAY", "ARRAY", "ARRAY", "JSON", "nvarchar(max)"], + ["BIGINT", "INT64"], + ["BOOL", "BOOLEAN"], + ["CHAR", "CHARACTER"], + ["CHAR(10)", "CHARACTER(10)"], + ["CHARACTER VARYING(10)", "string(10)", "TEXT(10)", "VARCHAR(10)"], + ["DATETIME", "DATETIME(0)", "DATETIME(10)"], + ["DECIMAL( 38 , 0 )", "NUMERIC( 38 , 0 )"], + ["DECIMAL(10, 2)", "NUMERIC(10,2)"], + ["DOUBLE", "DOUBLE PRECISION", "FLOAT"], + ["DOUBLE PRECISION(10)", "DOUBLE(10)", "FLOAT(10)"], + ["FLOAT64( 38 , 0 )", "FLOAT64(10)", "FLOAT64(10, 2)", "FLOAT64(MAX)"], + ["INT", "integer"], + ["INT(10)", "integer(10)"], + ["INT64( 38 , 0 )", "INT64(10)", "INT64(10,2)", "INT64(MAX)"], + ["LONG", "LONG(10)"], + ["TIMESTAMP", "TIMESTAMP WITHOUT TIME ZONE"], + ["TIMESTAMP WITH TIME ZONE", "TIMESTAMPTZ", "TIMESTAMP_TZ"], + ["TIMESTAMPNTZ", "TIMESTAMPNTZ(0)", "TIMESTAMPNTZ(10)", "TIMESTAMP_NTZ", "TIMESTAMP_NTZ(0)", "TIMESTAMP_NTZ(10)"], + ["TIMESTAMPTZ(10)", "TIMESTAMP_TZ(10)"], + ["TIMESTAMP_LTZ", "TIMESTAMP_LTZ(0)", "TIMESTAMP_LTZ(10)"], + ["character varying", "VARCHAR", "VARCHAR(MAX)"], + ["date", "DATE(10)"], + ["decimal", "NUMERIC"], + ["decimal(10)", "NUMERIC(10)"], + ["ntext", "STRING", "TEXT"], + ["timestamptz(0)", "TIMESTAMP_TZ(0)"], + ["uniqueidentifier", "UUID"] + ] +} diff --git a/crates/sqlbuild-analysis/src/type_system/tests/helpers.rs b/crates/sqlbuild-analysis/src/type_system/tests/helpers.rs index cc51b6f245..cb6ab068fa 100644 --- a/crates/sqlbuild-analysis/src/type_system/tests/helpers.rs +++ b/crates/sqlbuild-analysis/src/type_system/tests/helpers.rs @@ -1,4 +1,5 @@ -use crate::type_system::models::{PythonInteger, TypeNormalization}; +use crate::type_system::main::normalize_type::normalize_type; +use crate::type_system::models::{NormalizedType, PythonInteger, TypeNormalization}; pub(super) fn normalization_text(normalization: &TypeNormalization) -> String { let optional = @@ -14,3 +15,10 @@ pub(super) fn normalization_text(normalization: &TypeNormalization) -> String { normalization.parse_error.as_deref().unwrap_or("-"), ) } + +/// The normalized type of `type_sql` in `dialect`, which must be a known dialect. +pub(super) fn normalized_type(type_sql: &str, dialect: &str) -> NormalizedType { + normalize_type(type_sql, dialect) + .expect("the dialect is known") + .normalized +} diff --git a/crates/sqlbuild-analysis/src/type_system/tests/test_normalize_type.rs b/crates/sqlbuild-analysis/src/type_system/tests/test_normalize_type.rs index afee3fc479..e33e79fba0 100644 --- a/crates/sqlbuild-analysis/src/type_system/tests/test_normalize_type.rs +++ b/crates/sqlbuild-analysis/src/type_system/tests/test_normalize_type.rs @@ -83,17 +83,17 @@ fn given_type_strings_when_normalizing_then_python_normalization_is_returned() { type_sql: "order status", dialect: "generic", expected_normalization: Ok( - "ORDERSTATUS | other | - | - | - | Parse error at line 1, column 13: Unexpected token after data type: status", + "ORDERSTATUS | other | - | - | - | unexpected 'status' after the type", ), }, NormalizeTypeTestCase { - description: "every Polyglot dialect is normalized natively", + description: "another SQL analysis dialect reads types as generic SQL", type_sql: "INT", dialect: "mysql", expected_normalization: Ok("INT | integer | - | - | - | -"), }, NormalizeTypeTestCase { - description: "a dialect Polyglot does not know raises Python's error", + description: "a dialect name SQLBuild does not know raises Python's error", type_sql: "INT", dialect: "motherduck", expected_normalization: Err("Unknown dialect: motherduck"), @@ -110,7 +110,8 @@ fn given_type_strings_when_normalizing_then_python_normalization_is_returned() { dialect: "duckdb", expected_normalization: Ok( "DECIMAL(99999999999999999999,2) | decimal | 99999999999999999999 | 2 | - | \ - Parse error at line 1, column 30: Invalid number: 99999999999999999999", + Parse(Parse { message: \"Invalid number: 99999999999999999999\", line: 1, \ + column: 50, start: 48, end: 49 })", ), }, NormalizeTypeTestCase { @@ -118,7 +119,8 @@ fn given_type_strings_when_normalizing_then_python_normalization_is_returned() { type_sql: "VARCHAR(\u{663})", dialect: "snowflake", expected_normalization: Ok( - "VARCHAR(3) | string | - | - | 3 | Parse error at line 1, column 10: Expected number", + "VARCHAR(3) | string | - | - | 3 | Parse(Parse { message: \"Expected number\", \ + line: 1, column: 30, start: 28, end: 29 })", ), }, ]; diff --git a/crates/sqlbuild-analysis/src/type_system/tests/test_same_type.rs b/crates/sqlbuild-analysis/src/type_system/tests/test_same_type.rs index c97bf25c73..52f84e5e99 100644 --- a/crates/sqlbuild-analysis/src/type_system/tests/test_same_type.rs +++ b/crates/sqlbuild-analysis/src/type_system/tests/test_same_type.rs @@ -1,9 +1,15 @@ +use std::collections::BTreeMap; + use crate::type_system::main::normalize_type::normalize_type; use crate::type_system::main::same_type::same_type; -use crate::type_system::tests::test_types::SameTypeTestCase; +use crate::type_system::tests::helpers::normalized_type; +use crate::type_system::tests::test_types::{AliasGroupsTestCase, SameTypeTestCase}; + +/// Each dialect's groups of valid spellings that the Polyglot-based normalization read alike. +const ALIAS_GROUPS: &str = include_str!("alias_groups.json"); #[test] -fn given_two_types_when_comparing_then_an_unstated_snowflake_temporal_precision_matches() { +fn given_two_types_when_comparing_then_same_type_answers() { let test_cases = [ SameTypeTestCase { description: "a declared millisecond time matches metadata's precision-less time", @@ -65,6 +71,24 @@ fn given_two_types_when_comparing_then_an_unstated_snowflake_temporal_precision_ types: ("VARCHAR(10)", "VARCHAR(20)"), expected_same: false, }, + SameTypeTestCase { + description: "Postgres FLOAT without a precision is DOUBLE PRECISION, not REAL", + dialect: "postgres", + types: ("FLOAT", "REAL"), + expected_same: false, + }, + SameTypeTestCase { + description: "BigQuery's TIMESTAMP is an absolute time, not a civil DATETIME", + dialect: "bigquery", + types: ("TIMESTAMP", "TIMESTAMP WITHOUT TIME ZONE"), + expected_same: false, + }, + SameTypeTestCase { + description: "BigQuery's civil time is DATETIME", + dialect: "bigquery", + types: ("DATETIME", "TIMESTAMP WITHOUT TIME ZONE"), + expected_same: true, + }, ]; for test_case in test_cases { let (left, right) = test_case.types; @@ -82,3 +106,34 @@ fn given_two_types_when_comparing_then_an_unstated_snowflake_temporal_precision_ ); } } + +#[test] +fn given_spellings_of_one_type_when_comparing_then_every_pair_is_the_same_type() { + let test_cases = [AliasGroupsTestCase { + description: "every alias group of the Polyglot-based normalization", + groups: ALIAS_GROUPS, + expected_same: true, + }]; + for test_case in test_cases { + let groups: BTreeMap>> = + serde_json::from_str(test_case.groups).expect("the alias groups are JSON"); + for (dialect, dialect_groups) in &groups { + for group in dialect_groups { + for (index, left) in group.iter().enumerate() { + for right in &group[index + 1..] { + assert_eq!( + same_type( + &normalized_type(left, dialect), + &normalized_type(right, dialect), + dialect, + ), + test_case.expected_same, + "{}: {dialect} `{left}` and `{right}`", + test_case.description + ); + } + } + } + } + } +} diff --git a/crates/sqlbuild-analysis/src/type_system/tests/test_types.rs b/crates/sqlbuild-analysis/src/type_system/tests/test_types.rs index 2b9aeec95d..b678a56f7f 100644 --- a/crates/sqlbuild-analysis/src/type_system/tests/test_types.rs +++ b/crates/sqlbuild-analysis/src/type_system/tests/test_types.rs @@ -30,3 +30,10 @@ pub(super) struct SameTypeTestCase { pub(super) types: (&'static str, &'static str), pub(super) expected_same: bool, } + +pub(super) struct AliasGroupsTestCase { + pub(super) description: &'static str, + /// JSON: each dialect's groups of spellings of one type. + pub(super) groups: &'static str, + pub(super) expected_same: bool, +} diff --git a/crates/sqlbuild-analysis/src/warehouse_types/_helpers/mod.rs b/crates/sqlbuild-analysis/src/warehouse_types/_helpers/mod.rs new file mode 100644 index 0000000000..b0d2d9639d --- /dev/null +++ b/crates/sqlbuild-analysis/src/warehouse_types/_helpers/mod.rs @@ -0,0 +1 @@ +pub(crate) mod type_trees; diff --git a/crates/sqlbuild-analysis/src/warehouse_types/_helpers/type_trees.rs b/crates/sqlbuild-analysis/src/warehouse_types/_helpers/type_trees.rs new file mode 100644 index 0000000000..6a810dcfc0 --- /dev/null +++ b/crates/sqlbuild-analysis/src/warehouse_types/_helpers/type_trees.rs @@ -0,0 +1,67 @@ +//! Type trees from SQLQuack's parsers: Postgres's type grammar, or a cast target's type. + +use serde_json::Value; +use sqlquack_syntax::tokens::models::Dialect; +use sqlquack_syntax::{Expression, ParseOptions, parse_strict}; + +use crate::warehouse_types::constants::{CAST_KEY, CAST_TARGET_KEY}; + +/// `spelling` read by `sqlquack-syntax-postgres`'s type grammar. +pub(crate) fn postgres_type(spelling: &str) -> Result { + let data_type = + sqlquack_syntax_postgres::parse_type(spelling).map_err(|error| format!("{error:?}"))?; + serde_json::to_value(data_type).map_err(|error| error.to_string()) +} + +/// `spelling` read as a cast target by SQLQuack's core parser for `dialect`. +pub(crate) fn core_type(spelling: &str, dialect: Dialect) -> Result { + let statements: Vec = + parse_strict(&cast(spelling), dialect, &ParseOptions::default()) + .map_err(|error| format!("{error:?}"))?; + single_cast_target(spelling, &statements) +} + +/// `spelling` read as a cast target by `sqlquack-syntax-bigquery`. +pub(crate) fn bigquery_type(spelling: &str) -> Result { + let statements: Vec = sqlquack_syntax_bigquery::parse_strict(&cast(spelling)) + .map_err(|error| format!("{error:?}"))? + .statements; + single_cast_target(spelling, &statements) +} + +/// A statement that casts NULL to `spelling`. +fn cast(spelling: &str) -> String { + format!("SELECT CAST(NULL AS {spelling})") +} + +/// The target type of the one cast in the one statement, or why `spelling` is not one type. +fn single_cast_target(spelling: &str, statements: &[Expression]) -> Result { + let tree: Value = serde_json::to_value(statements).map_err(|error| error.to_string())?; + match (statements.len(), cast_targets(&tree).as_slice()) { + (1, [target]) => Ok((*target).clone()), + _ => Err(format!("`{spelling}` is not a single type name")), + } +} + +/// Every cast's target type in `tree`, in order. +fn cast_targets(tree: &Value) -> Vec<&Value> { + let mut targets: Vec<&Value> = Vec::new(); + let mut pending: Vec<&Value> = vec![tree]; + while let Some(node) = pending.pop() { + match node { + Value::Object(fields) => { + for (key, value) in fields.iter().rev() { + pending.push(value); + if key == CAST_KEY + && let Some(target) = value.get(CAST_TARGET_KEY) + { + targets.push(target); + } + } + } + Value::Array(items) => pending.extend(items.iter().rev()), + _ => {} + } + } + targets +} diff --git a/crates/sqlbuild-analysis/src/warehouse_types/main/mod.rs b/crates/sqlbuild-analysis/src/warehouse_types/main/mod.rs index fdbebafce0..5d33207517 100644 --- a/crates/sqlbuild-analysis/src/warehouse_types/main/mod.rs +++ b/crates/sqlbuild-analysis/src/warehouse_types/main/mod.rs @@ -1 +1,2 @@ pub mod parse_warehouse_type; +pub mod type_tree; diff --git a/crates/sqlbuild-analysis/src/warehouse_types/main/parse_warehouse_type.rs b/crates/sqlbuild-analysis/src/warehouse_types/main/parse_warehouse_type.rs index dc6682be9a..4128082fb4 100644 --- a/crates/sqlbuild-analysis/src/warehouse_types/main/parse_warehouse_type.rs +++ b/crates/sqlbuild-analysis/src/warehouse_types/main/parse_warehouse_type.rs @@ -2,58 +2,17 @@ use serde_json::Value; use sqlquack_syntax::tokens::models::Dialect; -use sqlquack_syntax::{Expression, ParseOptions, parse_strict}; -use crate::warehouse_types::constants::{ - BIGQUERY_DIALECT, CAST_KEY, CAST_TARGET_KEY, POSTGRES_DIALECT, SNOWFLAKE_DIALECT, -}; +use crate::warehouse_types::constants::{BIGQUERY_DIALECT, POSTGRES_DIALECT, SNOWFLAKE_DIALECT}; +use crate::warehouse_types::main::type_tree::type_tree; +use crate::warehouse_types::models::TypeParser; /// The type tree of `spelling` in `dialect`, None without a parser, or why the parser rejects it. pub fn parse_warehouse_type(spelling: &str, dialect: &str) -> Result, String> { - let cast: String = format!("SELECT CAST(NULL AS {spelling})"); - let statements: Vec = match dialect { - POSTGRES_DIALECT => { - let data_type = sqlquack_syntax_postgres::parse_type(spelling) - .map_err(|error| format!("{error:?}"))?; - return serde_json::to_value(data_type) - .map(Some) - .map_err(|error| error.to_string()); - } - SNOWFLAKE_DIALECT => parse_strict(&cast, Dialect::Snowflake, &ParseOptions::default()) - .map_err(|error| format!("{error:?}"))?, - BIGQUERY_DIALECT => { - sqlquack_syntax_bigquery::parse_strict(&cast) - .map_err(|error| format!("{error:?}"))? - .statements - } - _ => return Ok(None), - }; - let tree: Value = serde_json::to_value(&statements).map_err(|error| error.to_string())?; - match (statements.len(), cast_targets(&tree).as_slice()) { - (1, [target]) => Ok(Some((*target).clone())), - _ => Err(format!("`{spelling}` is not a single type name")), + match dialect { + POSTGRES_DIALECT => type_tree(spelling, TypeParser::Postgres).map(Some), + SNOWFLAKE_DIALECT => type_tree(spelling, TypeParser::Core(Dialect::Snowflake)).map(Some), + BIGQUERY_DIALECT => type_tree(spelling, TypeParser::BigQuery).map(Some), + _ => Ok(None), } } - -/// Every cast's target type in `tree`, in order. -fn cast_targets(tree: &Value) -> Vec<&Value> { - let mut targets: Vec<&Value> = Vec::new(); - let mut pending: Vec<&Value> = vec![tree]; - while let Some(node) = pending.pop() { - match node { - Value::Object(fields) => { - for (key, value) in fields.iter().rev() { - pending.push(value); - if key == CAST_KEY - && let Some(target) = value.get(CAST_TARGET_KEY) - { - targets.push(target); - } - } - } - Value::Array(items) => pending.extend(items.iter().rev()), - _ => {} - } - } - targets -} diff --git a/crates/sqlbuild-analysis/src/warehouse_types/main/type_tree.rs b/crates/sqlbuild-analysis/src/warehouse_types/main/type_tree.rs new file mode 100644 index 0000000000..cfa9c12309 --- /dev/null +++ b/crates/sqlbuild-analysis/src/warehouse_types/main/type_tree.rs @@ -0,0 +1,15 @@ +//! `type_tree`: a type name as one of SQLQuack's parsers reads it. + +use serde_json::Value; + +use crate::warehouse_types::_helpers::type_trees::{bigquery_type, core_type, postgres_type}; +use crate::warehouse_types::models::TypeParser; + +/// The type tree `parser` reads from `spelling`, or why it rejects it. +pub fn type_tree(spelling: &str, parser: TypeParser) -> Result { + match parser { + TypeParser::Postgres => postgres_type(spelling), + TypeParser::BigQuery => bigquery_type(spelling), + TypeParser::Core(dialect) => core_type(spelling, dialect), + } +} diff --git a/crates/sqlbuild-analysis/src/warehouse_types/mod.rs b/crates/sqlbuild-analysis/src/warehouse_types/mod.rs index c0ebb26c0f..8e1b42f6e3 100644 --- a/crates/sqlbuild-analysis/src/warehouse_types/mod.rs +++ b/crates/sqlbuild-analysis/src/warehouse_types/mod.rs @@ -1,4 +1,6 @@ //! Warehouse column type names as SQLQuack's parser for each warehouse dialect reads them. +pub(crate) mod _helpers; pub(crate) mod constants; pub mod main; +pub mod models; diff --git a/crates/sqlbuild-analysis/src/warehouse_types/models.rs b/crates/sqlbuild-analysis/src/warehouse_types/models.rs new file mode 100644 index 0000000000..8fb7d377e6 --- /dev/null +++ b/crates/sqlbuild-analysis/src/warehouse_types/models.rs @@ -0,0 +1,14 @@ +//! The SQLQuack parsers that read warehouse type names. + +use sqlquack_syntax::tokens::models::Dialect; + +/// Which SQLQuack parser reads a type name. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TypeParser { + /// `sqlquack-syntax-postgres`'s type grammar. + Postgres, + /// A cast target read by `sqlquack-syntax-bigquery`. + BigQuery, + /// A cast target read by SQLQuack's core parser for the dialect. + Core(Dialect), +} diff --git a/crates/sqlbuild-python/src/bindings/_helpers/contracts/model_contracts.rs b/crates/sqlbuild-python/src/bindings/_helpers/contracts/model_contracts.rs index b7ef9cdd1b..99d744dae5 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/contracts/model_contracts.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/contracts/model_contracts.rs @@ -51,7 +51,7 @@ pub(crate) fn evaluate_project_model_contracts( .compiler_detach(|| Ok(evaluate_model_contracts(&request))) .map_err(compiler_error)?; let outcomes: Vec> = - outcomes.map_err(|error| normalization_error(py, &error))?; + outcomes.map_err(|error| normalization_error(&error))?; Ok(models .iter() .zip(outcomes) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/completion.rs b/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/completion.rs index fc4d785cc2..8fdb86fd0a 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/completion.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/completion.rs @@ -66,7 +66,7 @@ pub(crate) fn completed_diagnostics<'py>( let (completed, model_bindings, order) = py .compiler_detach(|| Ok(complete_semantic_diagnostics(&request, shared.catalog))) .map_err(compiler_error)? - .map_err(|failure| semantic_error(py, &failure))? + .map_err(|failure| semantic_error(&failure))? .into_parts(); let mut rebuilt: Vec> = Vec::with_capacity(completed.len()); for row in completed { diff --git a/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/failures.rs b/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/failures.rs index 8535b64c6c..486267e427 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/failures.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/failures.rs @@ -1,7 +1,7 @@ //! The exception a failed native semantic completion step raises. +use pyo3::PyErr; use pyo3::exceptions::{PyTypeError, PyValueError}; -use pyo3::{PyErr, Python}; use sqlbuild_analysis::semantic_checks::models::SemanticFailure; use sqlbuild_core::constants::PANIC_MESSAGE; use sqlbuild_core::panics::main::is_compiler_panic::is_compiler_panic; @@ -15,13 +15,13 @@ const SEMANTIC_CONTEXT: &str = "native semantic completion"; const NO_DIALECT_MESSAGE: &str = "argument 'dialect': 'None' is not an instance of 'str'"; /// The error Python's semantic completion raised for the same input, or `NativeCompilerError`. -pub(crate) fn semantic_error(python: Python<'_>, failure: &SemanticFailure) -> PyErr { +pub(crate) fn semantic_error(failure: &SemanticFailure) -> PyErr { match failure { SemanticFailure::NoDialect => PyTypeError::new_err(NO_DIALECT_MESSAGE), SemanticFailure::UnknownDialect(name) => { PyValueError::new_err(format!("Unknown dialect: {name}")) } - SemanticFailure::TypeNormalization(error) => normalization_error(python, error), + SemanticFailure::TypeNormalization(error) => normalization_error(error), SemanticFailure::Internal(reason) => internal_error(reason), } } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/metadata.rs b/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/metadata.rs index 422162374f..629a05e747 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/metadata.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/metadata.rs @@ -70,7 +70,7 @@ pub(crate) fn metadata_diagnostics<'py>( let outcome = py .compiler_detach(|| Ok(check_semantic_metadata(&request, catalog))) .map_err(compiler_error)? - .map_err(|failure| semantic_error(py, &failure))?; + .map_err(|failure| semantic_error(&failure))?; let dialect: Bound<'py, PyAny> = native_field(profile, "sql_analysis_dialect")?; for type_sql in &outcome.fallback_types { let _ = classes.call( diff --git a/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/type_recovery.rs b/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/type_recovery.rs index c75a2b2af4..eddbc4fc81 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/type_recovery.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/semantic_checks/type_recovery.rs @@ -93,7 +93,7 @@ pub(crate) fn recovered_types<'py>( let step = py .compiler_detach(|| Ok(plan_type_recovery(&request, shared.catalog))) .map_err(compiler_error)? - .map_err(|failure| semantic_error(py, &failure))?; + .map_err(|failure| semantic_error(&failure))?; if step.status() == UNCHANGED { return Ok(project.clone()); } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/type_system/equality.rs b/crates/sqlbuild-python/src/bindings/_helpers/type_system/equality.rs index 125c688b3c..805d5de931 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/type_system/equality.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/type_system/equality.rs @@ -16,7 +16,7 @@ use crate::bindings::_helpers::native_host::host; use crate::bindings::_helpers::type_system::normalization::normalization_error; const TYPE_NORMALIZATION_LOGGER: &str = "sqlbuild.adapter"; -const TYPE_PARSE_FALLBACK_MESSAGE: &str = "type normalization polyglot parse failed; falling back"; +const TYPE_PARSE_FALLBACK_MESSAGE: &str = "type normalization parse failed; falling back"; const DEFAULT_DIALECT: &str = "generic"; const CACHE_LIMIT: usize = 1024; @@ -37,7 +37,6 @@ pub(crate) fn types_equal(dialect: &Bound<'_, PyAny>, left: &str, right: &str) - } fn normalized(dialect: &Bound<'_, PyAny>, name: &str, type_sql: &str) -> PyResult { - let py: Python<'_> = dialect.py(); let cache: &Cache = NORMALIZED.get_or_init(|| Mutex::new(HashMap::new())); let key: (String, String) = (type_sql.to_owned(), name.to_owned()); let cached: Option = cache @@ -52,7 +51,7 @@ fn normalized(dialect: &Bound<'_, PyAny>, name: &str, type_sql: &str) -> PyResul normalized, parse_error, } = compiler_guard(|| Ok(normalize_type(type_sql, name)))? - .map_err(|error| normalization_error(py, &error))?; + .map_err(|error| normalization_error(&error))?; if let Some(parse_error) = parse_error { log_parse_fallback(dialect, type_sql, &parse_error)?; } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/type_system/normalization.rs b/crates/sqlbuild-python/src/bindings/_helpers/type_system/normalization.rs index 74fe591603..3193722270 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/type_system/normalization.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/type_system/normalization.rs @@ -1,14 +1,13 @@ //! Native type normalization. use pyo3::exceptions::PyValueError; -use pyo3::prelude::{Bound, PyAnyMethods, PyErr, PyModule, PyModuleMethods, PyResult, Python}; +use pyo3::prelude::{Bound, PyErr, PyModule, PyModuleMethods, PyResult}; use pyo3::{pyfunction, wrap_pyfunction}; use sqlbuild_analysis::type_system::errors::TypeNormalizationError; use sqlbuild_analysis::type_system::main::normalize_type::normalize_type; use sqlbuild_analysis::type_system::models::{NormalizedType, TypeNormalization}; use crate::bindings::_helpers::boundary::panics::compiler_guard; -use crate::bindings::_helpers::records::fields::field as native_field; positional_record! { into; @@ -22,11 +21,10 @@ positional_record! { } } -/// Normalize one type: `(normalized row, Polyglot parse error)`, or Python's exception. +/// Normalize one type: `(normalized row, parse error)`, or Python's exception. #[pyfunction] #[pyo3(name = "normalize_type")] fn normalize_type_binding( - python: Python<'_>, type_sql: &str, dialect: &str, ) -> PyResult<(NormalizedRow, Option)> { @@ -37,20 +35,13 @@ fn normalize_type_binding( normalized, parse_error, }) => Ok((normalized_row(normalized), parse_error)), - Err(error) => Err(normalization_error(python, &error)), + Err(error) => Err(normalization_error(&error)), } } -/// The exception the Polyglot wheel raises for the same failure. -pub(crate) fn normalization_error(python: Python<'_>, error: &TypeNormalizationError) -> PyErr { - match error { - TypeNormalizationError::UnknownDialect(_) => PyValueError::new_err(error.message()), - TypeNormalizationError::Generation(_) => python - .import("polyglot_sql") - .and_then(|module| native_field(&module, "PolyglotError")) - .and_then(|class| class.call1((error.message(),))) - .map_or_else(|import_error| import_error, PyErr::from_value), - } +/// Python's exception for a type that cannot be normalized. +pub(crate) fn normalization_error(error: &TypeNormalizationError) -> PyErr { + PyValueError::new_err(error.message()) } fn normalized_row(normalized: NormalizedType) -> NormalizedRow { diff --git a/fensu.toml b/fensu.toml index bf5365df88..7f0ef427f5 100644 --- a/fensu.toml +++ b/fensu.toml @@ -2440,7 +2440,7 @@ pending_budgets = [ "crates/sqlbuild-python/src/bindings/_helpers/state_rows/state_tables.rs=1", "crates/sqlbuild-python/src/bindings/_helpers/state_rows/timestamps.rs=4", "crates/sqlbuild-python/src/bindings/_helpers/type_system/equality.rs=3", - "crates/sqlbuild-python/src/bindings/_helpers/type_system/normalization.rs=2", + "crates/sqlbuild-python/src/bindings/_helpers/type_system/normalization.rs=0", "crates/sqlbuild-python/src/bindings/_helpers/workspace_commands/commands.rs=5", "crates/sqlbuild-python/src/bindings/models.rs=0", ] @@ -2934,5 +2934,5 @@ path = "src/sqlbuild/orchestration_declarations/models/sensor_context.py" reason = "Only SensorContext lives here: invocation-local ctx.cursor assignment proposes progress, committed solely by native fenced completion. The package regression guards this declaration-only scope; class-symbol exceptions are unsupported." [[dupes.allowlist]] -paths = ["crates/sqlbuild-analysis/src/warehouse_types/main/parse_warehouse_type.rs", "crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dict_walk.rs", "crates/sqlbuild-analysis/src/relation_names/_helpers/walk.rs"] +paths = ["crates/sqlbuild-analysis/src/warehouse_types/_helpers/type_trees.rs", "crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dict_walk.rs", "crates/sqlbuild-analysis/src/relation_names/_helpers/walk.rs"] reason = "Each is a short stack walk over a JSON syntax tree with its own stop and collect rule: cast_targets collects every cast's target type, branch_selects stops at each set-operation branch, and walked lists every object. A shared visitor would need a callback for each rule and save no logic." diff --git a/src/sqlbuild/adapter/type_system/constants.py b/src/sqlbuild/adapter/type_system/constants.py index 345fe35380..83af16becc 100644 --- a/src/sqlbuild/adapter/type_system/constants.py +++ b/src/sqlbuild/adapter/type_system/constants.py @@ -1,4 +1,4 @@ """Logging names shared by the Python and native type normalization.""" TYPE_NORMALIZATION_LOGGER_NAME: str = "sqlbuild.adapter" -TYPE_PARSE_FALLBACK_MESSAGE: str = "type normalization polyglot parse failed; falling back" +TYPE_PARSE_FALLBACK_MESSAGE: str = "type normalization parse failed; falling back" diff --git a/src/sqlbuild/adapter/type_system/main/_native_normalize_type.py b/src/sqlbuild/adapter/type_system/main/_native_normalize_type.py index 77f1c5d714..97882418d7 100644 --- a/src/sqlbuild/adapter/type_system/main/_native_normalize_type.py +++ b/src/sqlbuild/adapter/type_system/main/_native_normalize_type.py @@ -15,7 +15,7 @@ def normalize_native_type(*, type_sql: str, dialect: TypeDialect | str | None) -> NormalizedType: - """Return the native normalization of one type, raising as Polyglot does.""" + """Return the native normalization of one type, raising ValueError for an unknown dialect.""" (normalized_name, family, precision, scale, length), parse_error = _native.normalize_type( type_sql, str(dialect or "generic") diff --git a/tests/goldens/python_oracle/contracts_generated_enforced_implicit_typed_nullability_and_dynamic_contracts.json b/tests/goldens/python_oracle/contracts_generated_enforced_implicit_typed_nullability_and_dynamic_contracts.json index 06b2d6254c..ef13a2abfb 100644 --- a/tests/goldens/python_oracle/contracts_generated_enforced_implicit_typed_nullability_and_dynamic_contracts.json +++ b/tests/goldens/python_oracle/contracts_generated_enforced_implicit_typed_nullability_and_dynamic_contracts.json @@ -426,7 +426,7 @@ "bigquery": "0fb1be349cccda275d77113d", "databricks": "0fb1be349cccda275d77113d", "duckdb": "0fb1be349cccda275d77113d", -"postgres": "0fb1be349cccda275d77113d", +"postgres": "a281bca2dfa9d33402e687f3", "snowflake": "0fb1be349cccda275d77113d", "tsql": "0fb1be349cccda275d77113d" } @@ -594,7 +594,7 @@ "bigquery": "6e5f3057275c88687658bb88", "databricks": "6e5f3057275c88687658bb88", "duckdb": "6e5f3057275c88687658bb88", -"postgres": "6e5f3057275c88687658bb88", +"postgres": "2a42e291118d5fa419ada6b8", "snowflake": "6e5f3057275c88687658bb88", "tsql": "6e5f3057275c88687658bb88" } diff --git a/tests/goldens/python_oracle/types_generated_seeded_scalar_parameterized_nested_padded_and_invalid_types.json b/tests/goldens/python_oracle/types_generated_seeded_scalar_parameterized_nested_padded_and_invalid_types.json index 1729e43aef..10981f8afe 100644 --- a/tests/goldens/python_oracle/types_generated_seeded_scalar_parameterized_nested_padded_and_invalid_types.json +++ b/tests/goldens/python_oracle/types_generated_seeded_scalar_parameterized_nested_padded_and_invalid_types.json @@ -44,8 +44,8 @@ "generic": "19a1a80ffc06169f32694de8", "motherduck": "901c5133ed47d4769286e860", "mysql": "19a1a80ffc06169f32694de8", -"postgres": "19a1a80ffc06169f32694de8", -"postgresql": "19a1a80ffc06169f32694de8", +"postgres": "ab1f5f46e69284778bd66d58", +"postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", "tsql": "19a1a80ffc06169f32694de8" } @@ -122,34 +122,34 @@ "type", { "DuckDB": "5c8b98f516cedefc237fa721", -"None": "5c8b98f516cedefc237fa721", -"bigquery": "5c8b98f516cedefc237fa721", -"databricks": "5c8b98f516cedefc237fa721", +"None": "81a45386abae5f6be0a027d0", +"bigquery": "09b7f1f9111bc2e0767cc059", +"databricks": "81a45386abae5f6be0a027d0", "duckdb": "5c8b98f516cedefc237fa721", -"generic": "5c8b98f516cedefc237fa721", +"generic": "81a45386abae5f6be0a027d0", "motherduck": "901c5133ed47d4769286e860", -"mysql": "5c8b98f516cedefc237fa721", +"mysql": "81a45386abae5f6be0a027d0", "postgres": "5c8b98f516cedefc237fa721", "postgresql": "5c8b98f516cedefc237fa721", "snowflake": "5c8b98f516cedefc237fa721", -"tsql": "5c8b98f516cedefc237fa721" +"tsql": "81a45386abae5f6be0a027d0" } ], [ "type", { -"DuckDB": "d00038134428e2ce04bc8e09", -"None": "ab7940999dc6f4c634249230", -"bigquery": "ab7940999dc6f4c634249230", -"databricks": "ab7940999dc6f4c634249230", -"duckdb": "d00038134428e2ce04bc8e09", -"generic": "ab7940999dc6f4c634249230", +"DuckDB": "6f9bc04f2e8c71a032255bac", +"None": "9ae2060b9503b8d23efaa736", +"bigquery": "9ae2060b9503b8d23efaa736", +"databricks": "9ae2060b9503b8d23efaa736", +"duckdb": "6f9bc04f2e8c71a032255bac", +"generic": "9ae2060b9503b8d23efaa736", "motherduck": "901c5133ed47d4769286e860", -"mysql": "ab7940999dc6f4c634249230", -"postgres": "ab7940999dc6f4c634249230", -"postgresql": "ab7940999dc6f4c634249230", -"snowflake": "d00038134428e2ce04bc8e09", -"tsql": "ab7940999dc6f4c634249230" +"mysql": "9ae2060b9503b8d23efaa736", +"postgres": "9ae2060b9503b8d23efaa736", +"postgresql": "9ae2060b9503b8d23efaa736", +"snowflake": "6f9bc04f2e8c71a032255bac", +"tsql": "9ae2060b9503b8d23efaa736" } ], [ @@ -191,7 +191,7 @@ { "DuckDB": "0f799a95a4f5ef502897f02c", "None": "0f799a95a4f5ef502897f02c", -"bigquery": "b1b23029bedaa16e2a6f8516", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "0f799a95a4f5ef502897f02c", "duckdb": "0f799a95a4f5ef502897f02c", "generic": "0f799a95a4f5ef502897f02c", @@ -248,8 +248,8 @@ "generic": "18db5afe2c1e80d1ee652b54", "motherduck": "901c5133ed47d4769286e860", "mysql": "18db5afe2c1e80d1ee652b54", -"postgres": "18db5afe2c1e80d1ee652b54", -"postgresql": "18db5afe2c1e80d1ee652b54", +"postgres": "47d64a5994d42a2b577166ea", +"postgresql": "47d64a5994d42a2b577166ea", "snowflake": "18db5afe2c1e80d1ee652b54", "tsql": "18db5afe2c1e80d1ee652b54" } @@ -259,14 +259,14 @@ { "DuckDB": "73151c1398b30e5c5d18f4ce", "None": "73151c1398b30e5c5d18f4ce", -"bigquery": "73151c1398b30e5c5d18f4ce", +"bigquery": "893ff554934dfc6f8253597b", "databricks": "73151c1398b30e5c5d18f4ce", "duckdb": "73151c1398b30e5c5d18f4ce", "generic": "73151c1398b30e5c5d18f4ce", "motherduck": "901c5133ed47d4769286e860", "mysql": "73151c1398b30e5c5d18f4ce", -"postgres": "73151c1398b30e5c5d18f4ce", -"postgresql": "73151c1398b30e5c5d18f4ce", +"postgres": "f68b7dbd74ec7c3b9f3066ef", +"postgresql": "f68b7dbd74ec7c3b9f3066ef", "snowflake": "73151c1398b30e5c5d18f4ce", "tsql": "73151c1398b30e5c5d18f4ce" } @@ -275,17 +275,17 @@ "type", { "DuckDB": "3147a9e06daa6de4a4df5abc", -"None": "3147a9e06daa6de4a4df5abc", -"bigquery": "f276e907770196606577c932", -"databricks": "3147a9e06daa6de4a4df5abc", +"None": "da0625706f12b764a6912278", +"bigquery": "b0477b36767a62145aa9158c", +"databricks": "da0625706f12b764a6912278", "duckdb": "3147a9e06daa6de4a4df5abc", -"generic": "3147a9e06daa6de4a4df5abc", +"generic": "da0625706f12b764a6912278", "motherduck": "901c5133ed47d4769286e860", -"mysql": "3147a9e06daa6de4a4df5abc", +"mysql": "da0625706f12b764a6912278", "postgres": "3147a9e06daa6de4a4df5abc", "postgresql": "3147a9e06daa6de4a4df5abc", "snowflake": "67b5874a2c569226c490211f", -"tsql": "3147a9e06daa6de4a4df5abc" +"tsql": "da0625706f12b764a6912278" } ], [ @@ -366,7 +366,7 @@ "duckdb": "a6c6f4aa4257d422cbf3f875", "generic": "a6c6f4aa4257d422cbf3f875", "motherduck": "901c5133ed47d4769286e860", -"mysql": "ea191a784359be345cd9ded6", +"mysql": "a6c6f4aa4257d422cbf3f875", "postgres": "a6c6f4aa4257d422cbf3f875", "postgresql": "a6c6f4aa4257d422cbf3f875", "snowflake": "a6c6f4aa4257d422cbf3f875", @@ -395,7 +395,7 @@ { "DuckDB": "0f799a95a4f5ef502897f02c", "None": "0f799a95a4f5ef502897f02c", -"bigquery": "b1b23029bedaa16e2a6f8516", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "0f799a95a4f5ef502897f02c", "duckdb": "0f799a95a4f5ef502897f02c", "generic": "0f799a95a4f5ef502897f02c", @@ -418,8 +418,8 @@ "generic": "e9b32e3b5a5505b32f33e169", "motherduck": "901c5133ed47d4769286e860", "mysql": "e9b32e3b5a5505b32f33e169", -"postgres": "e9b32e3b5a5505b32f33e169", -"postgresql": "e9b32e3b5a5505b32f33e169", +"postgres": "3fd123ac5ae610dffb11b495", +"postgresql": "3fd123ac5ae610dffb11b495", "snowflake": "e9b32e3b5a5505b32f33e169", "tsql": "e9b32e3b5a5505b32f33e169" } @@ -451,7 +451,7 @@ "duckdb": "a766fea06fc070b352976b0a", "generic": "a766fea06fc070b352976b0a", "motherduck": "901c5133ed47d4769286e860", -"mysql": "e4f0244beaa01667e80482b6", +"mysql": "a766fea06fc070b352976b0a", "postgres": "a766fea06fc070b352976b0a", "postgresql": "a766fea06fc070b352976b0a", "snowflake": "a766fea06fc070b352976b0a", @@ -565,14 +565,14 @@ { "DuckDB": "de89632e277dbc8c97115419", "None": "de89632e277dbc8c97115419", -"bigquery": "de89632e277dbc8c97115419", +"bigquery": "5abf4ec799a2ec32fb10da7e", "databricks": "de89632e277dbc8c97115419", "duckdb": "de89632e277dbc8c97115419", "generic": "de89632e277dbc8c97115419", "motherduck": "901c5133ed47d4769286e860", "mysql": "de89632e277dbc8c97115419", -"postgres": "de89632e277dbc8c97115419", -"postgresql": "de89632e277dbc8c97115419", +"postgres": "5abf4ec799a2ec32fb10da7e", +"postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", "tsql": "de89632e277dbc8c97115419" } @@ -582,7 +582,7 @@ { "DuckDB": "2e60720c3255d112fcd0938a", "None": "2e60720c3255d112fcd0938a", -"bigquery": "2e60720c3255d112fcd0938a", +"bigquery": "d65a7c279b0fc9d3804ee30e", "databricks": "2e60720c3255d112fcd0938a", "duckdb": "2e60720c3255d112fcd0938a", "generic": "2e60720c3255d112fcd0938a", @@ -605,8 +605,8 @@ "generic": "669084d1ea704ae5b8ee05c2", "motherduck": "901c5133ed47d4769286e860", "mysql": "669084d1ea704ae5b8ee05c2", -"postgres": "669084d1ea704ae5b8ee05c2", -"postgresql": "669084d1ea704ae5b8ee05c2", +"postgres": "93c1b5b5960e26479baf88d0", +"postgresql": "93c1b5b5960e26479baf88d0", "snowflake": "669084d1ea704ae5b8ee05c2", "tsql": "669084d1ea704ae5b8ee05c2" } @@ -615,17 +615,17 @@ "type", { "DuckDB": "c9cc0c1ce817773e19f141b5", -"None": "d7249dcf128bf2bb675aa0d9", -"bigquery": "d7249dcf128bf2bb675aa0d9", -"databricks": "f1640dc6a7437eae3cc39eec", +"None": "c70e0a464fcbb5e5a1f7a8b7", +"bigquery": "c70e0a464fcbb5e5a1f7a8b7", +"databricks": "c70e0a464fcbb5e5a1f7a8b7", "duckdb": "c9cc0c1ce817773e19f141b5", -"generic": "d7249dcf128bf2bb675aa0d9", +"generic": "c70e0a464fcbb5e5a1f7a8b7", "motherduck": "901c5133ed47d4769286e860", -"mysql": "d7249dcf128bf2bb675aa0d9", -"postgres": "d7249dcf128bf2bb675aa0d9", -"postgresql": "d7249dcf128bf2bb675aa0d9", +"mysql": "c70e0a464fcbb5e5a1f7a8b7", +"postgres": "c70e0a464fcbb5e5a1f7a8b7", +"postgresql": "c70e0a464fcbb5e5a1f7a8b7", "snowflake": "89091c4ddfa26794a7c15ab6", -"tsql": "d7249dcf128bf2bb675aa0d9" +"tsql": "c70e0a464fcbb5e5a1f7a8b7" } ], [ @@ -638,7 +638,7 @@ "duckdb": "e060b5a5a8e9774b580d58d3", "generic": "e060b5a5a8e9774b580d58d3", "motherduck": "901c5133ed47d4769286e860", -"mysql": "319a9fbb02ed503eb114164b", +"mysql": "e060b5a5a8e9774b580d58d3", "postgres": "e060b5a5a8e9774b580d58d3", "postgresql": "e060b5a5a8e9774b580d58d3", "snowflake": "e060b5a5a8e9774b580d58d3", @@ -718,14 +718,14 @@ { "DuckDB": "4b356542044f1267fcfc487a", "None": "4b356542044f1267fcfc487a", -"bigquery": "4b356542044f1267fcfc487a", +"bigquery": "a5656d70e208254d1d3e9b4f", "databricks": "4b356542044f1267fcfc487a", "duckdb": "4b356542044f1267fcfc487a", "generic": "4b356542044f1267fcfc487a", "motherduck": "901c5133ed47d4769286e860", "mysql": "4b356542044f1267fcfc487a", -"postgres": "4b356542044f1267fcfc487a", -"postgresql": "4b356542044f1267fcfc487a", +"postgres": "a5656d70e208254d1d3e9b4f", +"postgresql": "a5656d70e208254d1d3e9b4f", "snowflake": "4b356542044f1267fcfc487a", "tsql": "4b356542044f1267fcfc487a" } @@ -752,14 +752,14 @@ { "DuckDB": "08736485547ba339930697e9", "None": "08736485547ba339930697e9", -"bigquery": "08736485547ba339930697e9", +"bigquery": "837468174ef3e5cb4e4b4070", "databricks": "08736485547ba339930697e9", "duckdb": "08736485547ba339930697e9", "generic": "08736485547ba339930697e9", "motherduck": "901c5133ed47d4769286e860", "mysql": "08736485547ba339930697e9", -"postgres": "08736485547ba339930697e9", -"postgresql": "08736485547ba339930697e9", +"postgres": "eecd452b4f5f3f4133d0bd0a", +"postgresql": "eecd452b4f5f3f4133d0bd0a", "snowflake": "08736485547ba339930697e9", "tsql": "08736485547ba339930697e9" } @@ -792,8 +792,8 @@ "generic": "64fc778308b798a730b1d5c9", "motherduck": "901c5133ed47d4769286e860", "mysql": "64fc778308b798a730b1d5c9", -"postgres": "64fc778308b798a730b1d5c9", -"postgresql": "64fc778308b798a730b1d5c9", +"postgres": "c8ca7a940ad228dd836cad6f", +"postgresql": "c8ca7a940ad228dd836cad6f", "snowflake": "64fc778308b798a730b1d5c9", "tsql": "64fc778308b798a730b1d5c9" } @@ -871,14 +871,14 @@ { "DuckDB": "38b2ec94bf02be40007ce525", "None": "38b2ec94bf02be40007ce525", -"bigquery": "38b2ec94bf02be40007ce525", +"bigquery": "850deea990b52b82b36a9848", "databricks": "38b2ec94bf02be40007ce525", "duckdb": "38b2ec94bf02be40007ce525", "generic": "38b2ec94bf02be40007ce525", "motherduck": "901c5133ed47d4769286e860", "mysql": "38b2ec94bf02be40007ce525", -"postgres": "38b2ec94bf02be40007ce525", -"postgresql": "38b2ec94bf02be40007ce525", +"postgres": "850deea990b52b82b36a9848", +"postgresql": "850deea990b52b82b36a9848", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "38b2ec94bf02be40007ce525" } @@ -945,8 +945,8 @@ "generic": "de89632e277dbc8c97115419", "motherduck": "901c5133ed47d4769286e860", "mysql": "de89632e277dbc8c97115419", -"postgres": "de89632e277dbc8c97115419", -"postgresql": "de89632e277dbc8c97115419", +"postgres": "5abf4ec799a2ec32fb10da7e", +"postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", "tsql": "de89632e277dbc8c97115419" } @@ -1007,7 +1007,7 @@ { "DuckDB": "461923a0d34defd0c726d6e5", "None": "461923a0d34defd0c726d6e5", -"bigquery": "b5c4d5044b2a6e70633f0d0a", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "461923a0d34defd0c726d6e5", "duckdb": "461923a0d34defd0c726d6e5", "generic": "461923a0d34defd0c726d6e5", @@ -1058,14 +1058,14 @@ { "DuckDB": "e46d4be44ab9e500e9190d26", "None": "e46d4be44ab9e500e9190d26", -"bigquery": "e6ffdece2977be55da73b59a", +"bigquery": "c50a316d091c821866e4bfba", "databricks": "e46d4be44ab9e500e9190d26", "duckdb": "e46d4be44ab9e500e9190d26", "generic": "e46d4be44ab9e500e9190d26", "motherduck": "901c5133ed47d4769286e860", "mysql": "e46d4be44ab9e500e9190d26", -"postgres": "e46d4be44ab9e500e9190d26", -"postgresql": "e46d4be44ab9e500e9190d26", +"postgres": "251aba3fef503ce64556c701", +"postgresql": "251aba3fef503ce64556c701", "snowflake": "e46d4be44ab9e500e9190d26", "tsql": "e46d4be44ab9e500e9190d26" } @@ -1202,7 +1202,7 @@ "mysql": "b5c4d5044b2a6e70633f0d0a", "postgres": "b5c4d5044b2a6e70633f0d0a", "postgresql": "b5c4d5044b2a6e70633f0d0a", -"snowflake": "4e24a80b20b9dee0cbbfac17", +"snowflake": "8ff614b7306c0edbe36ce00b", "tsql": "b5c4d5044b2a6e70633f0d0a" } ], @@ -1216,7 +1216,7 @@ "duckdb": "7a8df655ee308ea4f929b9fd", "generic": "7a8df655ee308ea4f929b9fd", "motherduck": "901c5133ed47d4769286e860", -"mysql": "ac984ee30077ed3bb3c7c9d5", +"mysql": "7a8df655ee308ea4f929b9fd", "postgres": "7a8df655ee308ea4f929b9fd", "postgresql": "7a8df655ee308ea4f929b9fd", "snowflake": "7a8df655ee308ea4f929b9fd", @@ -1234,8 +1234,8 @@ "generic": "804fec6876d0b43935790142", "motherduck": "901c5133ed47d4769286e860", "mysql": "804fec6876d0b43935790142", -"postgres": "804fec6876d0b43935790142", -"postgresql": "804fec6876d0b43935790142", +"postgres": "191fc59c754706512337edc1", +"postgresql": "191fc59c754706512337edc1", "snowflake": "804fec6876d0b43935790142", "tsql": "804fec6876d0b43935790142" } @@ -1279,14 +1279,14 @@ { "DuckDB": "fe23594c781e79cbb0c8927a", "None": "fe23594c781e79cbb0c8927a", -"bigquery": "fe23594c781e79cbb0c8927a", +"bigquery": "8a3f094f18fb4047f0febf59", "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", "motherduck": "901c5133ed47d4769286e860", "mysql": "fe23594c781e79cbb0c8927a", -"postgres": "fe23594c781e79cbb0c8927a", -"postgresql": "fe23594c781e79cbb0c8927a", +"postgres": "8a3f094f18fb4047f0febf59", +"postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "fe23594c781e79cbb0c8927a" } @@ -1313,7 +1313,7 @@ { "DuckDB": "06aec7cca4b30489405665e2", "None": "06aec7cca4b30489405665e2", -"bigquery": "06aec7cca4b30489405665e2", +"bigquery": "96fb49e9038c050a39793710", "databricks": "06aec7cca4b30489405665e2", "duckdb": "06aec7cca4b30489405665e2", "generic": "06aec7cca4b30489405665e2", @@ -1449,7 +1449,7 @@ { "DuckDB": "b561884132dbe7e42b719110", "None": "b561884132dbe7e42b719110", -"bigquery": "b1b23029bedaa16e2a6f8516", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "b561884132dbe7e42b719110", "duckdb": "b561884132dbe7e42b719110", "generic": "b561884132dbe7e42b719110", @@ -1472,8 +1472,8 @@ "generic": "fb6eefdc9bed918e3ac82197", "motherduck": "901c5133ed47d4769286e860", "mysql": "fb6eefdc9bed918e3ac82197", -"postgres": "fb6eefdc9bed918e3ac82197", -"postgresql": "fb6eefdc9bed918e3ac82197", +"postgres": "4d3368f4985b5822bc4239af", +"postgresql": "4d3368f4985b5822bc4239af", "snowflake": "fb6eefdc9bed918e3ac82197", "tsql": "fb6eefdc9bed918e3ac82197" } @@ -1481,16 +1481,16 @@ [ "type", { -"DuckDB": "8698d724534891869075175e", -"None": "0017a88bb60ec3403c786ed8", +"DuckDB": "aa7afbd57560be67e5221a3b", +"None": "47e0816fecdd9e015d7aeb79", "bigquery": "47e0816fecdd9e015d7aeb79", -"databricks": "0017a88bb60ec3403c786ed8", -"duckdb": "8698d724534891869075175e", -"generic": "0017a88bb60ec3403c786ed8", +"databricks": "47e0816fecdd9e015d7aeb79", +"duckdb": "aa7afbd57560be67e5221a3b", +"generic": "47e0816fecdd9e015d7aeb79", "motherduck": "901c5133ed47d4769286e860", "mysql": "6c03a3450ea4742ee7396e3c", -"postgres": "8698d724534891869075175e", -"postgresql": "8698d724534891869075175e", +"postgres": "aa7afbd57560be67e5221a3b", +"postgresql": "aa7afbd57560be67e5221a3b", "snowflake": "e6f36041b1d9fb8fe3a18cbb", "tsql": "d1dc9a140dbe2eb1874c6ad7" } @@ -1534,14 +1534,14 @@ { "DuckDB": "d2c97d221599d038d9875873", "None": "d2c97d221599d038d9875873", -"bigquery": "d2c97d221599d038d9875873", +"bigquery": "55155766823b6a3a5e882fc2", "databricks": "d2c97d221599d038d9875873", "duckdb": "d2c97d221599d038d9875873", "generic": "d2c97d221599d038d9875873", "motherduck": "901c5133ed47d4769286e860", "mysql": "d2c97d221599d038d9875873", -"postgres": "d2c97d221599d038d9875873", -"postgresql": "d2c97d221599d038d9875873", +"postgres": "55155766823b6a3a5e882fc2", +"postgresql": "55155766823b6a3a5e882fc2", "snowflake": "d2c97d221599d038d9875873", "tsql": "d2c97d221599d038d9875873" } @@ -1557,8 +1557,8 @@ "generic": "e2879cf4770e7f48d2dd704a", "motherduck": "901c5133ed47d4769286e860", "mysql": "e2879cf4770e7f48d2dd704a", -"postgres": "e2879cf4770e7f48d2dd704a", -"postgresql": "e2879cf4770e7f48d2dd704a", +"postgres": "2d4aea8dec5521e258596f3d", +"postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", "tsql": "e2879cf4770e7f48d2dd704a" } @@ -1585,14 +1585,14 @@ { "DuckDB": "aab0389e2b5470136660bf8a", "None": "aab0389e2b5470136660bf8a", -"bigquery": "c9b810639795a7bfab91b98d", +"bigquery": "a428302e4848ffe877284c84", "databricks": "aab0389e2b5470136660bf8a", "duckdb": "aab0389e2b5470136660bf8a", "generic": "aab0389e2b5470136660bf8a", "motherduck": "901c5133ed47d4769286e860", "mysql": "aab0389e2b5470136660bf8a", -"postgres": "aab0389e2b5470136660bf8a", -"postgresql": "aab0389e2b5470136660bf8a", +"postgres": "f16105cbc61d547e6c08efc8", +"postgresql": "f16105cbc61d547e6c08efc8", "snowflake": "97a20344d811fb77c7690863", "tsql": "aab0389e2b5470136660bf8a" } @@ -1670,14 +1670,14 @@ { "DuckDB": "5d25d5e13f81700741695ed4", "None": "5d25d5e13f81700741695ed4", -"bigquery": "23ee196b7bd6039855a1460f", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "5d25d5e13f81700741695ed4", "duckdb": "5d25d5e13f81700741695ed4", "generic": "5d25d5e13f81700741695ed4", "motherduck": "901c5133ed47d4769286e860", "mysql": "5d25d5e13f81700741695ed4", -"postgres": "5d25d5e13f81700741695ed4", -"postgresql": "5d25d5e13f81700741695ed4", +"postgres": "62131fb5b065f5de2e74a0f1", +"postgresql": "62131fb5b065f5de2e74a0f1", "snowflake": "5d25d5e13f81700741695ed4", "tsql": "5d25d5e13f81700741695ed4" } @@ -1710,8 +1710,8 @@ "generic": "e2879cf4770e7f48d2dd704a", "motherduck": "901c5133ed47d4769286e860", "mysql": "e2879cf4770e7f48d2dd704a", -"postgres": "e2879cf4770e7f48d2dd704a", -"postgresql": "e2879cf4770e7f48d2dd704a", +"postgres": "2d4aea8dec5521e258596f3d", +"postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", "tsql": "e2879cf4770e7f48d2dd704a" } @@ -1721,14 +1721,14 @@ { "DuckDB": "ee47e1424cb01deb0cf51760", "None": "ee47e1424cb01deb0cf51760", -"bigquery": "ee47e1424cb01deb0cf51760", +"bigquery": "893ff554934dfc6f8253597b", "databricks": "ee47e1424cb01deb0cf51760", "duckdb": "ee47e1424cb01deb0cf51760", "generic": "ee47e1424cb01deb0cf51760", "motherduck": "901c5133ed47d4769286e860", "mysql": "ee47e1424cb01deb0cf51760", -"postgres": "ee47e1424cb01deb0cf51760", -"postgresql": "ee47e1424cb01deb0cf51760", +"postgres": "3c1521654088dd54d0dc9ead", +"postgresql": "3c1521654088dd54d0dc9ead", "snowflake": "ee47e1424cb01deb0cf51760", "tsql": "ee47e1424cb01deb0cf51760" } @@ -1743,7 +1743,7 @@ "duckdb": "1655128a72b1db9af6a89df3", "generic": "1655128a72b1db9af6a89df3", "motherduck": "901c5133ed47d4769286e860", -"mysql": "b4c316e81db1cb6a277b5d27", +"mysql": "1655128a72b1db9af6a89df3", "postgres": "1655128a72b1db9af6a89df3", "postgresql": "1655128a72b1db9af6a89df3", "snowflake": "1655128a72b1db9af6a89df3", @@ -1754,17 +1754,17 @@ "type", { "DuckDB": "f2ed52f6bf46b4db9343551c", -"None": "f2ed52f6bf46b4db9343551c", -"bigquery": "f10bdd4d40eb495cbcecd31d", -"databricks": "f2ed52f6bf46b4db9343551c", +"None": "3f998a9ab68d8b2514840489", +"bigquery": "1e75e5227bea0ae294ac7af1", +"databricks": "3f998a9ab68d8b2514840489", "duckdb": "f2ed52f6bf46b4db9343551c", -"generic": "f2ed52f6bf46b4db9343551c", +"generic": "3f998a9ab68d8b2514840489", "motherduck": "901c5133ed47d4769286e860", -"mysql": "f2ed52f6bf46b4db9343551c", -"postgres": "f2ed52f6bf46b4db9343551c", -"postgresql": "f2ed52f6bf46b4db9343551c", +"mysql": "3f998a9ab68d8b2514840489", +"postgres": "7ce41cc5375b618de22b6a4e", +"postgresql": "7ce41cc5375b618de22b6a4e", "snowflake": "f2ed52f6bf46b4db9343551c", -"tsql": "f2ed52f6bf46b4db9343551c" +"tsql": "3f998a9ab68d8b2514840489" } ], [ @@ -1812,8 +1812,8 @@ "generic": "3e611643f0beab4953c4e09b", "motherduck": "901c5133ed47d4769286e860", "mysql": "3e611643f0beab4953c4e09b", -"postgres": "3e611643f0beab4953c4e09b", -"postgresql": "3e611643f0beab4953c4e09b", +"postgres": "cae477ce59c72869f37a9a40", +"postgresql": "cae477ce59c72869f37a9a40", "snowflake": "3e611643f0beab4953c4e09b", "tsql": "3e611643f0beab4953c4e09b" } @@ -1829,8 +1829,8 @@ "generic": "e2879cf4770e7f48d2dd704a", "motherduck": "901c5133ed47d4769286e860", "mysql": "e2879cf4770e7f48d2dd704a", -"postgres": "e2879cf4770e7f48d2dd704a", -"postgresql": "e2879cf4770e7f48d2dd704a", +"postgres": "2d4aea8dec5521e258596f3d", +"postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", "tsql": "e2879cf4770e7f48d2dd704a" } @@ -1840,7 +1840,7 @@ { "DuckDB": "62368fc15cefd5807807b209", "None": "62368fc15cefd5807807b209", -"bigquery": "e6ffdece2977be55da73b59a", +"bigquery": "c50a316d091c821866e4bfba", "databricks": "62368fc15cefd5807807b209", "duckdb": "62368fc15cefd5807807b209", "generic": "62368fc15cefd5807807b209", @@ -1908,14 +1908,14 @@ { "DuckDB": "de89632e277dbc8c97115419", "None": "de89632e277dbc8c97115419", -"bigquery": "de89632e277dbc8c97115419", +"bigquery": "5abf4ec799a2ec32fb10da7e", "databricks": "de89632e277dbc8c97115419", "duckdb": "de89632e277dbc8c97115419", "generic": "de89632e277dbc8c97115419", "motherduck": "901c5133ed47d4769286e860", "mysql": "de89632e277dbc8c97115419", -"postgres": "de89632e277dbc8c97115419", -"postgresql": "de89632e277dbc8c97115419", +"postgres": "5abf4ec799a2ec32fb10da7e", +"postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", "tsql": "de89632e277dbc8c97115419" } @@ -1926,11 +1926,11 @@ "DuckDB": "14cd6e78d75f61b2bd1bbd71", "None": "14cd6e78d75f61b2bd1bbd71", "bigquery": "14cd6e78d75f61b2bd1bbd71", -"databricks": "73504f31156e8e661c1fb26c", +"databricks": "14cd6e78d75f61b2bd1bbd71", "duckdb": "14cd6e78d75f61b2bd1bbd71", "generic": "14cd6e78d75f61b2bd1bbd71", "motherduck": "901c5133ed47d4769286e860", -"mysql": "73504f31156e8e661c1fb26c", +"mysql": "14cd6e78d75f61b2bd1bbd71", "postgres": "14cd6e78d75f61b2bd1bbd71", "postgresql": "14cd6e78d75f61b2bd1bbd71", "snowflake": "14cd6e78d75f61b2bd1bbd71", @@ -1976,7 +1976,7 @@ { "DuckDB": "4f8af4650ce9c18ed0cb0bca", "None": "4f8af4650ce9c18ed0cb0bca", -"bigquery": "4f8af4650ce9c18ed0cb0bca", +"bigquery": "307965c44698c3db5bfa1db9", "databricks": "4f8af4650ce9c18ed0cb0bca", "duckdb": "4f8af4650ce9c18ed0cb0bca", "generic": "4f8af4650ce9c18ed0cb0bca", @@ -1993,7 +1993,7 @@ { "DuckDB": "cbd04e906c540e91d7c8ea88", "None": "cbd04e906c540e91d7c8ea88", -"bigquery": "b5c4d5044b2a6e70633f0d0a", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "cbd04e906c540e91d7c8ea88", "duckdb": "cbd04e906c540e91d7c8ea88", "generic": "cbd04e906c540e91d7c8ea88", @@ -2009,17 +2009,17 @@ "type", { "DuckDB": "70fb4728c3adcf6dbe4975d5", -"None": "70fb4728c3adcf6dbe4975d5", -"bigquery": "70fb4728c3adcf6dbe4975d5", -"databricks": "70fb4728c3adcf6dbe4975d5", +"None": "fb4b6f5101f973d16ea8d1f3", +"bigquery": "fb4b6f5101f973d16ea8d1f3", +"databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", -"generic": "70fb4728c3adcf6dbe4975d5", +"generic": "fb4b6f5101f973d16ea8d1f3", "motherduck": "901c5133ed47d4769286e860", -"mysql": "70fb4728c3adcf6dbe4975d5", +"mysql": "fb4b6f5101f973d16ea8d1f3", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", -"tsql": "70fb4728c3adcf6dbe4975d5" +"tsql": "fb4b6f5101f973d16ea8d1f3" } ], [ @@ -2033,8 +2033,8 @@ "generic": "8b8d5f75ba6dec68b4cebc6b", "motherduck": "901c5133ed47d4769286e860", "mysql": "8b8d5f75ba6dec68b4cebc6b", -"postgres": "8b8d5f75ba6dec68b4cebc6b", -"postgresql": "8b8d5f75ba6dec68b4cebc6b", +"postgres": "7914189ff18b0e70ddf7d042", +"postgresql": "7914189ff18b0e70ddf7d042", "snowflake": "8b8d5f75ba6dec68b4cebc6b", "tsql": "8b8d5f75ba6dec68b4cebc6b" } @@ -2044,7 +2044,7 @@ { "DuckDB": "aae2b1fe6f2d9807ecde412b", "None": "aae2b1fe6f2d9807ecde412b", -"bigquery": "e1c36a5cdd2427ff2b089ac9", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "aae2b1fe6f2d9807ecde412b", "duckdb": "aae2b1fe6f2d9807ecde412b", "generic": "aae2b1fe6f2d9807ecde412b", @@ -2067,8 +2067,8 @@ "generic": "38b2ec94bf02be40007ce525", "motherduck": "901c5133ed47d4769286e860", "mysql": "38b2ec94bf02be40007ce525", -"postgres": "38b2ec94bf02be40007ce525", -"postgresql": "38b2ec94bf02be40007ce525", +"postgres": "850deea990b52b82b36a9848", +"postgresql": "850deea990b52b82b36a9848", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "38b2ec94bf02be40007ce525" } @@ -2101,8 +2101,8 @@ "generic": "bce60580ef3a1dc4ce569fff", "motherduck": "901c5133ed47d4769286e860", "mysql": "bce60580ef3a1dc4ce569fff", -"postgres": "bce60580ef3a1dc4ce569fff", -"postgresql": "bce60580ef3a1dc4ce569fff", +"postgres": "eee236ea1427bdddbaf088ac", +"postgresql": "eee236ea1427bdddbaf088ac", "snowflake": "bce60580ef3a1dc4ce569fff", "tsql": "bce60580ef3a1dc4ce569fff" } @@ -2118,8 +2118,8 @@ "generic": "19e127c23b4598727a2d8ceb", "motherduck": "901c5133ed47d4769286e860", "mysql": "aed0419e7f2ce7c0a8892248", -"postgres": "19e127c23b4598727a2d8ceb", -"postgresql": "19e127c23b4598727a2d8ceb", +"postgres": "111af35848fe500ec2ae54b2", +"postgresql": "111af35848fe500ec2ae54b2", "snowflake": "aed0419e7f2ce7c0a8892248", "tsql": "19e127c23b4598727a2d8ceb" } @@ -2186,8 +2186,8 @@ "generic": "c6106f30f23f392e17eadef4", "motherduck": "901c5133ed47d4769286e860", "mysql": "c6106f30f23f392e17eadef4", -"postgres": "c6106f30f23f392e17eadef4", -"postgresql": "c6106f30f23f392e17eadef4", +"postgres": "f43dac38f34a1c0618a3f812", +"postgresql": "f43dac38f34a1c0618a3f812", "snowflake": "c6106f30f23f392e17eadef4", "tsql": "c6106f30f23f392e17eadef4" } @@ -2265,14 +2265,14 @@ { "DuckDB": "0dd2a0d5610068e51e271b9f", "None": "0dd2a0d5610068e51e271b9f", -"bigquery": "0dd2a0d5610068e51e271b9f", +"bigquery": "893ff554934dfc6f8253597b", "databricks": "0dd2a0d5610068e51e271b9f", "duckdb": "0dd2a0d5610068e51e271b9f", "generic": "0dd2a0d5610068e51e271b9f", "motherduck": "901c5133ed47d4769286e860", "mysql": "0dd2a0d5610068e51e271b9f", -"postgres": "0dd2a0d5610068e51e271b9f", -"postgresql": "0dd2a0d5610068e51e271b9f", +"postgres": "b4df93b58afcfa82db9f544e", +"postgresql": "b4df93b58afcfa82db9f544e", "snowflake": "0dd2a0d5610068e51e271b9f", "tsql": "0dd2a0d5610068e51e271b9f" } @@ -2333,14 +2333,14 @@ { "DuckDB": "de89632e277dbc8c97115419", "None": "de89632e277dbc8c97115419", -"bigquery": "de89632e277dbc8c97115419", +"bigquery": "5abf4ec799a2ec32fb10da7e", "databricks": "de89632e277dbc8c97115419", "duckdb": "de89632e277dbc8c97115419", "generic": "de89632e277dbc8c97115419", "motherduck": "901c5133ed47d4769286e860", "mysql": "de89632e277dbc8c97115419", -"postgres": "de89632e277dbc8c97115419", -"postgresql": "de89632e277dbc8c97115419", +"postgres": "5abf4ec799a2ec32fb10da7e", +"postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", "tsql": "de89632e277dbc8c97115419" } @@ -2365,14 +2365,14 @@ [ "type", { -"DuckDB": "285056becb5d467811be7cd6", +"DuckDB": "0712c33a016f7dd6a37ee011", "None": "285056becb5d467811be7cd6", "bigquery": "0712c33a016f7dd6a37ee011", "databricks": "285056becb5d467811be7cd6", -"duckdb": "285056becb5d467811be7cd6", +"duckdb": "0712c33a016f7dd6a37ee011", "generic": "285056becb5d467811be7cd6", "motherduck": "901c5133ed47d4769286e860", -"mysql": "fc10fc558d906a80a6404b7f", +"mysql": "285056becb5d467811be7cd6", "postgres": "285056becb5d467811be7cd6", "postgresql": "285056becb5d467811be7cd6", "snowflake": "0712c33a016f7dd6a37ee011", @@ -2389,7 +2389,7 @@ "duckdb": "1655128a72b1db9af6a89df3", "generic": "1655128a72b1db9af6a89df3", "motherduck": "901c5133ed47d4769286e860", -"mysql": "b4c316e81db1cb6a277b5d27", +"mysql": "1655128a72b1db9af6a89df3", "postgres": "1655128a72b1db9af6a89df3", "postgresql": "1655128a72b1db9af6a89df3", "snowflake": "1655128a72b1db9af6a89df3", @@ -2468,17 +2468,17 @@ "type", { "DuckDB": "d81d8c19074af0e39239164d", -"None": "1fec266134381c760a254d16", -"bigquery": "1fec266134381c760a254d16", -"databricks": "1fec266134381c760a254d16", +"None": "a52676af0c6b750acc33f021", +"bigquery": "1e75e5227bea0ae294ac7af1", +"databricks": "a52676af0c6b750acc33f021", "duckdb": "d81d8c19074af0e39239164d", -"generic": "1fec266134381c760a254d16", +"generic": "a52676af0c6b750acc33f021", "motherduck": "901c5133ed47d4769286e860", -"mysql": "1fec266134381c760a254d16", -"postgres": "1fec266134381c760a254d16", -"postgresql": "1fec266134381c760a254d16", +"mysql": "a52676af0c6b750acc33f021", +"postgres": "a52676af0c6b750acc33f021", +"postgresql": "a52676af0c6b750acc33f021", "snowflake": "1fec266134381c760a254d16", -"tsql": "1fec266134381c760a254d16" +"tsql": "a52676af0c6b750acc33f021" } ], [ @@ -2503,14 +2503,14 @@ { "DuckDB": "4928771fd9e503a30457d160", "None": "4928771fd9e503a30457d160", -"bigquery": "4928771fd9e503a30457d160", +"bigquery": "67d251c042daee0bb2bf7351", "databricks": "4928771fd9e503a30457d160", "duckdb": "4928771fd9e503a30457d160", "generic": "4928771fd9e503a30457d160", "motherduck": "901c5133ed47d4769286e860", "mysql": "4928771fd9e503a30457d160", -"postgres": "4928771fd9e503a30457d160", -"postgresql": "4928771fd9e503a30457d160", +"postgres": "50ddfb168c08b09892261e82", +"postgresql": "50ddfb168c08b09892261e82", "snowflake": "4928771fd9e503a30457d160", "tsql": "4928771fd9e503a30457d160" } @@ -2555,11 +2555,11 @@ "DuckDB": "533aa8bbcadbe01c5e7a5ba7", "None": "533aa8bbcadbe01c5e7a5ba7", "bigquery": "533aa8bbcadbe01c5e7a5ba7", -"databricks": "e8a1e0270aa8ec0f1dc6e35e", +"databricks": "533aa8bbcadbe01c5e7a5ba7", "duckdb": "533aa8bbcadbe01c5e7a5ba7", "generic": "533aa8bbcadbe01c5e7a5ba7", "motherduck": "901c5133ed47d4769286e860", -"mysql": "c619dae9f0c9870858ec6530", +"mysql": "533aa8bbcadbe01c5e7a5ba7", "postgres": "533aa8bbcadbe01c5e7a5ba7", "postgresql": "533aa8bbcadbe01c5e7a5ba7", "snowflake": "533aa8bbcadbe01c5e7a5ba7", @@ -2577,8 +2577,8 @@ "generic": "e2879cf4770e7f48d2dd704a", "motherduck": "901c5133ed47d4769286e860", "mysql": "e2879cf4770e7f48d2dd704a", -"postgres": "e2879cf4770e7f48d2dd704a", -"postgresql": "e2879cf4770e7f48d2dd704a", +"postgres": "2d4aea8dec5521e258596f3d", +"postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", "tsql": "e2879cf4770e7f48d2dd704a" } @@ -2622,7 +2622,7 @@ { "DuckDB": "8203bd5901a8ec71ff7c103e", "None": "8203bd5901a8ec71ff7c103e", -"bigquery": "2a0918be53051fe629d9ff8f", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "8203bd5901a8ec71ff7c103e", "duckdb": "8203bd5901a8ec71ff7c103e", "generic": "8203bd5901a8ec71ff7c103e", @@ -2672,17 +2672,17 @@ "type", { "DuckDB": "70fb4728c3adcf6dbe4975d5", -"None": "70fb4728c3adcf6dbe4975d5", -"bigquery": "70fb4728c3adcf6dbe4975d5", -"databricks": "70fb4728c3adcf6dbe4975d5", +"None": "fb4b6f5101f973d16ea8d1f3", +"bigquery": "fb4b6f5101f973d16ea8d1f3", +"databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", -"generic": "70fb4728c3adcf6dbe4975d5", +"generic": "fb4b6f5101f973d16ea8d1f3", "motherduck": "901c5133ed47d4769286e860", -"mysql": "70fb4728c3adcf6dbe4975d5", +"mysql": "fb4b6f5101f973d16ea8d1f3", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", -"tsql": "70fb4728c3adcf6dbe4975d5" +"tsql": "fb4b6f5101f973d16ea8d1f3" } ], [ @@ -2696,8 +2696,8 @@ "generic": "19a1a80ffc06169f32694de8", "motherduck": "901c5133ed47d4769286e860", "mysql": "19a1a80ffc06169f32694de8", -"postgres": "19a1a80ffc06169f32694de8", -"postgresql": "19a1a80ffc06169f32694de8", +"postgres": "ab1f5f46e69284778bd66d58", +"postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", "tsql": "19a1a80ffc06169f32694de8" } @@ -2792,7 +2792,7 @@ { "DuckDB": "e08a957777bd278f921ca0ee", "None": "e08a957777bd278f921ca0ee", -"bigquery": "b1b23029bedaa16e2a6f8516", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "e08a957777bd278f921ca0ee", "duckdb": "e08a957777bd278f921ca0ee", "generic": "e08a957777bd278f921ca0ee", @@ -2866,8 +2866,8 @@ "generic": "d23bf939a7eb8e49c7e6867a", "motherduck": "901c5133ed47d4769286e860", "mysql": "d23bf939a7eb8e49c7e6867a", -"postgres": "d23bf939a7eb8e49c7e6867a", -"postgresql": "d23bf939a7eb8e49c7e6867a", +"postgres": "71f4f8fbb5f5c537d895ceb4", +"postgresql": "71f4f8fbb5f5c537d895ceb4", "snowflake": "d23bf939a7eb8e49c7e6867a", "tsql": "d23bf939a7eb8e49c7e6867a" } @@ -2928,7 +2928,7 @@ { "DuckDB": "28eb0d6de9724959ce5ba43e", "None": "28eb0d6de9724959ce5ba43e", -"bigquery": "f28a96f0eb38ad8874f6ec5d", +"bigquery": "5c28019dce48a74708c90a76", "databricks": "28eb0d6de9724959ce5ba43e", "duckdb": "28eb0d6de9724959ce5ba43e", "generic": "28eb0d6de9724959ce5ba43e", @@ -3029,34 +3029,34 @@ "type", { "DuckDB": "70fb4728c3adcf6dbe4975d5", -"None": "70fb4728c3adcf6dbe4975d5", -"bigquery": "70fb4728c3adcf6dbe4975d5", -"databricks": "70fb4728c3adcf6dbe4975d5", +"None": "fb4b6f5101f973d16ea8d1f3", +"bigquery": "fb4b6f5101f973d16ea8d1f3", +"databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", -"generic": "70fb4728c3adcf6dbe4975d5", +"generic": "fb4b6f5101f973d16ea8d1f3", "motherduck": "901c5133ed47d4769286e860", -"mysql": "70fb4728c3adcf6dbe4975d5", +"mysql": "fb4b6f5101f973d16ea8d1f3", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", -"tsql": "70fb4728c3adcf6dbe4975d5" +"tsql": "fb4b6f5101f973d16ea8d1f3" } ], [ "type", { "DuckDB": "3af3f9124a8649e4f72030c1", -"None": "3af3f9124a8649e4f72030c1", +"None": "8191fff105ab0743bf9ffd39", "bigquery": "8191fff105ab0743bf9ffd39", "databricks": "8191fff105ab0743bf9ffd39", "duckdb": "3af3f9124a8649e4f72030c1", -"generic": "3af3f9124a8649e4f72030c1", +"generic": "8191fff105ab0743bf9ffd39", "motherduck": "901c5133ed47d4769286e860", "mysql": "8191fff105ab0743bf9ffd39", -"postgres": "3af3f9124a8649e4f72030c1", -"postgresql": "3af3f9124a8649e4f72030c1", +"postgres": "23cc34d931e18f55168723f0", +"postgresql": "23cc34d931e18f55168723f0", "snowflake": "3af3f9124a8649e4f72030c1", -"tsql": "3af3f9124a8649e4f72030c1" +"tsql": "8191fff105ab0743bf9ffd39" } ], [ @@ -3064,14 +3064,14 @@ { "DuckDB": "e46d4be44ab9e500e9190d26", "None": "e46d4be44ab9e500e9190d26", -"bigquery": "e6ffdece2977be55da73b59a", +"bigquery": "c50a316d091c821866e4bfba", "databricks": "e46d4be44ab9e500e9190d26", "duckdb": "e46d4be44ab9e500e9190d26", "generic": "e46d4be44ab9e500e9190d26", "motherduck": "901c5133ed47d4769286e860", "mysql": "e46d4be44ab9e500e9190d26", -"postgres": "e46d4be44ab9e500e9190d26", -"postgresql": "e46d4be44ab9e500e9190d26", +"postgres": "251aba3fef503ce64556c701", +"postgresql": "251aba3fef503ce64556c701", "snowflake": "e46d4be44ab9e500e9190d26", "tsql": "e46d4be44ab9e500e9190d26" } @@ -3081,7 +3081,7 @@ { "DuckDB": "5c28019dce48a74708c90a76", "None": "5c28019dce48a74708c90a76", -"bigquery": "f28a96f0eb38ad8874f6ec5d", +"bigquery": "70fb4728c3adcf6dbe4975d5", "databricks": "5c28019dce48a74708c90a76", "duckdb": "5c28019dce48a74708c90a76", "generic": "5c28019dce48a74708c90a76", @@ -3132,14 +3132,14 @@ { "DuckDB": "b561884132dbe7e42b719110", "None": "b561884132dbe7e42b719110", -"bigquery": "b1b23029bedaa16e2a6f8516", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "b561884132dbe7e42b719110", "duckdb": "b561884132dbe7e42b719110", "generic": "b561884132dbe7e42b719110", "motherduck": "901c5133ed47d4769286e860", "mysql": "b561884132dbe7e42b719110", -"postgres": "b561884132dbe7e42b719110", -"postgresql": "b561884132dbe7e42b719110", +"postgres": "5aa51d84ac5e6c7a9306f348", +"postgresql": "5aa51d84ac5e6c7a9306f348", "snowflake": "b561884132dbe7e42b719110", "tsql": "b561884132dbe7e42b719110" } @@ -3182,17 +3182,17 @@ "type", { "DuckDB": "d81d8c19074af0e39239164d", -"None": "1fec266134381c760a254d16", -"bigquery": "1fec266134381c760a254d16", -"databricks": "1fec266134381c760a254d16", +"None": "a52676af0c6b750acc33f021", +"bigquery": "1e75e5227bea0ae294ac7af1", +"databricks": "a52676af0c6b750acc33f021", "duckdb": "d81d8c19074af0e39239164d", -"generic": "1fec266134381c760a254d16", +"generic": "a52676af0c6b750acc33f021", "motherduck": "901c5133ed47d4769286e860", -"mysql": "1fec266134381c760a254d16", -"postgres": "1fec266134381c760a254d16", -"postgresql": "1fec266134381c760a254d16", +"mysql": "a52676af0c6b750acc33f021", +"postgres": "a52676af0c6b750acc33f021", +"postgresql": "a52676af0c6b750acc33f021", "snowflake": "1fec266134381c760a254d16", -"tsql": "1fec266134381c760a254d16" +"tsql": "a52676af0c6b750acc33f021" } ], [ @@ -3200,7 +3200,7 @@ { "DuckDB": "62368fc15cefd5807807b209", "None": "62368fc15cefd5807807b209", -"bigquery": "e6ffdece2977be55da73b59a", +"bigquery": "c50a316d091c821866e4bfba", "databricks": "62368fc15cefd5807807b209", "duckdb": "62368fc15cefd5807807b209", "generic": "62368fc15cefd5807807b209", @@ -3290,7 +3290,7 @@ "duckdb": "c7b697bbfa82dd2821ad64e9", "generic": "c7b697bbfa82dd2821ad64e9", "motherduck": "901c5133ed47d4769286e860", -"mysql": "dcb00b7053ffe69fe3ce99cd", +"mysql": "c7b697bbfa82dd2821ad64e9", "postgres": "c7b697bbfa82dd2821ad64e9", "postgresql": "c7b697bbfa82dd2821ad64e9", "snowflake": "c7b697bbfa82dd2821ad64e9", @@ -3386,17 +3386,17 @@ "type", { "DuckDB": "fa45b7e4a9ef3b5a9c5781af", -"None": "fa45b7e4a9ef3b5a9c5781af", -"bigquery": "fa45b7e4a9ef3b5a9c5781af", -"databricks": "fa45b7e4a9ef3b5a9c5781af", +"None": "ebe2513b6cb4001e42cc464b", +"bigquery": "0c43dc837a50933667588ef1", +"databricks": "ebe2513b6cb4001e42cc464b", "duckdb": "fa45b7e4a9ef3b5a9c5781af", -"generic": "fa45b7e4a9ef3b5a9c5781af", +"generic": "ebe2513b6cb4001e42cc464b", "motherduck": "901c5133ed47d4769286e860", -"mysql": "fa45b7e4a9ef3b5a9c5781af", +"mysql": "ebe2513b6cb4001e42cc464b", "postgres": "fa45b7e4a9ef3b5a9c5781af", "postgresql": "fa45b7e4a9ef3b5a9c5781af", "snowflake": "fa45b7e4a9ef3b5a9c5781af", -"tsql": "fa45b7e4a9ef3b5a9c5781af" +"tsql": "ebe2513b6cb4001e42cc464b" } ], [ @@ -3489,7 +3489,7 @@ { "DuckDB": "0f799a95a4f5ef502897f02c", "None": "0f799a95a4f5ef502897f02c", -"bigquery": "b1b23029bedaa16e2a6f8516", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "0f799a95a4f5ef502897f02c", "duckdb": "0f799a95a4f5ef502897f02c", "generic": "0f799a95a4f5ef502897f02c", @@ -3523,14 +3523,14 @@ { "DuckDB": "fe23594c781e79cbb0c8927a", "None": "fe23594c781e79cbb0c8927a", -"bigquery": "fe23594c781e79cbb0c8927a", +"bigquery": "8a3f094f18fb4047f0febf59", "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", "motherduck": "901c5133ed47d4769286e860", "mysql": "fe23594c781e79cbb0c8927a", -"postgres": "fe23594c781e79cbb0c8927a", -"postgresql": "fe23594c781e79cbb0c8927a", +"postgres": "8a3f094f18fb4047f0febf59", +"postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "fe23594c781e79cbb0c8927a" } @@ -3540,7 +3540,7 @@ { "DuckDB": "855e2727b4a8cb5d2711f102", "None": "855e2727b4a8cb5d2711f102", -"bigquery": "855e2727b4a8cb5d2711f102", +"bigquery": "01e56f2a28ba32841a6d614a", "databricks": "855e2727b4a8cb5d2711f102", "duckdb": "855e2727b4a8cb5d2711f102", "generic": "855e2727b4a8cb5d2711f102", @@ -3556,17 +3556,17 @@ "type", { "DuckDB": "1ebac487198b6c90b027faab", -"None": "1ebac487198b6c90b027faab", -"bigquery": "a428302e4848ffe877284c84", -"databricks": "1ebac487198b6c90b027faab", +"None": "79278352c17c9de0f5a28af5", +"bigquery": "c9b810639795a7bfab91b98d", +"databricks": "79278352c17c9de0f5a28af5", "duckdb": "1ebac487198b6c90b027faab", -"generic": "1ebac487198b6c90b027faab", +"generic": "79278352c17c9de0f5a28af5", "motherduck": "901c5133ed47d4769286e860", -"mysql": "1ebac487198b6c90b027faab", -"postgres": "1ebac487198b6c90b027faab", -"postgresql": "1ebac487198b6c90b027faab", -"snowflake": "652ff8b21f638eac8fdc46c5", -"tsql": "c692d974ee3f362ecb38f27c" +"mysql": "79278352c17c9de0f5a28af5", +"postgres": "79278352c17c9de0f5a28af5", +"postgresql": "79278352c17c9de0f5a28af5", +"snowflake": "97a20344d811fb77c7690863", +"tsql": "79278352c17c9de0f5a28af5" } ], [ @@ -3575,11 +3575,11 @@ "DuckDB": "9c2cff8be7225cf79883ca65", "None": "9c2cff8be7225cf79883ca65", "bigquery": "9c2cff8be7225cf79883ca65", -"databricks": "901ac89f8d9ec18bd3a61e4b", +"databricks": "9c2cff8be7225cf79883ca65", "duckdb": "9c2cff8be7225cf79883ca65", "generic": "9c2cff8be7225cf79883ca65", "motherduck": "901c5133ed47d4769286e860", -"mysql": "901ac89f8d9ec18bd3a61e4b", +"mysql": "9c2cff8be7225cf79883ca65", "postgres": "9c2cff8be7225cf79883ca65", "postgresql": "9c2cff8be7225cf79883ca65", "snowflake": "9c2cff8be7225cf79883ca65", @@ -3675,17 +3675,17 @@ "type", { "DuckDB": "3147a9e06daa6de4a4df5abc", -"None": "3147a9e06daa6de4a4df5abc", -"bigquery": "f276e907770196606577c932", -"databricks": "3147a9e06daa6de4a4df5abc", +"None": "da0625706f12b764a6912278", +"bigquery": "b0477b36767a62145aa9158c", +"databricks": "da0625706f12b764a6912278", "duckdb": "3147a9e06daa6de4a4df5abc", -"generic": "3147a9e06daa6de4a4df5abc", +"generic": "da0625706f12b764a6912278", "motherduck": "901c5133ed47d4769286e860", -"mysql": "3147a9e06daa6de4a4df5abc", +"mysql": "da0625706f12b764a6912278", "postgres": "3147a9e06daa6de4a4df5abc", "postgresql": "3147a9e06daa6de4a4df5abc", "snowflake": "67b5874a2c569226c490211f", -"tsql": "3147a9e06daa6de4a4df5abc" +"tsql": "da0625706f12b764a6912278" } ], [ @@ -3744,7 +3744,7 @@ { "DuckDB": "aae2b1fe6f2d9807ecde412b", "None": "aae2b1fe6f2d9807ecde412b", -"bigquery": "e1c36a5cdd2427ff2b089ac9", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "aae2b1fe6f2d9807ecde412b", "duckdb": "aae2b1fe6f2d9807ecde412b", "generic": "aae2b1fe6f2d9807ecde412b", @@ -3817,7 +3817,7 @@ "duckdb": "9d0ae138704cb97e3fe4d105", "generic": "9d0ae138704cb97e3fe4d105", "motherduck": "901c5133ed47d4769286e860", -"mysql": "6d07fc2f489d414e7eb90262", +"mysql": "9d0ae138704cb97e3fe4d105", "postgres": "9d0ae138704cb97e3fe4d105", "postgresql": "9d0ae138704cb97e3fe4d105", "snowflake": "9d0ae138704cb97e3fe4d105", @@ -3829,14 +3829,14 @@ { "DuckDB": "de89632e277dbc8c97115419", "None": "de89632e277dbc8c97115419", -"bigquery": "de89632e277dbc8c97115419", +"bigquery": "5abf4ec799a2ec32fb10da7e", "databricks": "de89632e277dbc8c97115419", "duckdb": "de89632e277dbc8c97115419", "generic": "de89632e277dbc8c97115419", "motherduck": "901c5133ed47d4769286e860", "mysql": "de89632e277dbc8c97115419", -"postgres": "de89632e277dbc8c97115419", -"postgresql": "de89632e277dbc8c97115419", +"postgres": "5abf4ec799a2ec32fb10da7e", +"postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", "tsql": "de89632e277dbc8c97115419" } @@ -3845,34 +3845,34 @@ "type", { "DuckDB": "70fb4728c3adcf6dbe4975d5", -"None": "70fb4728c3adcf6dbe4975d5", -"bigquery": "70fb4728c3adcf6dbe4975d5", -"databricks": "70fb4728c3adcf6dbe4975d5", +"None": "fb4b6f5101f973d16ea8d1f3", +"bigquery": "fb4b6f5101f973d16ea8d1f3", +"databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", -"generic": "70fb4728c3adcf6dbe4975d5", +"generic": "fb4b6f5101f973d16ea8d1f3", "motherduck": "901c5133ed47d4769286e860", -"mysql": "70fb4728c3adcf6dbe4975d5", +"mysql": "fb4b6f5101f973d16ea8d1f3", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", -"tsql": "70fb4728c3adcf6dbe4975d5" +"tsql": "fb4b6f5101f973d16ea8d1f3" } ], [ "type", { "DuckDB": "2c44dc35fd1ef8701101802e", -"None": "2c44dc35fd1ef8701101802e", -"bigquery": "a428302e4848ffe877284c84", -"databricks": "2c44dc35fd1ef8701101802e", +"None": "8441d00030df06ad6a5534d3", +"bigquery": "c9b810639795a7bfab91b98d", +"databricks": "8441d00030df06ad6a5534d3", "duckdb": "2c44dc35fd1ef8701101802e", -"generic": "2c44dc35fd1ef8701101802e", +"generic": "8441d00030df06ad6a5534d3", "motherduck": "901c5133ed47d4769286e860", -"mysql": "2c44dc35fd1ef8701101802e", -"postgres": "2c44dc35fd1ef8701101802e", -"postgresql": "2c44dc35fd1ef8701101802e", +"mysql": "8441d00030df06ad6a5534d3", +"postgres": "8441d00030df06ad6a5534d3", +"postgresql": "8441d00030df06ad6a5534d3", "snowflake": "652ff8b21f638eac8fdc46c5", -"tsql": "2c44dc35fd1ef8701101802e" +"tsql": "8441d00030df06ad6a5534d3" } ], [ @@ -3896,17 +3896,17 @@ "type", { "DuckDB": "8a3f094f18fb4047f0febf59", -"None": "e51e1eecca3e36c76e34498f", -"bigquery": "5c28019dce48a74708c90a76", -"databricks": "e51e1eecca3e36c76e34498f", +"None": "38b2ec94bf02be40007ce525", +"bigquery": "850deea990b52b82b36a9848", +"databricks": "38b2ec94bf02be40007ce525", "duckdb": "8a3f094f18fb4047f0febf59", -"generic": "e51e1eecca3e36c76e34498f", +"generic": "38b2ec94bf02be40007ce525", "motherduck": "901c5133ed47d4769286e860", -"mysql": "84fca9ac893777959cfd715c", -"postgres": "e51e1eecca3e36c76e34498f", -"postgresql": "e51e1eecca3e36c76e34498f", +"mysql": "38b2ec94bf02be40007ce525", +"postgres": "850deea990b52b82b36a9848", +"postgresql": "850deea990b52b82b36a9848", "snowflake": "e51e1eecca3e36c76e34498f", -"tsql": "8adf69b4c11191257f2953cc" +"tsql": "38b2ec94bf02be40007ce525" } ], [ @@ -3914,14 +3914,14 @@ { "DuckDB": "4b356542044f1267fcfc487a", "None": "4b356542044f1267fcfc487a", -"bigquery": "4b356542044f1267fcfc487a", +"bigquery": "a5656d70e208254d1d3e9b4f", "databricks": "4b356542044f1267fcfc487a", "duckdb": "4b356542044f1267fcfc487a", "generic": "4b356542044f1267fcfc487a", "motherduck": "901c5133ed47d4769286e860", "mysql": "4b356542044f1267fcfc487a", -"postgres": "4b356542044f1267fcfc487a", -"postgresql": "4b356542044f1267fcfc487a", +"postgres": "a5656d70e208254d1d3e9b4f", +"postgresql": "a5656d70e208254d1d3e9b4f", "snowflake": "4b356542044f1267fcfc487a", "tsql": "4b356542044f1267fcfc487a" } @@ -3948,14 +3948,14 @@ { "DuckDB": "afc451102e0ebe7f507119b9", "None": "afc451102e0ebe7f507119b9", -"bigquery": "afc451102e0ebe7f507119b9", +"bigquery": "c7c38fdeaeb7858a44976f1d", "databricks": "afc451102e0ebe7f507119b9", "duckdb": "afc451102e0ebe7f507119b9", "generic": "afc451102e0ebe7f507119b9", "motherduck": "901c5133ed47d4769286e860", "mysql": "afc451102e0ebe7f507119b9", -"postgres": "afc451102e0ebe7f507119b9", -"postgresql": "afc451102e0ebe7f507119b9", +"postgres": "ff3f47e5455500158628a360", +"postgresql": "ff3f47e5455500158628a360", "snowflake": "afc451102e0ebe7f507119b9", "tsql": "afc451102e0ebe7f507119b9" } @@ -4016,14 +4016,14 @@ { "DuckDB": "60b39a68a18438603dd5496d", "None": "60b39a68a18438603dd5496d", -"bigquery": "60b39a68a18438603dd5496d", +"bigquery": "893ff554934dfc6f8253597b", "databricks": "60b39a68a18438603dd5496d", "duckdb": "60b39a68a18438603dd5496d", "generic": "60b39a68a18438603dd5496d", "motherduck": "901c5133ed47d4769286e860", "mysql": "60b39a68a18438603dd5496d", -"postgres": "60b39a68a18438603dd5496d", -"postgresql": "60b39a68a18438603dd5496d", +"postgres": "7a007e6ff9e9465c9d7fadea", +"postgresql": "7a007e6ff9e9465c9d7fadea", "snowflake": "60b39a68a18438603dd5496d", "tsql": "60b39a68a18438603dd5496d" } @@ -4039,8 +4039,8 @@ "generic": "b561884132dbe7e42b719110", "motherduck": "901c5133ed47d4769286e860", "mysql": "b561884132dbe7e42b719110", -"postgres": "b561884132dbe7e42b719110", -"postgresql": "b561884132dbe7e42b719110", +"postgres": "5aa51d84ac5e6c7a9306f348", +"postgresql": "5aa51d84ac5e6c7a9306f348", "snowflake": "b561884132dbe7e42b719110", "tsql": "b561884132dbe7e42b719110" } @@ -4101,14 +4101,14 @@ { "DuckDB": "38b2ec94bf02be40007ce525", "None": "38b2ec94bf02be40007ce525", -"bigquery": "38b2ec94bf02be40007ce525", +"bigquery": "850deea990b52b82b36a9848", "databricks": "38b2ec94bf02be40007ce525", "duckdb": "38b2ec94bf02be40007ce525", "generic": "38b2ec94bf02be40007ce525", "motherduck": "901c5133ed47d4769286e860", "mysql": "38b2ec94bf02be40007ce525", -"postgres": "38b2ec94bf02be40007ce525", -"postgresql": "38b2ec94bf02be40007ce525", +"postgres": "850deea990b52b82b36a9848", +"postgresql": "850deea990b52b82b36a9848", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "38b2ec94bf02be40007ce525" } @@ -4152,14 +4152,14 @@ { "DuckDB": "776e4bf23c8ef10ffaceade2", "None": "776e4bf23c8ef10ffaceade2", -"bigquery": "776e4bf23c8ef10ffaceade2", +"bigquery": "c7c38fdeaeb7858a44976f1d", "databricks": "776e4bf23c8ef10ffaceade2", "duckdb": "776e4bf23c8ef10ffaceade2", "generic": "776e4bf23c8ef10ffaceade2", "motherduck": "901c5133ed47d4769286e860", "mysql": "776e4bf23c8ef10ffaceade2", -"postgres": "776e4bf23c8ef10ffaceade2", -"postgresql": "776e4bf23c8ef10ffaceade2", +"postgres": "5d06b10843e3e6487fb9906a", +"postgresql": "5d06b10843e3e6487fb9906a", "snowflake": "776e4bf23c8ef10ffaceade2", "tsql": "776e4bf23c8ef10ffaceade2" } @@ -4203,14 +4203,14 @@ { "DuckDB": "e408f2d8251e1675179187e6", "None": "e408f2d8251e1675179187e6", -"bigquery": "e408f2d8251e1675179187e6", +"bigquery": "837468174ef3e5cb4e4b4070", "databricks": "e408f2d8251e1675179187e6", "duckdb": "e408f2d8251e1675179187e6", "generic": "e408f2d8251e1675179187e6", "motherduck": "901c5133ed47d4769286e860", "mysql": "e408f2d8251e1675179187e6", -"postgres": "e408f2d8251e1675179187e6", -"postgresql": "e408f2d8251e1675179187e6", +"postgres": "04bfa329b4dc3b5c43ad2560", +"postgresql": "04bfa329b4dc3b5c43ad2560", "snowflake": "e408f2d8251e1675179187e6", "tsql": "e408f2d8251e1675179187e6" } @@ -4220,14 +4220,14 @@ { "DuckDB": "aab0389e2b5470136660bf8a", "None": "aab0389e2b5470136660bf8a", -"bigquery": "c9b810639795a7bfab91b98d", +"bigquery": "a428302e4848ffe877284c84", "databricks": "aab0389e2b5470136660bf8a", "duckdb": "aab0389e2b5470136660bf8a", "generic": "aab0389e2b5470136660bf8a", "motherduck": "901c5133ed47d4769286e860", "mysql": "aab0389e2b5470136660bf8a", -"postgres": "aab0389e2b5470136660bf8a", -"postgresql": "aab0389e2b5470136660bf8a", +"postgres": "f16105cbc61d547e6c08efc8", +"postgresql": "f16105cbc61d547e6c08efc8", "snowflake": "97a20344d811fb77c7690863", "tsql": "aab0389e2b5470136660bf8a" } @@ -4287,51 +4287,51 @@ "type", { "DuckDB": "f8262a8fca64e4491f151610", -"None": "dee6957f9182718819d5f721", -"bigquery": "c9c3ab06d6a47f6501ca2f3f", -"databricks": "47f8ea982c36ab449140ac7a", +"None": "fbcb21692163c35ec690c1a4", +"bigquery": "c74425c5ce650ac69c83ca5b", +"databricks": "fbcb21692163c35ec690c1a4", "duckdb": "f8262a8fca64e4491f151610", -"generic": "dee6957f9182718819d5f721", +"generic": "fbcb21692163c35ec690c1a4", "motherduck": "901c5133ed47d4769286e860", -"mysql": "dee6957f9182718819d5f721", -"postgres": "5bc5cf0052fdc17435b19fc2", -"postgresql": "5bc5cf0052fdc17435b19fc2", +"mysql": "fbcb21692163c35ec690c1a4", +"postgres": "fbcb21692163c35ec690c1a4", +"postgresql": "fbcb21692163c35ec690c1a4", "snowflake": "f8262a8fca64e4491f151610", -"tsql": "dee6957f9182718819d5f721" +"tsql": "fbcb21692163c35ec690c1a4" } ], [ "type", { "DuckDB": "b694aa93969368381cb9d9bd", -"None": "b694aa93969368381cb9d9bd", -"bigquery": "df6620c838173adb34d41a29", -"databricks": "b694aa93969368381cb9d9bd", +"None": "fdd892c7bbc36567f9619175", +"bigquery": "3c0719ca5c83e0a20d0b0b42", +"databricks": "fdd892c7bbc36567f9619175", "duckdb": "b694aa93969368381cb9d9bd", -"generic": "b694aa93969368381cb9d9bd", +"generic": "fdd892c7bbc36567f9619175", "motherduck": "901c5133ed47d4769286e860", -"mysql": "b694aa93969368381cb9d9bd", +"mysql": "fdd892c7bbc36567f9619175", "postgres": "b694aa93969368381cb9d9bd", "postgresql": "b694aa93969368381cb9d9bd", "snowflake": "58d5f123164e58c358315897", -"tsql": "b694aa93969368381cb9d9bd" +"tsql": "fdd892c7bbc36567f9619175" } ], [ "type", { "DuckDB": "c50a316d091c821866e4bfba", -"None": "c50a316d091c821866e4bfba", -"bigquery": "c50a316d091c821866e4bfba", -"databricks": "c50a316d091c821866e4bfba", +"None": "e6ffdece2977be55da73b59a", +"bigquery": "e6ffdece2977be55da73b59a", +"databricks": "e6ffdece2977be55da73b59a", "duckdb": "c50a316d091c821866e4bfba", -"generic": "c50a316d091c821866e4bfba", +"generic": "e6ffdece2977be55da73b59a", "motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"mysql": "e6ffdece2977be55da73b59a", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", -"tsql": "c50a316d091c821866e4bfba" +"tsql": "e6ffdece2977be55da73b59a" } ], [ @@ -4344,7 +4344,7 @@ "duckdb": "2a0e1cbf59d282b31816d003", "generic": "2a0e1cbf59d282b31816d003", "motherduck": "901c5133ed47d4769286e860", -"mysql": "a49cf160806465e17f3bb015", +"mysql": "2a0e1cbf59d282b31816d003", "postgres": "2a0e1cbf59d282b31816d003", "postgresql": "2a0e1cbf59d282b31816d003", "snowflake": "8cdd1c724939d35c854480a1", @@ -4373,7 +4373,7 @@ { "DuckDB": "401e8475dd39d667a86332fd", "None": "401e8475dd39d667a86332fd", -"bigquery": "122a82cc999a6e522d790b87", +"bigquery": "0439c1de159d15c43453a07b", "databricks": "e33f1ebea03ff1cb2c6e78c4", "duckdb": "401e8475dd39d667a86332fd", "generic": "401e8475dd39d667a86332fd", @@ -4441,14 +4441,14 @@ { "DuckDB": "de89632e277dbc8c97115419", "None": "de89632e277dbc8c97115419", -"bigquery": "de89632e277dbc8c97115419", +"bigquery": "5abf4ec799a2ec32fb10da7e", "databricks": "de89632e277dbc8c97115419", "duckdb": "de89632e277dbc8c97115419", "generic": "de89632e277dbc8c97115419", "motherduck": "901c5133ed47d4769286e860", "mysql": "de89632e277dbc8c97115419", -"postgres": "de89632e277dbc8c97115419", -"postgresql": "de89632e277dbc8c97115419", +"postgres": "5abf4ec799a2ec32fb10da7e", +"postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", "tsql": "de89632e277dbc8c97115419" } @@ -4457,17 +4457,17 @@ "type", { "DuckDB": "3147a9e06daa6de4a4df5abc", -"None": "3147a9e06daa6de4a4df5abc", -"bigquery": "f276e907770196606577c932", -"databricks": "3147a9e06daa6de4a4df5abc", +"None": "da0625706f12b764a6912278", +"bigquery": "b0477b36767a62145aa9158c", +"databricks": "da0625706f12b764a6912278", "duckdb": "3147a9e06daa6de4a4df5abc", -"generic": "3147a9e06daa6de4a4df5abc", +"generic": "da0625706f12b764a6912278", "motherduck": "901c5133ed47d4769286e860", -"mysql": "3147a9e06daa6de4a4df5abc", +"mysql": "da0625706f12b764a6912278", "postgres": "3147a9e06daa6de4a4df5abc", "postgresql": "3147a9e06daa6de4a4df5abc", "snowflake": "67b5874a2c569226c490211f", -"tsql": "3147a9e06daa6de4a4df5abc" +"tsql": "da0625706f12b764a6912278" } ], [ @@ -4497,7 +4497,7 @@ "duckdb": "8a7aa7fa75696327320e0add", "generic": "8a7aa7fa75696327320e0add", "motherduck": "901c5133ed47d4769286e860", -"mysql": "e62af34c2d31bc9ab880629e", +"mysql": "8a7aa7fa75696327320e0add", "postgres": "8a7aa7fa75696327320e0add", "postgresql": "8a7aa7fa75696327320e0add", "snowflake": "8a7aa7fa75696327320e0add", @@ -4526,7 +4526,7 @@ { "DuckDB": "aae2b1fe6f2d9807ecde412b", "None": "aae2b1fe6f2d9807ecde412b", -"bigquery": "e1c36a5cdd2427ff2b089ac9", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "aae2b1fe6f2d9807ecde412b", "duckdb": "aae2b1fe6f2d9807ecde412b", "generic": "aae2b1fe6f2d9807ecde412b", @@ -4560,14 +4560,14 @@ { "DuckDB": "95050a5e73c2bff179a785b7", "None": "95050a5e73c2bff179a785b7", -"bigquery": "95050a5e73c2bff179a785b7", +"bigquery": "837468174ef3e5cb4e4b4070", "databricks": "95050a5e73c2bff179a785b7", "duckdb": "95050a5e73c2bff179a785b7", "generic": "95050a5e73c2bff179a785b7", "motherduck": "901c5133ed47d4769286e860", "mysql": "95050a5e73c2bff179a785b7", -"postgres": "95050a5e73c2bff179a785b7", -"postgresql": "95050a5e73c2bff179a785b7", +"postgres": "e197cb69fc5e5754a25d4038", +"postgresql": "e197cb69fc5e5754a25d4038", "snowflake": "95050a5e73c2bff179a785b7", "tsql": "95050a5e73c2bff179a785b7" } @@ -4594,14 +4594,14 @@ { "DuckDB": "e46d4be44ab9e500e9190d26", "None": "e46d4be44ab9e500e9190d26", -"bigquery": "e6ffdece2977be55da73b59a", +"bigquery": "c50a316d091c821866e4bfba", "databricks": "e46d4be44ab9e500e9190d26", "duckdb": "e46d4be44ab9e500e9190d26", "generic": "e46d4be44ab9e500e9190d26", "motherduck": "901c5133ed47d4769286e860", "mysql": "e46d4be44ab9e500e9190d26", -"postgres": "e46d4be44ab9e500e9190d26", -"postgresql": "e46d4be44ab9e500e9190d26", +"postgres": "251aba3fef503ce64556c701", +"postgresql": "251aba3fef503ce64556c701", "snowflake": "e46d4be44ab9e500e9190d26", "tsql": "e46d4be44ab9e500e9190d26" } @@ -4610,17 +4610,17 @@ "type", { "DuckDB": "314ed506f24e92300a425780", -"None": "314ed506f24e92300a425780", -"bigquery": "314ed506f24e92300a425780", -"databricks": "314ed506f24e92300a425780", +"None": "9a66317fb8d040bf991502c0", +"bigquery": "9bafb08d41fd1c208b5deca4", +"databricks": "9a66317fb8d040bf991502c0", "duckdb": "314ed506f24e92300a425780", -"generic": "314ed506f24e92300a425780", +"generic": "9a66317fb8d040bf991502c0", "motherduck": "901c5133ed47d4769286e860", -"mysql": "314ed506f24e92300a425780", +"mysql": "9a66317fb8d040bf991502c0", "postgres": "314ed506f24e92300a425780", "postgresql": "314ed506f24e92300a425780", "snowflake": "314ed506f24e92300a425780", -"tsql": "314ed506f24e92300a425780" +"tsql": "9a66317fb8d040bf991502c0" } ], [ @@ -4651,8 +4651,8 @@ "generic": "f8d53ecf4ba59d519bb15d38", "motherduck": "901c5133ed47d4769286e860", "mysql": "f8d53ecf4ba59d519bb15d38", -"postgres": "f8d53ecf4ba59d519bb15d38", -"postgresql": "f8d53ecf4ba59d519bb15d38", +"postgres": "61d79925ac143bbc1051c8fa", +"postgresql": "61d79925ac143bbc1051c8fa", "snowflake": "f8d53ecf4ba59d519bb15d38", "tsql": "f8d53ecf4ba59d519bb15d38" } @@ -4728,18 +4728,18 @@ [ "type", { -"DuckDB": "57a219193ee01aa40a7a339d", -"None": "1cff449f8adc98f1234ffeec", -"bigquery": "c49aa38b33ba6204f42571c8", -"databricks": "1cff449f8adc98f1234ffeec", -"duckdb": "57a219193ee01aa40a7a339d", -"generic": "1cff449f8adc98f1234ffeec", +"DuckDB": "bfb659237d70eae4d07a6d10", +"None": "009acb58d411c47680971462", +"bigquery": "c74425c5ce650ac69c83ca5b", +"databricks": "009acb58d411c47680971462", +"duckdb": "bfb659237d70eae4d07a6d10", +"generic": "009acb58d411c47680971462", "motherduck": "901c5133ed47d4769286e860", -"mysql": "1cff449f8adc98f1234ffeec", -"postgres": "1cff449f8adc98f1234ffeec", -"postgresql": "1cff449f8adc98f1234ffeec", +"mysql": "009acb58d411c47680971462", +"postgres": "bfb659237d70eae4d07a6d10", +"postgresql": "bfb659237d70eae4d07a6d10", "snowflake": "bfb659237d70eae4d07a6d10", -"tsql": "1cff449f8adc98f1234ffeec" +"tsql": "009acb58d411c47680971462" } ], [ @@ -4770,8 +4770,8 @@ "generic": "a3acd7ba35e81a800f967bd3", "motherduck": "901c5133ed47d4769286e860", "mysql": "a3acd7ba35e81a800f967bd3", -"postgres": "a3acd7ba35e81a800f967bd3", -"postgresql": "a3acd7ba35e81a800f967bd3", +"postgres": "42d4a23d9a52e79d0c3458d3", +"postgresql": "42d4a23d9a52e79d0c3458d3", "snowflake": "a3acd7ba35e81a800f967bd3", "tsql": "a3acd7ba35e81a800f967bd3" } @@ -4804,8 +4804,8 @@ "generic": "de89632e277dbc8c97115419", "motherduck": "901c5133ed47d4769286e860", "mysql": "de89632e277dbc8c97115419", -"postgres": "de89632e277dbc8c97115419", -"postgresql": "de89632e277dbc8c97115419", +"postgres": "5abf4ec799a2ec32fb10da7e", +"postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", "tsql": "de89632e277dbc8c97115419" } @@ -4871,7 +4871,7 @@ "duckdb": "110327dc45eac0e8d54a96dc", "generic": "110327dc45eac0e8d54a96dc", "motherduck": "901c5133ed47d4769286e860", -"mysql": "1a804f317a3e9a0016925b04", +"mysql": "110327dc45eac0e8d54a96dc", "postgres": "110327dc45eac0e8d54a96dc", "postgresql": "110327dc45eac0e8d54a96dc", "snowflake": "110327dc45eac0e8d54a96dc", @@ -4882,34 +4882,34 @@ "type", { "DuckDB": "5abf4ec799a2ec32fb10da7e", -"None": "5abf4ec799a2ec32fb10da7e", -"bigquery": "5abf4ec799a2ec32fb10da7e", -"databricks": "5abf4ec799a2ec32fb10da7e", +"None": "4b356542044f1267fcfc487a", +"bigquery": "a5656d70e208254d1d3e9b4f", +"databricks": "4b356542044f1267fcfc487a", "duckdb": "5abf4ec799a2ec32fb10da7e", -"generic": "5abf4ec799a2ec32fb10da7e", +"generic": "4b356542044f1267fcfc487a", "motherduck": "901c5133ed47d4769286e860", -"mysql": "5abf4ec799a2ec32fb10da7e", -"postgres": "5abf4ec799a2ec32fb10da7e", -"postgresql": "5abf4ec799a2ec32fb10da7e", +"mysql": "4b356542044f1267fcfc487a", +"postgres": "a5656d70e208254d1d3e9b4f", +"postgresql": "a5656d70e208254d1d3e9b4f", "snowflake": "a5656d70e208254d1d3e9b4f", -"tsql": "5abf4ec799a2ec32fb10da7e" +"tsql": "4b356542044f1267fcfc487a" } ], [ "type", { "DuckDB": "f7fca17b74cc50ae3de8c826", -"None": "f7fca17b74cc50ae3de8c826", -"bigquery": "f7fca17b74cc50ae3de8c826", -"databricks": "f7fca17b74cc50ae3de8c826", +"None": "d2c97d221599d038d9875873", +"bigquery": "55155766823b6a3a5e882fc2", +"databricks": "d2c97d221599d038d9875873", "duckdb": "f7fca17b74cc50ae3de8c826", -"generic": "f7fca17b74cc50ae3de8c826", +"generic": "d2c97d221599d038d9875873", "motherduck": "901c5133ed47d4769286e860", -"mysql": "f7fca17b74cc50ae3de8c826", -"postgres": "f7fca17b74cc50ae3de8c826", -"postgresql": "f7fca17b74cc50ae3de8c826", +"mysql": "d2c97d221599d038d9875873", +"postgres": "55155766823b6a3a5e882fc2", +"postgresql": "55155766823b6a3a5e882fc2", "snowflake": "55155766823b6a3a5e882fc2", -"tsql": "f7fca17b74cc50ae3de8c826" +"tsql": "d2c97d221599d038d9875873" } ], [ @@ -4917,7 +4917,7 @@ { "DuckDB": "4d02ddd0ab80d0d46bf48a5f", "None": "4d02ddd0ab80d0d46bf48a5f", -"bigquery": "4d02ddd0ab80d0d46bf48a5f", +"bigquery": "3d7437d0bdf5ed3aa20531f2", "databricks": "4d02ddd0ab80d0d46bf48a5f", "duckdb": "4d02ddd0ab80d0d46bf48a5f", "generic": "4d02ddd0ab80d0d46bf48a5f", @@ -4934,14 +4934,14 @@ { "DuckDB": "18db5afe2c1e80d1ee652b54", "None": "18db5afe2c1e80d1ee652b54", -"bigquery": "e1c36a5cdd2427ff2b089ac9", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "18db5afe2c1e80d1ee652b54", "duckdb": "18db5afe2c1e80d1ee652b54", "generic": "18db5afe2c1e80d1ee652b54", "motherduck": "901c5133ed47d4769286e860", "mysql": "18db5afe2c1e80d1ee652b54", -"postgres": "18db5afe2c1e80d1ee652b54", -"postgresql": "18db5afe2c1e80d1ee652b54", +"postgres": "47d64a5994d42a2b577166ea", +"postgresql": "47d64a5994d42a2b577166ea", "snowflake": "18db5afe2c1e80d1ee652b54", "tsql": "18db5afe2c1e80d1ee652b54" } @@ -4990,7 +4990,7 @@ "duckdb": "38e28682c0f47e13e642ab1c", "generic": "38e28682c0f47e13e642ab1c", "motherduck": "901c5133ed47d4769286e860", -"mysql": "25f9faed41d5d23ca3fa8175", +"mysql": "38e28682c0f47e13e642ab1c", "postgres": "38e28682c0f47e13e642ab1c", "postgresql": "38e28682c0f47e13e642ab1c", "snowflake": "41cf14d12db9a99ffd9a4514", @@ -5036,14 +5036,14 @@ { "DuckDB": "38b2ec94bf02be40007ce525", "None": "38b2ec94bf02be40007ce525", -"bigquery": "38b2ec94bf02be40007ce525", +"bigquery": "850deea990b52b82b36a9848", "databricks": "38b2ec94bf02be40007ce525", "duckdb": "38b2ec94bf02be40007ce525", "generic": "38b2ec94bf02be40007ce525", "motherduck": "901c5133ed47d4769286e860", "mysql": "38b2ec94bf02be40007ce525", -"postgres": "38b2ec94bf02be40007ce525", -"postgresql": "38b2ec94bf02be40007ce525", +"postgres": "850deea990b52b82b36a9848", +"postgresql": "850deea990b52b82b36a9848", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "38b2ec94bf02be40007ce525" } @@ -5154,17 +5154,17 @@ "type", { "DuckDB": "70fb4728c3adcf6dbe4975d5", -"None": "70fb4728c3adcf6dbe4975d5", -"bigquery": "70fb4728c3adcf6dbe4975d5", -"databricks": "70fb4728c3adcf6dbe4975d5", +"None": "fb4b6f5101f973d16ea8d1f3", +"bigquery": "fb4b6f5101f973d16ea8d1f3", +"databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", -"generic": "70fb4728c3adcf6dbe4975d5", +"generic": "fb4b6f5101f973d16ea8d1f3", "motherduck": "901c5133ed47d4769286e860", -"mysql": "70fb4728c3adcf6dbe4975d5", +"mysql": "fb4b6f5101f973d16ea8d1f3", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", -"tsql": "70fb4728c3adcf6dbe4975d5" +"tsql": "fb4b6f5101f973d16ea8d1f3" } ], [ @@ -5172,7 +5172,7 @@ { "DuckDB": "3e611643f0beab4953c4e09b", "None": "3e611643f0beab4953c4e09b", -"bigquery": "2a0918be53051fe629d9ff8f", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "3e611643f0beab4953c4e09b", "duckdb": "3e611643f0beab4953c4e09b", "generic": "3e611643f0beab4953c4e09b", @@ -5189,14 +5189,14 @@ { "DuckDB": "776e4bf23c8ef10ffaceade2", "None": "776e4bf23c8ef10ffaceade2", -"bigquery": "776e4bf23c8ef10ffaceade2", +"bigquery": "c7c38fdeaeb7858a44976f1d", "databricks": "776e4bf23c8ef10ffaceade2", "duckdb": "776e4bf23c8ef10ffaceade2", "generic": "776e4bf23c8ef10ffaceade2", "motherduck": "901c5133ed47d4769286e860", "mysql": "776e4bf23c8ef10ffaceade2", -"postgres": "776e4bf23c8ef10ffaceade2", -"postgresql": "776e4bf23c8ef10ffaceade2", +"postgres": "5d06b10843e3e6487fb9906a", +"postgresql": "5d06b10843e3e6487fb9906a", "snowflake": "776e4bf23c8ef10ffaceade2", "tsql": "776e4bf23c8ef10ffaceade2" } @@ -5256,17 +5256,17 @@ "type", { "DuckDB": "c50a316d091c821866e4bfba", -"None": "c50a316d091c821866e4bfba", -"bigquery": "c50a316d091c821866e4bfba", -"databricks": "c50a316d091c821866e4bfba", +"None": "e6ffdece2977be55da73b59a", +"bigquery": "e6ffdece2977be55da73b59a", +"databricks": "e6ffdece2977be55da73b59a", "duckdb": "c50a316d091c821866e4bfba", -"generic": "c50a316d091c821866e4bfba", +"generic": "e6ffdece2977be55da73b59a", "motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"mysql": "e6ffdece2977be55da73b59a", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", -"tsql": "c50a316d091c821866e4bfba" +"tsql": "e6ffdece2977be55da73b59a" } ], [ @@ -5314,8 +5314,8 @@ "generic": "e2879cf4770e7f48d2dd704a", "motherduck": "901c5133ed47d4769286e860", "mysql": "e2879cf4770e7f48d2dd704a", -"postgres": "e2879cf4770e7f48d2dd704a", -"postgresql": "e2879cf4770e7f48d2dd704a", +"postgres": "2d4aea8dec5521e258596f3d", +"postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", "tsql": "e2879cf4770e7f48d2dd704a" } @@ -5375,17 +5375,17 @@ "type", { "DuckDB": "363159f251e264604338e690", -"None": "363159f251e264604338e690", -"bigquery": "363159f251e264604338e690", -"databricks": "363159f251e264604338e690", +"None": "9a63ad6168418af23d9b6fd1", +"bigquery": "8b8d5f75ba6dec68b4cebc6b", +"databricks": "9a63ad6168418af23d9b6fd1", "duckdb": "363159f251e264604338e690", -"generic": "363159f251e264604338e690", +"generic": "9a63ad6168418af23d9b6fd1", "motherduck": "901c5133ed47d4769286e860", -"mysql": "363159f251e264604338e690", -"postgres": "363159f251e264604338e690", -"postgresql": "363159f251e264604338e690", +"mysql": "9a63ad6168418af23d9b6fd1", +"postgres": "f276e907770196606577c932", +"postgresql": "f276e907770196606577c932", "snowflake": "363159f251e264604338e690", -"tsql": "363159f251e264604338e690" +"tsql": "9a63ad6168418af23d9b6fd1" } ], [ @@ -5416,8 +5416,8 @@ "generic": "5829c1427d562e21c1ed4dfe", "motherduck": "901c5133ed47d4769286e860", "mysql": "5829c1427d562e21c1ed4dfe", -"postgres": "5829c1427d562e21c1ed4dfe", -"postgresql": "5829c1427d562e21c1ed4dfe", +"postgres": "348b6977d03b636ecd10e63d", +"postgresql": "348b6977d03b636ecd10e63d", "snowflake": "5829c1427d562e21c1ed4dfe", "tsql": "5829c1427d562e21c1ed4dfe" } @@ -5443,15 +5443,15 @@ "type", { "DuckDB": "e3ba57c3380e7ca53675e905", -"None": "85dad253f82f475892a7b35b", -"bigquery": "85dad253f82f475892a7b35b", -"databricks": "534433bc99c8d49f592ba435", +"None": "87673dc143bcd33d8688dc34", +"bigquery": "87673dc143bcd33d8688dc34", +"databricks": "87673dc143bcd33d8688dc34", "duckdb": "e3ba57c3380e7ca53675e905", -"generic": "85dad253f82f475892a7b35b", +"generic": "87673dc143bcd33d8688dc34", "motherduck": "901c5133ed47d4769286e860", -"mysql": "3dd1a4411040373d9cd82f86", -"postgres": "463c3432687f6ea95aa60355", -"postgresql": "463c3432687f6ea95aa60355", +"mysql": "87673dc143bcd33d8688dc34", +"postgres": "87673dc143bcd33d8688dc34", +"postgresql": "87673dc143bcd33d8688dc34", "snowflake": "0161e12cd4d2efd1b5557348", "tsql": "87673dc143bcd33d8688dc34" } @@ -5460,17 +5460,17 @@ "type", { "DuckDB": "1fe8a2d5a41383bd8e502808", -"None": "1fe8a2d5a41383bd8e502808", +"None": "70f3d045595fbd43219efb36", "bigquery": "70f3d045595fbd43219efb36", "databricks": "70f3d045595fbd43219efb36", "duckdb": "1fe8a2d5a41383bd8e502808", -"generic": "1fe8a2d5a41383bd8e502808", +"generic": "70f3d045595fbd43219efb36", "motherduck": "901c5133ed47d4769286e860", "mysql": "70f3d045595fbd43219efb36", -"postgres": "1fe8a2d5a41383bd8e502808", -"postgresql": "1fe8a2d5a41383bd8e502808", +"postgres": "3c17ab67cd8c034fc4911ef2", +"postgresql": "3c17ab67cd8c034fc4911ef2", "snowflake": "1fe8a2d5a41383bd8e502808", -"tsql": "1fe8a2d5a41383bd8e502808" +"tsql": "70f3d045595fbd43219efb36" } ], [ @@ -5478,14 +5478,14 @@ { "DuckDB": "e46d4be44ab9e500e9190d26", "None": "e46d4be44ab9e500e9190d26", -"bigquery": "e6ffdece2977be55da73b59a", +"bigquery": "c50a316d091c821866e4bfba", "databricks": "e46d4be44ab9e500e9190d26", "duckdb": "e46d4be44ab9e500e9190d26", "generic": "e46d4be44ab9e500e9190d26", "motherduck": "901c5133ed47d4769286e860", "mysql": "e46d4be44ab9e500e9190d26", -"postgres": "e46d4be44ab9e500e9190d26", -"postgresql": "e46d4be44ab9e500e9190d26", +"postgres": "251aba3fef503ce64556c701", +"postgresql": "251aba3fef503ce64556c701", "snowflake": "e46d4be44ab9e500e9190d26", "tsql": "e46d4be44ab9e500e9190d26" } @@ -5495,7 +5495,7 @@ { "DuckDB": "1c58068460e5c94bc2f83d53", "None": "1c58068460e5c94bc2f83d53", -"bigquery": "1c58068460e5c94bc2f83d53", +"bigquery": "c74425c5ce650ac69c83ca5b", "databricks": "1c58068460e5c94bc2f83d53", "duckdb": "1c58068460e5c94bc2f83d53", "generic": "1c58068460e5c94bc2f83d53", @@ -5569,8 +5569,8 @@ "generic": "19a1a80ffc06169f32694de8", "motherduck": "901c5133ed47d4769286e860", "mysql": "19a1a80ffc06169f32694de8", -"postgres": "19a1a80ffc06169f32694de8", -"postgresql": "19a1a80ffc06169f32694de8", +"postgres": "ab1f5f46e69284778bd66d58", +"postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", "tsql": "19a1a80ffc06169f32694de8" } @@ -5647,17 +5647,17 @@ "type", { "DuckDB": "2c44dc35fd1ef8701101802e", -"None": "2c44dc35fd1ef8701101802e", -"bigquery": "a428302e4848ffe877284c84", -"databricks": "2c44dc35fd1ef8701101802e", +"None": "8441d00030df06ad6a5534d3", +"bigquery": "c9b810639795a7bfab91b98d", +"databricks": "8441d00030df06ad6a5534d3", "duckdb": "2c44dc35fd1ef8701101802e", -"generic": "2c44dc35fd1ef8701101802e", +"generic": "8441d00030df06ad6a5534d3", "motherduck": "901c5133ed47d4769286e860", -"mysql": "2c44dc35fd1ef8701101802e", -"postgres": "2c44dc35fd1ef8701101802e", -"postgresql": "2c44dc35fd1ef8701101802e", +"mysql": "8441d00030df06ad6a5534d3", +"postgres": "8441d00030df06ad6a5534d3", +"postgresql": "8441d00030df06ad6a5534d3", "snowflake": "652ff8b21f638eac8fdc46c5", -"tsql": "2c44dc35fd1ef8701101802e" +"tsql": "8441d00030df06ad6a5534d3" } ], [ @@ -5665,14 +5665,14 @@ { "DuckDB": "c4346d0257336219f1d26890", "None": "c4346d0257336219f1d26890", -"bigquery": "b5c4d5044b2a6e70633f0d0a", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "c4346d0257336219f1d26890", "duckdb": "c4346d0257336219f1d26890", "generic": "c4346d0257336219f1d26890", "motherduck": "901c5133ed47d4769286e860", "mysql": "c4346d0257336219f1d26890", -"postgres": "c4346d0257336219f1d26890", -"postgresql": "c4346d0257336219f1d26890", +"postgres": "19bdd92ea0cc5fb028657510", +"postgresql": "19bdd92ea0cc5fb028657510", "snowflake": "4e24a80b20b9dee0cbbfac17", "tsql": "c4346d0257336219f1d26890" } @@ -5716,14 +5716,14 @@ { "DuckDB": "097487e169d70127caa10537", "None": "097487e169d70127caa10537", -"bigquery": "097487e169d70127caa10537", +"bigquery": "837468174ef3e5cb4e4b4070", "databricks": "097487e169d70127caa10537", "duckdb": "097487e169d70127caa10537", "generic": "097487e169d70127caa10537", "motherduck": "901c5133ed47d4769286e860", "mysql": "097487e169d70127caa10537", -"postgres": "097487e169d70127caa10537", -"postgresql": "097487e169d70127caa10537", +"postgres": "ff580202d964731766475fad", +"postgresql": "ff580202d964731766475fad", "snowflake": "097487e169d70127caa10537", "tsql": "097487e169d70127caa10537" } @@ -5750,7 +5750,7 @@ { "DuckDB": "383310937b9286f8d0206ea4", "None": "383310937b9286f8d0206ea4", -"bigquery": "383310937b9286f8d0206ea4", +"bigquery": "720295cb000a4e00c08800ef", "databricks": "383310937b9286f8d0206ea4", "duckdb": "383310937b9286f8d0206ea4", "generic": "383310937b9286f8d0206ea4", @@ -5773,8 +5773,8 @@ "generic": "3efe8bb57c98fcbe74096b3c", "motherduck": "901c5133ed47d4769286e860", "mysql": "1d8b945fd793ecdcacb90c21", -"postgres": "3efe8bb57c98fcbe74096b3c", -"postgresql": "3efe8bb57c98fcbe74096b3c", +"postgres": "111af35848fe500ec2ae54b2", +"postgresql": "111af35848fe500ec2ae54b2", "snowflake": "1d8b945fd793ecdcacb90c21", "tsql": "3efe8bb57c98fcbe74096b3c" } @@ -5807,8 +5807,8 @@ "generic": "bbb97828cd28c64282709796", "motherduck": "901c5133ed47d4769286e860", "mysql": "bbb97828cd28c64282709796", -"postgres": "bbb97828cd28c64282709796", -"postgresql": "bbb97828cd28c64282709796", +"postgres": "b210f8eb26c37b422f786c6b", +"postgresql": "b210f8eb26c37b422f786c6b", "snowflake": "bbb97828cd28c64282709796", "tsql": "bbb97828cd28c64282709796" } @@ -5868,17 +5868,17 @@ "type", { "DuckDB": "d81d8c19074af0e39239164d", -"None": "1fec266134381c760a254d16", -"bigquery": "1fec266134381c760a254d16", -"databricks": "1fec266134381c760a254d16", +"None": "a2d22becd3f08974db215cfe", +"bigquery": "1e75e5227bea0ae294ac7af1", +"databricks": "a2d22becd3f08974db215cfe", "duckdb": "d81d8c19074af0e39239164d", -"generic": "1fec266134381c760a254d16", +"generic": "a2d22becd3f08974db215cfe", "motherduck": "901c5133ed47d4769286e860", -"mysql": "1fec266134381c760a254d16", -"postgres": "1fec266134381c760a254d16", -"postgresql": "1fec266134381c760a254d16", +"mysql": "a2d22becd3f08974db215cfe", +"postgres": "a2d22becd3f08974db215cfe", +"postgresql": "a2d22becd3f08974db215cfe", "snowflake": "1fec266134381c760a254d16", -"tsql": "1fec266134381c760a254d16" +"tsql": "a2d22becd3f08974db215cfe" } ], [ @@ -5926,8 +5926,8 @@ "generic": "38742651e9a5007a4262fae8", "motherduck": "901c5133ed47d4769286e860", "mysql": "38742651e9a5007a4262fae8", -"postgres": "38742651e9a5007a4262fae8", -"postgresql": "38742651e9a5007a4262fae8", +"postgres": "c5bb8c8a6d4b467b51e54a73", +"postgresql": "c5bb8c8a6d4b467b51e54a73", "snowflake": "38742651e9a5007a4262fae8", "tsql": "38742651e9a5007a4262fae8" } @@ -5937,14 +5937,14 @@ { "DuckDB": "de89632e277dbc8c97115419", "None": "de89632e277dbc8c97115419", -"bigquery": "de89632e277dbc8c97115419", +"bigquery": "5abf4ec799a2ec32fb10da7e", "databricks": "de89632e277dbc8c97115419", "duckdb": "de89632e277dbc8c97115419", "generic": "de89632e277dbc8c97115419", "motherduck": "901c5133ed47d4769286e860", "mysql": "de89632e277dbc8c97115419", -"postgres": "de89632e277dbc8c97115419", -"postgresql": "de89632e277dbc8c97115419", +"postgres": "5abf4ec799a2ec32fb10da7e", +"postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", "tsql": "de89632e277dbc8c97115419" } @@ -5952,11 +5952,11 @@ [ "type", { -"DuckDB": "cdf6c5188287878061490c2a", +"DuckDB": "67646dac6abfc002a1e7cacf", "None": "cdf6c5188287878061490c2a", "bigquery": "8b225d02320fa30e4a4e6cb1", "databricks": "f6e9bda6dd824d392effea47", -"duckdb": "cdf6c5188287878061490c2a", +"duckdb": "67646dac6abfc002a1e7cacf", "generic": "cdf6c5188287878061490c2a", "motherduck": "901c5133ed47d4769286e860", "mysql": "5598594492668be60871af14", @@ -5994,8 +5994,8 @@ "generic": "2c6b371b030464f525066893", "motherduck": "901c5133ed47d4769286e860", "mysql": "2c6b371b030464f525066893", -"postgres": "2c6b371b030464f525066893", -"postgresql": "2c6b371b030464f525066893", +"postgres": "3feb479faeec777c2ade8afd", +"postgresql": "3feb479faeec777c2ade8afd", "snowflake": "2c6b371b030464f525066893", "tsql": "2c6b371b030464f525066893" } @@ -6005,14 +6005,14 @@ { "DuckDB": "881106a4a0d2de0ff7c56373", "None": "881106a4a0d2de0ff7c56373", -"bigquery": "881106a4a0d2de0ff7c56373", +"bigquery": "893ff554934dfc6f8253597b", "databricks": "881106a4a0d2de0ff7c56373", "duckdb": "881106a4a0d2de0ff7c56373", "generic": "881106a4a0d2de0ff7c56373", "motherduck": "901c5133ed47d4769286e860", "mysql": "881106a4a0d2de0ff7c56373", -"postgres": "881106a4a0d2de0ff7c56373", -"postgresql": "881106a4a0d2de0ff7c56373", +"postgres": "cf167ec395fc843602e3b1e0", +"postgresql": "cf167ec395fc843602e3b1e0", "snowflake": "881106a4a0d2de0ff7c56373", "tsql": "881106a4a0d2de0ff7c56373" } @@ -6022,7 +6022,7 @@ { "DuckDB": "461923a0d34defd0c726d6e5", "None": "461923a0d34defd0c726d6e5", -"bigquery": "b5c4d5044b2a6e70633f0d0a", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "461923a0d34defd0c726d6e5", "duckdb": "461923a0d34defd0c726d6e5", "generic": "461923a0d34defd0c726d6e5", @@ -6039,14 +6039,14 @@ { "DuckDB": "d2c97d221599d038d9875873", "None": "d2c97d221599d038d9875873", -"bigquery": "d2c97d221599d038d9875873", +"bigquery": "55155766823b6a3a5e882fc2", "databricks": "d2c97d221599d038d9875873", "duckdb": "d2c97d221599d038d9875873", "generic": "d2c97d221599d038d9875873", "motherduck": "901c5133ed47d4769286e860", "mysql": "d2c97d221599d038d9875873", -"postgres": "d2c97d221599d038d9875873", -"postgresql": "d2c97d221599d038d9875873", +"postgres": "55155766823b6a3a5e882fc2", +"postgresql": "55155766823b6a3a5e882fc2", "snowflake": "d2c97d221599d038d9875873", "tsql": "d2c97d221599d038d9875873" } @@ -6056,7 +6056,7 @@ { "DuckDB": "07aa3fe366bb8a3f270ea3d3", "None": "07aa3fe366bb8a3f270ea3d3", -"bigquery": "07aa3fe366bb8a3f270ea3d3", +"bigquery": "c74425c5ce650ac69c83ca5b", "databricks": "07aa3fe366bb8a3f270ea3d3", "duckdb": "07aa3fe366bb8a3f270ea3d3", "generic": "07aa3fe366bb8a3f270ea3d3", @@ -6106,17 +6106,17 @@ "type", { "DuckDB": "a428302e4848ffe877284c84", -"None": "a428302e4848ffe877284c84", -"bigquery": "a428302e4848ffe877284c84", -"databricks": "a428302e4848ffe877284c84", +"None": "c9b810639795a7bfab91b98d", +"bigquery": "c9b810639795a7bfab91b98d", +"databricks": "c9b810639795a7bfab91b98d", "duckdb": "a428302e4848ffe877284c84", -"generic": "a428302e4848ffe877284c84", +"generic": "c9b810639795a7bfab91b98d", "motherduck": "901c5133ed47d4769286e860", -"mysql": "a428302e4848ffe877284c84", +"mysql": "c9b810639795a7bfab91b98d", "postgres": "a428302e4848ffe877284c84", "postgresql": "a428302e4848ffe877284c84", "snowflake": "652ff8b21f638eac8fdc46c5", -"tsql": "a428302e4848ffe877284c84" +"tsql": "c9b810639795a7bfab91b98d" } ], [ @@ -6174,17 +6174,17 @@ "type", { "DuckDB": "3147a9e06daa6de4a4df5abc", -"None": "3147a9e06daa6de4a4df5abc", -"bigquery": "f276e907770196606577c932", -"databricks": "3147a9e06daa6de4a4df5abc", +"None": "da0625706f12b764a6912278", +"bigquery": "b0477b36767a62145aa9158c", +"databricks": "da0625706f12b764a6912278", "duckdb": "3147a9e06daa6de4a4df5abc", -"generic": "3147a9e06daa6de4a4df5abc", +"generic": "da0625706f12b764a6912278", "motherduck": "901c5133ed47d4769286e860", -"mysql": "3147a9e06daa6de4a4df5abc", +"mysql": "da0625706f12b764a6912278", "postgres": "3147a9e06daa6de4a4df5abc", "postgresql": "3147a9e06daa6de4a4df5abc", "snowflake": "67b5874a2c569226c490211f", -"tsql": "3147a9e06daa6de4a4df5abc" +"tsql": "da0625706f12b764a6912278" } ], [ @@ -6198,8 +6198,8 @@ "generic": "e2879cf4770e7f48d2dd704a", "motherduck": "901c5133ed47d4769286e860", "mysql": "e2879cf4770e7f48d2dd704a", -"postgres": "e2879cf4770e7f48d2dd704a", -"postgresql": "e2879cf4770e7f48d2dd704a", +"postgres": "2d4aea8dec5521e258596f3d", +"postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", "tsql": "e2879cf4770e7f48d2dd704a" } @@ -6260,14 +6260,14 @@ { "DuckDB": "73151c1398b30e5c5d18f4ce", "None": "73151c1398b30e5c5d18f4ce", -"bigquery": "73151c1398b30e5c5d18f4ce", +"bigquery": "893ff554934dfc6f8253597b", "databricks": "73151c1398b30e5c5d18f4ce", "duckdb": "73151c1398b30e5c5d18f4ce", "generic": "73151c1398b30e5c5d18f4ce", "motherduck": "901c5133ed47d4769286e860", "mysql": "73151c1398b30e5c5d18f4ce", -"postgres": "73151c1398b30e5c5d18f4ce", -"postgresql": "73151c1398b30e5c5d18f4ce", +"postgres": "f68b7dbd74ec7c3b9f3066ef", +"postgresql": "f68b7dbd74ec7c3b9f3066ef", "snowflake": "73151c1398b30e5c5d18f4ce", "tsql": "73151c1398b30e5c5d18f4ce" } @@ -6278,11 +6278,11 @@ "DuckDB": "e3d9a113277b9c69432b32d1", "None": "e3d9a113277b9c69432b32d1", "bigquery": "e3d9a113277b9c69432b32d1", -"databricks": "4cda99191af9adfbe91e70f6", +"databricks": "e3d9a113277b9c69432b32d1", "duckdb": "e3d9a113277b9c69432b32d1", "generic": "e3d9a113277b9c69432b32d1", "motherduck": "901c5133ed47d4769286e860", -"mysql": "4cda99191af9adfbe91e70f6", +"mysql": "e3d9a113277b9c69432b32d1", "postgres": "e3d9a113277b9c69432b32d1", "postgresql": "e3d9a113277b9c69432b32d1", "snowflake": "e3d9a113277b9c69432b32d1", @@ -6333,7 +6333,7 @@ "duckdb": "66f7ca02161349d637fa7d4e", "generic": "66f7ca02161349d637fa7d4e", "motherduck": "901c5133ed47d4769286e860", -"mysql": "d90a78129f7050a540984f6f", +"mysql": "66f7ca02161349d637fa7d4e", "postgres": "66f7ca02161349d637fa7d4e", "postgresql": "66f7ca02161349d637fa7d4e", "snowflake": "66f7ca02161349d637fa7d4e", @@ -6430,14 +6430,14 @@ { "DuckDB": "e408f2d8251e1675179187e6", "None": "e408f2d8251e1675179187e6", -"bigquery": "e408f2d8251e1675179187e6", +"bigquery": "837468174ef3e5cb4e4b4070", "databricks": "e408f2d8251e1675179187e6", "duckdb": "e408f2d8251e1675179187e6", "generic": "e408f2d8251e1675179187e6", "motherduck": "901c5133ed47d4769286e860", "mysql": "e408f2d8251e1675179187e6", -"postgres": "e408f2d8251e1675179187e6", -"postgresql": "e408f2d8251e1675179187e6", +"postgres": "04bfa329b4dc3b5c43ad2560", +"postgresql": "04bfa329b4dc3b5c43ad2560", "snowflake": "e408f2d8251e1675179187e6", "tsql": "e408f2d8251e1675179187e6" } @@ -6480,17 +6480,17 @@ "type", { "DuckDB": "087a9417cb511ff7bcfd6ccb", -"None": "383f8922f4b6f167d384037d", -"bigquery": "383f8922f4b6f167d384037d", -"databricks": "383f8922f4b6f167d384037d", +"None": "2e8f6dfec4597fb9373329e7", +"bigquery": "2e8f6dfec4597fb9373329e7", +"databricks": "2e8f6dfec4597fb9373329e7", "duckdb": "087a9417cb511ff7bcfd6ccb", -"generic": "383f8922f4b6f167d384037d", +"generic": "2e8f6dfec4597fb9373329e7", "motherduck": "901c5133ed47d4769286e860", -"mysql": "383f8922f4b6f167d384037d", +"mysql": "2e8f6dfec4597fb9373329e7", "postgres": "383f8922f4b6f167d384037d", "postgresql": "383f8922f4b6f167d384037d", "snowflake": "383f8922f4b6f167d384037d", -"tsql": "383f8922f4b6f167d384037d" +"tsql": "2e8f6dfec4597fb9373329e7" } ], [ @@ -6549,14 +6549,14 @@ { "DuckDB": "38b2ec94bf02be40007ce525", "None": "38b2ec94bf02be40007ce525", -"bigquery": "38b2ec94bf02be40007ce525", +"bigquery": "850deea990b52b82b36a9848", "databricks": "38b2ec94bf02be40007ce525", "duckdb": "38b2ec94bf02be40007ce525", "generic": "38b2ec94bf02be40007ce525", "motherduck": "901c5133ed47d4769286e860", "mysql": "38b2ec94bf02be40007ce525", -"postgres": "38b2ec94bf02be40007ce525", -"postgresql": "38b2ec94bf02be40007ce525", +"postgres": "850deea990b52b82b36a9848", +"postgresql": "850deea990b52b82b36a9848", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "38b2ec94bf02be40007ce525" } @@ -6601,11 +6601,11 @@ "DuckDB": "1f9deff638e476a7f168220b", "None": "1f9deff638e476a7f168220b", "bigquery": "1f9deff638e476a7f168220b", -"databricks": "eeac51d8e73fdd4f598fd1cd", +"databricks": "1f9deff638e476a7f168220b", "duckdb": "1f9deff638e476a7f168220b", "generic": "1f9deff638e476a7f168220b", "motherduck": "901c5133ed47d4769286e860", -"mysql": "eeac51d8e73fdd4f598fd1cd", +"mysql": "1f9deff638e476a7f168220b", "postgres": "1f9deff638e476a7f168220b", "postgresql": "1f9deff638e476a7f168220b", "snowflake": "1f9deff638e476a7f168220b", @@ -6617,7 +6617,7 @@ { "DuckDB": "ebfad3a0b99f87bcac831ded", "None": "ebfad3a0b99f87bcac831ded", -"bigquery": "ebfad3a0b99f87bcac831ded", +"bigquery": "6da3227d362b48323df57544", "databricks": "ebfad3a0b99f87bcac831ded", "duckdb": "ebfad3a0b99f87bcac831ded", "generic": "ebfad3a0b99f87bcac831ded", @@ -6639,7 +6639,7 @@ "duckdb": "5620e893a908c6743b863bf6", "generic": "5620e893a908c6743b863bf6", "motherduck": "901c5133ed47d4769286e860", -"mysql": "4281b54de06f589b3507035a", +"mysql": "5620e893a908c6743b863bf6", "postgres": "5620e893a908c6743b863bf6", "postgresql": "5620e893a908c6743b863bf6", "snowflake": "5620e893a908c6743b863bf6", @@ -6685,14 +6685,14 @@ { "DuckDB": "4d0f0db1a550ed1e0da1a043", "None": "4d0f0db1a550ed1e0da1a043", -"bigquery": "4d0f0db1a550ed1e0da1a043", +"bigquery": "837468174ef3e5cb4e4b4070", "databricks": "4d0f0db1a550ed1e0da1a043", "duckdb": "4d0f0db1a550ed1e0da1a043", "generic": "4d0f0db1a550ed1e0da1a043", "motherduck": "901c5133ed47d4769286e860", "mysql": "4d0f0db1a550ed1e0da1a043", -"postgres": "4d0f0db1a550ed1e0da1a043", -"postgresql": "4d0f0db1a550ed1e0da1a043", +"postgres": "e47320d1030c166a05193bc4", +"postgresql": "e47320d1030c166a05193bc4", "snowflake": "4d0f0db1a550ed1e0da1a043", "tsql": "4d0f0db1a550ed1e0da1a043" } @@ -6810,8 +6810,8 @@ "generic": "669084d1ea704ae5b8ee05c2", "motherduck": "901c5133ed47d4769286e860", "mysql": "669084d1ea704ae5b8ee05c2", -"postgres": "669084d1ea704ae5b8ee05c2", -"postgresql": "669084d1ea704ae5b8ee05c2", +"postgres": "93c1b5b5960e26479baf88d0", +"postgresql": "93c1b5b5960e26479baf88d0", "snowflake": "669084d1ea704ae5b8ee05c2", "tsql": "669084d1ea704ae5b8ee05c2" } @@ -6821,7 +6821,7 @@ { "DuckDB": "8203bd5901a8ec71ff7c103e", "None": "8203bd5901a8ec71ff7c103e", -"bigquery": "2a0918be53051fe629d9ff8f", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "8203bd5901a8ec71ff7c103e", "duckdb": "8203bd5901a8ec71ff7c103e", "generic": "8203bd5901a8ec71ff7c103e", @@ -6844,8 +6844,8 @@ "generic": "9ff8ffdba7614214fe44bd7f", "motherduck": "901c5133ed47d4769286e860", "mysql": "9ff8ffdba7614214fe44bd7f", -"postgres": "9ff8ffdba7614214fe44bd7f", -"postgresql": "9ff8ffdba7614214fe44bd7f", +"postgres": "12a93f1979dd590479e084a8", +"postgresql": "12a93f1979dd590479e084a8", "snowflake": "9ff8ffdba7614214fe44bd7f", "tsql": "9ff8ffdba7614214fe44bd7f" } @@ -6861,8 +6861,8 @@ "generic": "336fd1b7a1f7d8105c29280c", "motherduck": "901c5133ed47d4769286e860", "mysql": "336fd1b7a1f7d8105c29280c", -"postgres": "336fd1b7a1f7d8105c29280c", -"postgresql": "336fd1b7a1f7d8105c29280c", +"postgres": "9ea4932505663b43f527adad", +"postgresql": "9ea4932505663b43f527adad", "snowflake": "336fd1b7a1f7d8105c29280c", "tsql": "336fd1b7a1f7d8105c29280c" } @@ -6871,17 +6871,17 @@ "type", { "DuckDB": "fa45b7e4a9ef3b5a9c5781af", -"None": "fa45b7e4a9ef3b5a9c5781af", -"bigquery": "fa45b7e4a9ef3b5a9c5781af", -"databricks": "fa45b7e4a9ef3b5a9c5781af", +"None": "ebe2513b6cb4001e42cc464b", +"bigquery": "0c43dc837a50933667588ef1", +"databricks": "ebe2513b6cb4001e42cc464b", "duckdb": "fa45b7e4a9ef3b5a9c5781af", -"generic": "fa45b7e4a9ef3b5a9c5781af", +"generic": "ebe2513b6cb4001e42cc464b", "motherduck": "901c5133ed47d4769286e860", -"mysql": "fa45b7e4a9ef3b5a9c5781af", +"mysql": "ebe2513b6cb4001e42cc464b", "postgres": "fa45b7e4a9ef3b5a9c5781af", "postgresql": "fa45b7e4a9ef3b5a9c5781af", "snowflake": "fa45b7e4a9ef3b5a9c5781af", -"tsql": "fa45b7e4a9ef3b5a9c5781af" +"tsql": "ebe2513b6cb4001e42cc464b" } ], [ @@ -6906,14 +6906,14 @@ { "DuckDB": "881106a4a0d2de0ff7c56373", "None": "881106a4a0d2de0ff7c56373", -"bigquery": "881106a4a0d2de0ff7c56373", +"bigquery": "893ff554934dfc6f8253597b", "databricks": "881106a4a0d2de0ff7c56373", "duckdb": "881106a4a0d2de0ff7c56373", "generic": "881106a4a0d2de0ff7c56373", "motherduck": "901c5133ed47d4769286e860", "mysql": "881106a4a0d2de0ff7c56373", -"postgres": "881106a4a0d2de0ff7c56373", -"postgresql": "881106a4a0d2de0ff7c56373", +"postgres": "cf167ec395fc843602e3b1e0", +"postgresql": "cf167ec395fc843602e3b1e0", "snowflake": "881106a4a0d2de0ff7c56373", "tsql": "881106a4a0d2de0ff7c56373" } @@ -6929,8 +6929,8 @@ "generic": "2705f19942a4194a72576911", "motherduck": "901c5133ed47d4769286e860", "mysql": "2705f19942a4194a72576911", -"postgres": "2705f19942a4194a72576911", -"postgresql": "2705f19942a4194a72576911", +"postgres": "5226d7e2a62c7650a87d6e84", +"postgresql": "5226d7e2a62c7650a87d6e84", "snowflake": "2705f19942a4194a72576911", "tsql": "2705f19942a4194a72576911" } @@ -6980,8 +6980,8 @@ "generic": "6f763a99e9c69109526b40c0", "motherduck": "901c5133ed47d4769286e860", "mysql": "6f763a99e9c69109526b40c0", -"postgres": "6f763a99e9c69109526b40c0", -"postgresql": "6f763a99e9c69109526b40c0", +"postgres": "5226d7e2a62c7650a87d6e84", +"postgresql": "5226d7e2a62c7650a87d6e84", "snowflake": "2705f19942a4194a72576911", "tsql": "6f763a99e9c69109526b40c0" } @@ -6990,17 +6990,17 @@ "type", { "DuckDB": "d9a22eb846c0561f4d74700b", -"None": "d9a22eb846c0561f4d74700b", -"bigquery": "d9a22eb846c0561f4d74700b", -"databricks": "d9a22eb846c0561f4d74700b", +"None": "93edc94c44e0732b1be558b1", +"bigquery": "93edc94c44e0732b1be558b1", +"databricks": "93edc94c44e0732b1be558b1", "duckdb": "d9a22eb846c0561f4d74700b", -"generic": "d9a22eb846c0561f4d74700b", +"generic": "93edc94c44e0732b1be558b1", "motherduck": "901c5133ed47d4769286e860", -"mysql": "d9a22eb846c0561f4d74700b", +"mysql": "93edc94c44e0732b1be558b1", "postgres": "d9a22eb846c0561f4d74700b", "postgresql": "d9a22eb846c0561f4d74700b", "snowflake": "9b5f44707b5f4764b82741be", -"tsql": "d9a22eb846c0561f4d74700b" +"tsql": "93edc94c44e0732b1be558b1" } ], [ @@ -7024,17 +7024,17 @@ "type", { "DuckDB": "70fb4728c3adcf6dbe4975d5", -"None": "70fb4728c3adcf6dbe4975d5", -"bigquery": "70fb4728c3adcf6dbe4975d5", -"databricks": "70fb4728c3adcf6dbe4975d5", +"None": "fb4b6f5101f973d16ea8d1f3", +"bigquery": "fb4b6f5101f973d16ea8d1f3", +"databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", -"generic": "70fb4728c3adcf6dbe4975d5", +"generic": "fb4b6f5101f973d16ea8d1f3", "motherduck": "901c5133ed47d4769286e860", -"mysql": "70fb4728c3adcf6dbe4975d5", +"mysql": "fb4b6f5101f973d16ea8d1f3", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", -"tsql": "70fb4728c3adcf6dbe4975d5" +"tsql": "fb4b6f5101f973d16ea8d1f3" } ], [ @@ -7082,8 +7082,8 @@ "generic": "5d25d5e13f81700741695ed4", "motherduck": "901c5133ed47d4769286e860", "mysql": "5d25d5e13f81700741695ed4", -"postgres": "5d25d5e13f81700741695ed4", -"postgresql": "5d25d5e13f81700741695ed4", +"postgres": "62131fb5b065f5de2e74a0f1", +"postgresql": "62131fb5b065f5de2e74a0f1", "snowflake": "5d25d5e13f81700741695ed4", "tsql": "5d25d5e13f81700741695ed4" } @@ -7099,8 +7099,8 @@ "generic": "aab0389e2b5470136660bf8a", "motherduck": "901c5133ed47d4769286e860", "mysql": "aab0389e2b5470136660bf8a", -"postgres": "aab0389e2b5470136660bf8a", -"postgresql": "aab0389e2b5470136660bf8a", +"postgres": "f16105cbc61d547e6c08efc8", +"postgresql": "f16105cbc61d547e6c08efc8", "snowflake": "97a20344d811fb77c7690863", "tsql": "aab0389e2b5470136660bf8a" } @@ -7110,14 +7110,14 @@ { "DuckDB": "fe23594c781e79cbb0c8927a", "None": "fe23594c781e79cbb0c8927a", -"bigquery": "fe23594c781e79cbb0c8927a", +"bigquery": "8a3f094f18fb4047f0febf59", "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", "motherduck": "901c5133ed47d4769286e860", "mysql": "fe23594c781e79cbb0c8927a", -"postgres": "fe23594c781e79cbb0c8927a", -"postgresql": "fe23594c781e79cbb0c8927a", +"postgres": "8a3f094f18fb4047f0febf59", +"postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "fe23594c781e79cbb0c8927a" } @@ -7161,7 +7161,7 @@ { "DuckDB": "18db5afe2c1e80d1ee652b54", "None": "18db5afe2c1e80d1ee652b54", -"bigquery": "e1c36a5cdd2427ff2b089ac9", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "18db5afe2c1e80d1ee652b54", "duckdb": "18db5afe2c1e80d1ee652b54", "generic": "18db5afe2c1e80d1ee652b54", @@ -7178,7 +7178,7 @@ { "DuckDB": "5e46ad7c70253689a5c6bb7f", "None": "5e46ad7c70253689a5c6bb7f", -"bigquery": "5e46ad7c70253689a5c6bb7f", +"bigquery": "c74425c5ce650ac69c83ca5b", "databricks": "5e46ad7c70253689a5c6bb7f", "duckdb": "5e46ad7c70253689a5c6bb7f", "generic": "5e46ad7c70253689a5c6bb7f", @@ -7194,17 +7194,17 @@ "type", { "DuckDB": "3147a9e06daa6de4a4df5abc", -"None": "3147a9e06daa6de4a4df5abc", -"bigquery": "f276e907770196606577c932", -"databricks": "3147a9e06daa6de4a4df5abc", +"None": "da0625706f12b764a6912278", +"bigquery": "b0477b36767a62145aa9158c", +"databricks": "da0625706f12b764a6912278", "duckdb": "3147a9e06daa6de4a4df5abc", -"generic": "3147a9e06daa6de4a4df5abc", +"generic": "da0625706f12b764a6912278", "motherduck": "901c5133ed47d4769286e860", -"mysql": "3147a9e06daa6de4a4df5abc", +"mysql": "da0625706f12b764a6912278", "postgres": "3147a9e06daa6de4a4df5abc", "postgresql": "3147a9e06daa6de4a4df5abc", "snowflake": "67b5874a2c569226c490211f", -"tsql": "3147a9e06daa6de4a4df5abc" +"tsql": "da0625706f12b764a6912278" } ], [ @@ -7286,8 +7286,8 @@ "generic": "e2879cf4770e7f48d2dd704a", "motherduck": "901c5133ed47d4769286e860", "mysql": "e2879cf4770e7f48d2dd704a", -"postgres": "e2879cf4770e7f48d2dd704a", -"postgresql": "e2879cf4770e7f48d2dd704a", +"postgres": "2d4aea8dec5521e258596f3d", +"postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", "tsql": "e2879cf4770e7f48d2dd704a" } @@ -7296,15 +7296,15 @@ "type", { "DuckDB": "8317b957016bdf11135b4836", -"None": "0a21e78538ff353d773b4eea", -"bigquery": "0a21e78538ff353d773b4eea", -"databricks": "0a21e78538ff353d773b4eea", +"None": "45310b1154cae517b4caedff", +"bigquery": "45310b1154cae517b4caedff", +"databricks": "45310b1154cae517b4caedff", "duckdb": "8317b957016bdf11135b4836", -"generic": "0a21e78538ff353d773b4eea", +"generic": "45310b1154cae517b4caedff", "motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", -"postgres": "8317b957016bdf11135b4836", -"postgresql": "8317b957016bdf11135b4836", +"mysql": "45310b1154cae517b4caedff", +"postgres": "45310b1154cae517b4caedff", +"postgresql": "45310b1154cae517b4caedff", "snowflake": "c2976683dc7003c9f4e8d7d0", "tsql": "45310b1154cae517b4caedff" } @@ -7312,18 +7312,18 @@ [ "type", { -"DuckDB": "e72e438b895cc91eaf4be018", -"None": "a1b6a3abd5c2785b419400de", -"bigquery": "f4105e8e41859869d2746e1d", -"databricks": "a1b6a3abd5c2785b419400de", -"duckdb": "e72e438b895cc91eaf4be018", -"generic": "a1b6a3abd5c2785b419400de", +"DuckDB": "f77bad2342e566026c6930f9", +"None": "325bc7668cc9012c12196cbf", +"bigquery": "325bc7668cc9012c12196cbf", +"databricks": "325bc7668cc9012c12196cbf", +"duckdb": "f77bad2342e566026c6930f9", +"generic": "325bc7668cc9012c12196cbf", "motherduck": "901c5133ed47d4769286e860", -"mysql": "a1b6a3abd5c2785b419400de", -"postgres": "a1b6a3abd5c2785b419400de", -"postgresql": "a1b6a3abd5c2785b419400de", +"mysql": "325bc7668cc9012c12196cbf", +"postgres": "6031cfaf0d57e25f8a111524", +"postgresql": "6031cfaf0d57e25f8a111524", "snowflake": "6031cfaf0d57e25f8a111524", -"tsql": "a1b6a3abd5c2785b419400de" +"tsql": "325bc7668cc9012c12196cbf" } ], [ @@ -7347,17 +7347,17 @@ "type", { "DuckDB": "1ebac487198b6c90b027faab", -"None": "1ebac487198b6c90b027faab", -"bigquery": "a428302e4848ffe877284c84", -"databricks": "1ebac487198b6c90b027faab", +"None": "1eb7fb4e8cf75773c5573562", +"bigquery": "c9b810639795a7bfab91b98d", +"databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1ebac487198b6c90b027faab", -"generic": "1ebac487198b6c90b027faab", +"generic": "1eb7fb4e8cf75773c5573562", "motherduck": "901c5133ed47d4769286e860", -"mysql": "1ebac487198b6c90b027faab", -"postgres": "1ebac487198b6c90b027faab", -"postgresql": "1ebac487198b6c90b027faab", -"snowflake": "652ff8b21f638eac8fdc46c5", -"tsql": "c692d974ee3f362ecb38f27c" +"mysql": "1eb7fb4e8cf75773c5573562", +"postgres": "1eb7fb4e8cf75773c5573562", +"postgresql": "1eb7fb4e8cf75773c5573562", +"snowflake": "97a20344d811fb77c7690863", +"tsql": "1eb7fb4e8cf75773c5573562" } ], [ @@ -7370,7 +7370,7 @@ "duckdb": "110327dc45eac0e8d54a96dc", "generic": "110327dc45eac0e8d54a96dc", "motherduck": "901c5133ed47d4769286e860", -"mysql": "1a804f317a3e9a0016925b04", +"mysql": "110327dc45eac0e8d54a96dc", "postgres": "110327dc45eac0e8d54a96dc", "postgresql": "110327dc45eac0e8d54a96dc", "snowflake": "110327dc45eac0e8d54a96dc", @@ -7416,14 +7416,14 @@ { "DuckDB": "38b2ec94bf02be40007ce525", "None": "38b2ec94bf02be40007ce525", -"bigquery": "38b2ec94bf02be40007ce525", +"bigquery": "850deea990b52b82b36a9848", "databricks": "38b2ec94bf02be40007ce525", "duckdb": "38b2ec94bf02be40007ce525", "generic": "38b2ec94bf02be40007ce525", "motherduck": "901c5133ed47d4769286e860", "mysql": "38b2ec94bf02be40007ce525", -"postgres": "38b2ec94bf02be40007ce525", -"postgresql": "38b2ec94bf02be40007ce525", +"postgres": "850deea990b52b82b36a9848", +"postgresql": "850deea990b52b82b36a9848", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "38b2ec94bf02be40007ce525" } @@ -7483,17 +7483,17 @@ "type", { "DuckDB": "5abf4ec799a2ec32fb10da7e", -"None": "5abf4ec799a2ec32fb10da7e", +"None": "de89632e277dbc8c97115419", "bigquery": "5abf4ec799a2ec32fb10da7e", -"databricks": "5abf4ec799a2ec32fb10da7e", +"databricks": "de89632e277dbc8c97115419", "duckdb": "5abf4ec799a2ec32fb10da7e", -"generic": "5abf4ec799a2ec32fb10da7e", +"generic": "de89632e277dbc8c97115419", "motherduck": "901c5133ed47d4769286e860", -"mysql": "5abf4ec799a2ec32fb10da7e", +"mysql": "de89632e277dbc8c97115419", "postgres": "5abf4ec799a2ec32fb10da7e", "postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "a5656d70e208254d1d3e9b4f", -"tsql": "5abf4ec799a2ec32fb10da7e" +"tsql": "de89632e277dbc8c97115419" } ], [ @@ -7501,14 +7501,14 @@ { "DuckDB": "08736485547ba339930697e9", "None": "08736485547ba339930697e9", -"bigquery": "08736485547ba339930697e9", +"bigquery": "837468174ef3e5cb4e4b4070", "databricks": "08736485547ba339930697e9", "duckdb": "08736485547ba339930697e9", "generic": "08736485547ba339930697e9", "motherduck": "901c5133ed47d4769286e860", "mysql": "08736485547ba339930697e9", -"postgres": "08736485547ba339930697e9", -"postgresql": "08736485547ba339930697e9", +"postgres": "eecd452b4f5f3f4133d0bd0a", +"postgresql": "eecd452b4f5f3f4133d0bd0a", "snowflake": "08736485547ba339930697e9", "tsql": "08736485547ba339930697e9" } @@ -7518,14 +7518,14 @@ { "DuckDB": "7cd31289a5b99b5a5e24492d", "None": "7cd31289a5b99b5a5e24492d", -"bigquery": "7cd31289a5b99b5a5e24492d", +"bigquery": "c7c38fdeaeb7858a44976f1d", "databricks": "7cd31289a5b99b5a5e24492d", "duckdb": "7cd31289a5b99b5a5e24492d", "generic": "7cd31289a5b99b5a5e24492d", "motherduck": "901c5133ed47d4769286e860", "mysql": "7cd31289a5b99b5a5e24492d", -"postgres": "7cd31289a5b99b5a5e24492d", -"postgresql": "7cd31289a5b99b5a5e24492d", +"postgres": "c5bdaf88df7ef3d9ae693106", +"postgresql": "c5bdaf88df7ef3d9ae693106", "snowflake": "7cd31289a5b99b5a5e24492d", "tsql": "7cd31289a5b99b5a5e24492d" } @@ -7558,8 +7558,8 @@ "generic": "8b8d5f75ba6dec68b4cebc6b", "motherduck": "901c5133ed47d4769286e860", "mysql": "8b8d5f75ba6dec68b4cebc6b", -"postgres": "8b8d5f75ba6dec68b4cebc6b", -"postgresql": "8b8d5f75ba6dec68b4cebc6b", +"postgres": "7914189ff18b0e70ddf7d042", +"postgresql": "7914189ff18b0e70ddf7d042", "snowflake": "8b8d5f75ba6dec68b4cebc6b", "tsql": "8b8d5f75ba6dec68b4cebc6b" } @@ -7636,17 +7636,17 @@ "type", { "DuckDB": "f2ed52f6bf46b4db9343551c", -"None": "f2ed52f6bf46b4db9343551c", -"bigquery": "f10bdd4d40eb495cbcecd31d", -"databricks": "f2ed52f6bf46b4db9343551c", +"None": "3f998a9ab68d8b2514840489", +"bigquery": "cc809c2a2f4e06379d6e4fe5", +"databricks": "3f998a9ab68d8b2514840489", "duckdb": "f2ed52f6bf46b4db9343551c", -"generic": "f2ed52f6bf46b4db9343551c", +"generic": "3f998a9ab68d8b2514840489", "motherduck": "901c5133ed47d4769286e860", -"mysql": "f2ed52f6bf46b4db9343551c", -"postgres": "f2ed52f6bf46b4db9343551c", -"postgresql": "f2ed52f6bf46b4db9343551c", +"mysql": "3f998a9ab68d8b2514840489", +"postgres": "7ce41cc5375b618de22b6a4e", +"postgresql": "7ce41cc5375b618de22b6a4e", "snowflake": "f2ed52f6bf46b4db9343551c", -"tsql": "f2ed52f6bf46b4db9343551c" +"tsql": "3f998a9ab68d8b2514840489" } ], [ @@ -7660,8 +7660,8 @@ "generic": "e2879cf4770e7f48d2dd704a", "motherduck": "901c5133ed47d4769286e860", "mysql": "e2879cf4770e7f48d2dd704a", -"postgres": "e2879cf4770e7f48d2dd704a", -"postgresql": "e2879cf4770e7f48d2dd704a", +"postgres": "2d4aea8dec5521e258596f3d", +"postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", "tsql": "e2879cf4770e7f48d2dd704a" } @@ -7687,17 +7687,17 @@ "type", { "DuckDB": "4ebb6333caac069be16c795d", -"None": "4ebb6333caac069be16c795d", -"bigquery": "4ebb6333caac069be16c795d", -"databricks": "4ebb6333caac069be16c795d", +"None": "d5ce089ec505733f59838ffd", +"bigquery": "d5ce089ec505733f59838ffd", +"databricks": "d5ce089ec505733f59838ffd", "duckdb": "4ebb6333caac069be16c795d", -"generic": "4ebb6333caac069be16c795d", +"generic": "d5ce089ec505733f59838ffd", "motherduck": "901c5133ed47d4769286e860", -"mysql": "4ebb6333caac069be16c795d", +"mysql": "d5ce089ec505733f59838ffd", "postgres": "4ebb6333caac069be16c795d", "postgresql": "4ebb6333caac069be16c795d", "snowflake": "4ebb6333caac069be16c795d", -"tsql": "4ebb6333caac069be16c795d" +"tsql": "d5ce089ec505733f59838ffd" } ], [ @@ -7789,17 +7789,17 @@ "type", { "DuckDB": "13f1401e7cd410600a4c9485", -"None": "13f1401e7cd410600a4c9485", -"bigquery": "74a30f6aa9def0b58afc616c", -"databricks": "13f1401e7cd410600a4c9485", +"None": "75c45736873ac5c99a62bc20", +"bigquery": "a686bb6db17ab079cfc79224", +"databricks": "75c45736873ac5c99a62bc20", "duckdb": "13f1401e7cd410600a4c9485", -"generic": "13f1401e7cd410600a4c9485", +"generic": "75c45736873ac5c99a62bc20", "motherduck": "901c5133ed47d4769286e860", -"mysql": "13f1401e7cd410600a4c9485", +"mysql": "75c45736873ac5c99a62bc20", "postgres": "13f1401e7cd410600a4c9485", "postgresql": "13f1401e7cd410600a4c9485", "snowflake": "9b5f44707b5f4764b82741be", -"tsql": "13f1401e7cd410600a4c9485" +"tsql": "75c45736873ac5c99a62bc20" } ], [ @@ -7807,14 +7807,14 @@ { "DuckDB": "d2c97d221599d038d9875873", "None": "d2c97d221599d038d9875873", -"bigquery": "d2c97d221599d038d9875873", +"bigquery": "55155766823b6a3a5e882fc2", "databricks": "d2c97d221599d038d9875873", "duckdb": "d2c97d221599d038d9875873", "generic": "d2c97d221599d038d9875873", "motherduck": "901c5133ed47d4769286e860", "mysql": "d2c97d221599d038d9875873", -"postgres": "d2c97d221599d038d9875873", -"postgresql": "d2c97d221599d038d9875873", +"postgres": "55155766823b6a3a5e882fc2", +"postgresql": "55155766823b6a3a5e882fc2", "snowflake": "d2c97d221599d038d9875873", "tsql": "d2c97d221599d038d9875873" } @@ -7858,14 +7858,14 @@ { "DuckDB": "4b356542044f1267fcfc487a", "None": "4b356542044f1267fcfc487a", -"bigquery": "4b356542044f1267fcfc487a", +"bigquery": "a5656d70e208254d1d3e9b4f", "databricks": "4b356542044f1267fcfc487a", "duckdb": "4b356542044f1267fcfc487a", "generic": "4b356542044f1267fcfc487a", "motherduck": "901c5133ed47d4769286e860", "mysql": "4b356542044f1267fcfc487a", -"postgres": "4b356542044f1267fcfc487a", -"postgresql": "4b356542044f1267fcfc487a", +"postgres": "a5656d70e208254d1d3e9b4f", +"postgresql": "a5656d70e208254d1d3e9b4f", "snowflake": "4b356542044f1267fcfc487a", "tsql": "4b356542044f1267fcfc487a" } @@ -7897,7 +7897,7 @@ "duckdb": "2844656bba2e199281ab8d9b", "generic": "2844656bba2e199281ab8d9b", "motherduck": "901c5133ed47d4769286e860", -"mysql": "916f1a306af797bd328aec64", +"mysql": "2844656bba2e199281ab8d9b", "postgres": "2844656bba2e199281ab8d9b", "postgresql": "2844656bba2e199281ab8d9b", "snowflake": "2844656bba2e199281ab8d9b", @@ -7914,7 +7914,7 @@ "duckdb": "8a7aa7fa75696327320e0add", "generic": "8a7aa7fa75696327320e0add", "motherduck": "901c5133ed47d4769286e860", -"mysql": "e62af34c2d31bc9ab880629e", +"mysql": "8a7aa7fa75696327320e0add", "postgres": "8a7aa7fa75696327320e0add", "postgresql": "8a7aa7fa75696327320e0add", "snowflake": "8a7aa7fa75696327320e0add", @@ -7934,7 +7934,7 @@ "mysql": "cbea30a905a72d3b17dc7211", "postgres": "cbea30a905a72d3b17dc7211", "postgresql": "cbea30a905a72d3b17dc7211", -"snowflake": "cbea30a905a72d3b17dc7211", +"snowflake": "7d597ede6f7cf27affdde1b0", "tsql": "cbea30a905a72d3b17dc7211" } ], @@ -8000,8 +8000,8 @@ "generic": "4e10d2f9eb8994ead08d1761", "motherduck": "901c5133ed47d4769286e860", "mysql": "4e10d2f9eb8994ead08d1761", -"postgres": "4e10d2f9eb8994ead08d1761", -"postgresql": "4e10d2f9eb8994ead08d1761", +"postgres": "f7f38c204fc2a5b7e7986a31", +"postgresql": "f7f38c204fc2a5b7e7986a31", "snowflake": "4e10d2f9eb8994ead08d1761", "tsql": "4e10d2f9eb8994ead08d1761" } @@ -8044,17 +8044,17 @@ "type", { "DuckDB": "7515c0db413239044adef879", -"None": "7515c0db413239044adef879", -"bigquery": "f276e907770196606577c932", -"databricks": "7515c0db413239044adef879", +"None": "d4b89e61a66aeb076db92954", +"bigquery": "bb92c01c64d1a2d32abb67b5", +"databricks": "d4b89e61a66aeb076db92954", "duckdb": "7515c0db413239044adef879", -"generic": "7515c0db413239044adef879", +"generic": "d4b89e61a66aeb076db92954", "motherduck": "901c5133ed47d4769286e860", -"mysql": "7515c0db413239044adef879", +"mysql": "d4b89e61a66aeb076db92954", "postgres": "7515c0db413239044adef879", "postgresql": "7515c0db413239044adef879", "snowflake": "67b5874a2c569226c490211f", -"tsql": "7515c0db413239044adef879" +"tsql": "d4b89e61a66aeb076db92954" } ], [ @@ -8095,15 +8095,15 @@ "type", { "DuckDB": "a55ce7977ac874a3b204217e", -"None": "86b982a98982cd844f498c36", -"bigquery": "86b982a98982cd844f498c36", -"databricks": "86b982a98982cd844f498c36", +"None": "a13763a505b15f311cd22d08", +"bigquery": "a13763a505b15f311cd22d08", +"databricks": "a13763a505b15f311cd22d08", "duckdb": "a55ce7977ac874a3b204217e", -"generic": "86b982a98982cd844f498c36", +"generic": "a13763a505b15f311cd22d08", "motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", -"postgres": "a55ce7977ac874a3b204217e", -"postgresql": "a55ce7977ac874a3b204217e", +"mysql": "a13763a505b15f311cd22d08", +"postgres": "a13763a505b15f311cd22d08", +"postgresql": "a13763a505b15f311cd22d08", "snowflake": "5a4429eb67014c1c8f9e10d4", "tsql": "a13763a505b15f311cd22d08" } @@ -8113,7 +8113,7 @@ { "DuckDB": "62368fc15cefd5807807b209", "None": "62368fc15cefd5807807b209", -"bigquery": "e6ffdece2977be55da73b59a", +"bigquery": "c50a316d091c821866e4bfba", "databricks": "62368fc15cefd5807807b209", "duckdb": "62368fc15cefd5807807b209", "generic": "62368fc15cefd5807807b209", @@ -8136,8 +8136,8 @@ "generic": "bbb97828cd28c64282709796", "motherduck": "901c5133ed47d4769286e860", "mysql": "bbb97828cd28c64282709796", -"postgres": "bbb97828cd28c64282709796", -"postgresql": "bbb97828cd28c64282709796", +"postgres": "b210f8eb26c37b422f786c6b", +"postgresql": "b210f8eb26c37b422f786c6b", "snowflake": "bbb97828cd28c64282709796", "tsql": "bbb97828cd28c64282709796" } @@ -8164,14 +8164,14 @@ { "DuckDB": "38b2ec94bf02be40007ce525", "None": "38b2ec94bf02be40007ce525", -"bigquery": "38b2ec94bf02be40007ce525", +"bigquery": "850deea990b52b82b36a9848", "databricks": "38b2ec94bf02be40007ce525", "duckdb": "38b2ec94bf02be40007ce525", "generic": "38b2ec94bf02be40007ce525", "motherduck": "901c5133ed47d4769286e860", "mysql": "38b2ec94bf02be40007ce525", -"postgres": "38b2ec94bf02be40007ce525", -"postgresql": "38b2ec94bf02be40007ce525", +"postgres": "850deea990b52b82b36a9848", +"postgresql": "850deea990b52b82b36a9848", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "38b2ec94bf02be40007ce525" } @@ -8180,15 +8180,15 @@ "type", { "DuckDB": "02861a11fe2b8f1a7351cb12", -"None": "3abc5e3c14192017bfd1ead5", +"None": "f2c7751cca55dc87c36b6fc2", "bigquery": "f2c7751cca55dc87c36b6fc2", "databricks": "f2c7751cca55dc87c36b6fc2", "duckdb": "02861a11fe2b8f1a7351cb12", -"generic": "3abc5e3c14192017bfd1ead5", +"generic": "f2c7751cca55dc87c36b6fc2", "motherduck": "901c5133ed47d4769286e860", "mysql": "f2c7751cca55dc87c36b6fc2", -"postgres": "35efca2fc5df9bbd4013257a", -"postgresql": "35efca2fc5df9bbd4013257a", +"postgres": "f2c7751cca55dc87c36b6fc2", +"postgresql": "f2c7751cca55dc87c36b6fc2", "snowflake": "5b65e3054ea3260a0a6dd837", "tsql": "f2c7751cca55dc87c36b6fc2" } @@ -8203,7 +8203,7 @@ "duckdb": "d1a9a884331f069d89b0935f", "generic": "d1a9a884331f069d89b0935f", "motherduck": "901c5133ed47d4769286e860", -"mysql": "efe77000c8cb77e8bcc49328", +"mysql": "d1a9a884331f069d89b0935f", "postgres": "d1a9a884331f069d89b0935f", "postgresql": "d1a9a884331f069d89b0935f", "snowflake": "d1a9a884331f069d89b0935f", @@ -8238,8 +8238,8 @@ "generic": "bce60580ef3a1dc4ce569fff", "motherduck": "901c5133ed47d4769286e860", "mysql": "bce60580ef3a1dc4ce569fff", -"postgres": "bce60580ef3a1dc4ce569fff", -"postgresql": "bce60580ef3a1dc4ce569fff", +"postgres": "eee236ea1427bdddbaf088ac", +"postgresql": "eee236ea1427bdddbaf088ac", "snowflake": "bce60580ef3a1dc4ce569fff", "tsql": "bce60580ef3a1dc4ce569fff" } @@ -8249,14 +8249,14 @@ { "DuckDB": "aab0389e2b5470136660bf8a", "None": "aab0389e2b5470136660bf8a", -"bigquery": "c9b810639795a7bfab91b98d", +"bigquery": "a428302e4848ffe877284c84", "databricks": "aab0389e2b5470136660bf8a", "duckdb": "aab0389e2b5470136660bf8a", "generic": "aab0389e2b5470136660bf8a", "motherduck": "901c5133ed47d4769286e860", "mysql": "aab0389e2b5470136660bf8a", -"postgres": "aab0389e2b5470136660bf8a", -"postgresql": "aab0389e2b5470136660bf8a", +"postgres": "f16105cbc61d547e6c08efc8", +"postgresql": "f16105cbc61d547e6c08efc8", "snowflake": "97a20344d811fb77c7690863", "tsql": "aab0389e2b5470136660bf8a" } @@ -8289,8 +8289,8 @@ "generic": "4381456d770bb42050076d00", "motherduck": "901c5133ed47d4769286e860", "mysql": "4381456d770bb42050076d00", -"postgres": "4381456d770bb42050076d00", -"postgresql": "4381456d770bb42050076d00", +"postgres": "1f89b04f39a23f492cb6e5eb", +"postgresql": "1f89b04f39a23f492cb6e5eb", "snowflake": "4381456d770bb42050076d00", "tsql": "4381456d770bb42050076d00" } @@ -8352,11 +8352,11 @@ "DuckDB": "0d58e20cd83f047b3b85cd1c", "None": "0d58e20cd83f047b3b85cd1c", "bigquery": "0d58e20cd83f047b3b85cd1c", -"databricks": "9f2957c784cb1f6b640c10f7", +"databricks": "0d58e20cd83f047b3b85cd1c", "duckdb": "0d58e20cd83f047b3b85cd1c", "generic": "0d58e20cd83f047b3b85cd1c", "motherduck": "901c5133ed47d4769286e860", -"mysql": "9f2957c784cb1f6b640c10f7", +"mysql": "0d58e20cd83f047b3b85cd1c", "postgres": "0d58e20cd83f047b3b85cd1c", "postgresql": "0d58e20cd83f047b3b85cd1c", "snowflake": "0d58e20cd83f047b3b85cd1c", @@ -8368,14 +8368,14 @@ { "DuckDB": "fe23594c781e79cbb0c8927a", "None": "fe23594c781e79cbb0c8927a", -"bigquery": "fe23594c781e79cbb0c8927a", +"bigquery": "8a3f094f18fb4047f0febf59", "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", "motherduck": "901c5133ed47d4769286e860", "mysql": "fe23594c781e79cbb0c8927a", -"postgres": "fe23594c781e79cbb0c8927a", -"postgresql": "fe23594c781e79cbb0c8927a", +"postgres": "8a3f094f18fb4047f0febf59", +"postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "fe23594c781e79cbb0c8927a" } @@ -8418,17 +8418,17 @@ "type", { "DuckDB": "919b7ec6a7b49d8527b784cd", -"None": "919b7ec6a7b49d8527b784cd", -"bigquery": "204206d1d1eecf007966cc5b", -"databricks": "e3bc1b8bad22d8104417b00f", +"None": "558da38ec4d9d5d6c72f60cf", +"bigquery": "c50a316d091c821866e4bfba", +"databricks": "558da38ec4d9d5d6c72f60cf", "duckdb": "919b7ec6a7b49d8527b784cd", -"generic": "919b7ec6a7b49d8527b784cd", +"generic": "558da38ec4d9d5d6c72f60cf", "motherduck": "901c5133ed47d4769286e860", -"mysql": "919b7ec6a7b49d8527b784cd", -"postgres": "a3b6b25265be330ec52a3322", -"postgresql": "a3b6b25265be330ec52a3322", +"mysql": "558da38ec4d9d5d6c72f60cf", +"postgres": "558da38ec4d9d5d6c72f60cf", +"postgresql": "558da38ec4d9d5d6c72f60cf", "snowflake": "919b7ec6a7b49d8527b784cd", -"tsql": "919b7ec6a7b49d8527b784cd" +"tsql": "558da38ec4d9d5d6c72f60cf" } ], [ @@ -8522,11 +8522,11 @@ "DuckDB": "0f3d33062aebbc5edec7b64f", "None": "0f3d33062aebbc5edec7b64f", "bigquery": "0f3d33062aebbc5edec7b64f", -"databricks": "544d1979b70121c2f5770b6a", +"databricks": "0f3d33062aebbc5edec7b64f", "duckdb": "0f3d33062aebbc5edec7b64f", "generic": "0f3d33062aebbc5edec7b64f", "motherduck": "901c5133ed47d4769286e860", -"mysql": "544d1979b70121c2f5770b6a", +"mysql": "0f3d33062aebbc5edec7b64f", "postgres": "0f3d33062aebbc5edec7b64f", "postgresql": "0f3d33062aebbc5edec7b64f", "snowflake": "0f3d33062aebbc5edec7b64f", @@ -8572,14 +8572,14 @@ { "DuckDB": "fe23594c781e79cbb0c8927a", "None": "fe23594c781e79cbb0c8927a", -"bigquery": "fe23594c781e79cbb0c8927a", +"bigquery": "8a3f094f18fb4047f0febf59", "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", "motherduck": "901c5133ed47d4769286e860", "mysql": "fe23594c781e79cbb0c8927a", -"postgres": "fe23594c781e79cbb0c8927a", -"postgresql": "fe23594c781e79cbb0c8927a", +"postgres": "8a3f094f18fb4047f0febf59", +"postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "fe23594c781e79cbb0c8927a" } @@ -8646,8 +8646,8 @@ "generic": "64fc778308b798a730b1d5c9", "motherduck": "901c5133ed47d4769286e860", "mysql": "64fc778308b798a730b1d5c9", -"postgres": "64fc778308b798a730b1d5c9", -"postgresql": "64fc778308b798a730b1d5c9", +"postgres": "c8ca7a940ad228dd836cad6f", +"postgresql": "c8ca7a940ad228dd836cad6f", "snowflake": "64fc778308b798a730b1d5c9", "tsql": "64fc778308b798a730b1d5c9" } @@ -8690,30 +8690,30 @@ "type", { "DuckDB": "3147a9e06daa6de4a4df5abc", -"None": "3147a9e06daa6de4a4df5abc", -"bigquery": "f276e907770196606577c932", -"databricks": "3147a9e06daa6de4a4df5abc", +"None": "da0625706f12b764a6912278", +"bigquery": "b0477b36767a62145aa9158c", +"databricks": "da0625706f12b764a6912278", "duckdb": "3147a9e06daa6de4a4df5abc", -"generic": "3147a9e06daa6de4a4df5abc", +"generic": "da0625706f12b764a6912278", "motherduck": "901c5133ed47d4769286e860", -"mysql": "3147a9e06daa6de4a4df5abc", +"mysql": "da0625706f12b764a6912278", "postgres": "3147a9e06daa6de4a4df5abc", "postgresql": "3147a9e06daa6de4a4df5abc", "snowflake": "67b5874a2c569226c490211f", -"tsql": "3147a9e06daa6de4a4df5abc" +"tsql": "da0625706f12b764a6912278" } ], [ "type", { "DuckDB": "ff99ba717583eafc6fdd9442", -"None": "a915140f475538632ca59017", -"bigquery": "a915140f475538632ca59017", -"databricks": "a915140f475538632ca59017", +"None": "3b16d8eb223ae1333ecbc2bf", +"bigquery": "3b16d8eb223ae1333ecbc2bf", +"databricks": "3b16d8eb223ae1333ecbc2bf", "duckdb": "ff99ba717583eafc6fdd9442", -"generic": "a915140f475538632ca59017", +"generic": "3b16d8eb223ae1333ecbc2bf", "motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"mysql": "3b16d8eb223ae1333ecbc2bf", "postgres": "39ac50273811909ae35979b7", "postgresql": "39ac50273811909ae35979b7", "snowflake": "ca443b97e784bced05b62f0b", @@ -8765,8 +8765,8 @@ "generic": "669084d1ea704ae5b8ee05c2", "motherduck": "901c5133ed47d4769286e860", "mysql": "669084d1ea704ae5b8ee05c2", -"postgres": "669084d1ea704ae5b8ee05c2", -"postgresql": "669084d1ea704ae5b8ee05c2", +"postgres": "93c1b5b5960e26479baf88d0", +"postgresql": "93c1b5b5960e26479baf88d0", "snowflake": "669084d1ea704ae5b8ee05c2", "tsql": "669084d1ea704ae5b8ee05c2" } @@ -8809,17 +8809,17 @@ "type", { "DuckDB": "a428302e4848ffe877284c84", -"None": "a428302e4848ffe877284c84", -"bigquery": "a428302e4848ffe877284c84", -"databricks": "a428302e4848ffe877284c84", +"None": "c9b810639795a7bfab91b98d", +"bigquery": "c9b810639795a7bfab91b98d", +"databricks": "c9b810639795a7bfab91b98d", "duckdb": "a428302e4848ffe877284c84", -"generic": "a428302e4848ffe877284c84", +"generic": "c9b810639795a7bfab91b98d", "motherduck": "901c5133ed47d4769286e860", -"mysql": "a428302e4848ffe877284c84", +"mysql": "c9b810639795a7bfab91b98d", "postgres": "a428302e4848ffe877284c84", "postgresql": "a428302e4848ffe877284c84", "snowflake": "652ff8b21f638eac8fdc46c5", -"tsql": "a428302e4848ffe877284c84" +"tsql": "c9b810639795a7bfab91b98d" } ], [ @@ -8861,7 +8861,7 @@ { "DuckDB": "d41d40c68507f4d02e348833", "None": "d41d40c68507f4d02e348833", -"bigquery": "e1c36a5cdd2427ff2b089ac9", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "d41d40c68507f4d02e348833", "duckdb": "d41d40c68507f4d02e348833", "generic": "d41d40c68507f4d02e348833", @@ -8878,14 +8878,14 @@ { "DuckDB": "18db5afe2c1e80d1ee652b54", "None": "18db5afe2c1e80d1ee652b54", -"bigquery": "e1c36a5cdd2427ff2b089ac9", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "18db5afe2c1e80d1ee652b54", "duckdb": "18db5afe2c1e80d1ee652b54", "generic": "18db5afe2c1e80d1ee652b54", "motherduck": "901c5133ed47d4769286e860", "mysql": "18db5afe2c1e80d1ee652b54", -"postgres": "18db5afe2c1e80d1ee652b54", -"postgresql": "18db5afe2c1e80d1ee652b54", +"postgres": "47d64a5994d42a2b577166ea", +"postgresql": "47d64a5994d42a2b577166ea", "snowflake": "18db5afe2c1e80d1ee652b54", "tsql": "18db5afe2c1e80d1ee652b54" } @@ -8945,17 +8945,17 @@ "type", { "DuckDB": "70fb4728c3adcf6dbe4975d5", -"None": "70fb4728c3adcf6dbe4975d5", -"bigquery": "70fb4728c3adcf6dbe4975d5", -"databricks": "70fb4728c3adcf6dbe4975d5", +"None": "fb4b6f5101f973d16ea8d1f3", +"bigquery": "fb4b6f5101f973d16ea8d1f3", +"databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", -"generic": "70fb4728c3adcf6dbe4975d5", +"generic": "fb4b6f5101f973d16ea8d1f3", "motherduck": "901c5133ed47d4769286e860", -"mysql": "70fb4728c3adcf6dbe4975d5", +"mysql": "fb4b6f5101f973d16ea8d1f3", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", -"tsql": "70fb4728c3adcf6dbe4975d5" +"tsql": "fb4b6f5101f973d16ea8d1f3" } ], [ @@ -8969,8 +8969,8 @@ "generic": "64fc778308b798a730b1d5c9", "motherduck": "901c5133ed47d4769286e860", "mysql": "64fc778308b798a730b1d5c9", -"postgres": "64fc778308b798a730b1d5c9", -"postgresql": "64fc778308b798a730b1d5c9", +"postgres": "c8ca7a940ad228dd836cad6f", +"postgresql": "c8ca7a940ad228dd836cad6f", "snowflake": "64fc778308b798a730b1d5c9", "tsql": "64fc778308b798a730b1d5c9" } @@ -9031,7 +9031,7 @@ { "DuckDB": "461923a0d34defd0c726d6e5", "None": "461923a0d34defd0c726d6e5", -"bigquery": "b5c4d5044b2a6e70633f0d0a", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "461923a0d34defd0c726d6e5", "duckdb": "461923a0d34defd0c726d6e5", "generic": "461923a0d34defd0c726d6e5", @@ -9048,14 +9048,14 @@ { "DuckDB": "d2c97d221599d038d9875873", "None": "d2c97d221599d038d9875873", -"bigquery": "d2c97d221599d038d9875873", +"bigquery": "55155766823b6a3a5e882fc2", "databricks": "d2c97d221599d038d9875873", "duckdb": "d2c97d221599d038d9875873", "generic": "d2c97d221599d038d9875873", "motherduck": "901c5133ed47d4769286e860", "mysql": "d2c97d221599d038d9875873", -"postgres": "d2c97d221599d038d9875873", -"postgresql": "d2c97d221599d038d9875873", +"postgres": "55155766823b6a3a5e882fc2", +"postgresql": "55155766823b6a3a5e882fc2", "snowflake": "d2c97d221599d038d9875873", "tsql": "d2c97d221599d038d9875873" } @@ -9064,17 +9064,17 @@ "type", { "DuckDB": "f7fca17b74cc50ae3de8c826", -"None": "f7fca17b74cc50ae3de8c826", -"bigquery": "f7fca17b74cc50ae3de8c826", -"databricks": "f7fca17b74cc50ae3de8c826", +"None": "d2c97d221599d038d9875873", +"bigquery": "55155766823b6a3a5e882fc2", +"databricks": "d2c97d221599d038d9875873", "duckdb": "f7fca17b74cc50ae3de8c826", -"generic": "f7fca17b74cc50ae3de8c826", +"generic": "d2c97d221599d038d9875873", "motherduck": "901c5133ed47d4769286e860", -"mysql": "f7fca17b74cc50ae3de8c826", -"postgres": "f7fca17b74cc50ae3de8c826", -"postgresql": "f7fca17b74cc50ae3de8c826", +"mysql": "d2c97d221599d038d9875873", +"postgres": "55155766823b6a3a5e882fc2", +"postgresql": "55155766823b6a3a5e882fc2", "snowflake": "55155766823b6a3a5e882fc2", -"tsql": "f7fca17b74cc50ae3de8c826" +"tsql": "d2c97d221599d038d9875873" } ], [ @@ -9116,14 +9116,14 @@ { "DuckDB": "b4284f955ab1914d3f556d82", "None": "b4284f955ab1914d3f556d82", -"bigquery": "b4284f955ab1914d3f556d82", +"bigquery": "c7c38fdeaeb7858a44976f1d", "databricks": "b4284f955ab1914d3f556d82", "duckdb": "b4284f955ab1914d3f556d82", "generic": "b4284f955ab1914d3f556d82", "motherduck": "901c5133ed47d4769286e860", "mysql": "b4284f955ab1914d3f556d82", -"postgres": "b4284f955ab1914d3f556d82", -"postgresql": "b4284f955ab1914d3f556d82", +"postgres": "063c794e14d2f13bb89a7a4d", +"postgresql": "063c794e14d2f13bb89a7a4d", "snowflake": "b4284f955ab1914d3f556d82", "tsql": "b4284f955ab1914d3f556d82" } @@ -9133,14 +9133,14 @@ { "DuckDB": "c84c0b1d4d7caa6744130456", "None": "c84c0b1d4d7caa6744130456", -"bigquery": "c84c0b1d4d7caa6744130456", +"bigquery": "c7c38fdeaeb7858a44976f1d", "databricks": "c84c0b1d4d7caa6744130456", "duckdb": "c84c0b1d4d7caa6744130456", "generic": "c84c0b1d4d7caa6744130456", "motherduck": "901c5133ed47d4769286e860", "mysql": "c84c0b1d4d7caa6744130456", -"postgres": "c84c0b1d4d7caa6744130456", -"postgresql": "c84c0b1d4d7caa6744130456", +"postgres": "725c73e74f311417bb4f95e8", +"postgresql": "725c73e74f311417bb4f95e8", "snowflake": "c84c0b1d4d7caa6744130456", "tsql": "c84c0b1d4d7caa6744130456" } @@ -9150,7 +9150,7 @@ { "DuckDB": "62368fc15cefd5807807b209", "None": "62368fc15cefd5807807b209", -"bigquery": "e6ffdece2977be55da73b59a", +"bigquery": "c50a316d091c821866e4bfba", "databricks": "62368fc15cefd5807807b209", "duckdb": "62368fc15cefd5807807b209", "generic": "62368fc15cefd5807807b209", @@ -9183,17 +9183,17 @@ "type", { "DuckDB": "70fb4728c3adcf6dbe4975d5", -"None": "70fb4728c3adcf6dbe4975d5", -"bigquery": "70fb4728c3adcf6dbe4975d5", -"databricks": "70fb4728c3adcf6dbe4975d5", +"None": "fb4b6f5101f973d16ea8d1f3", +"bigquery": "fb4b6f5101f973d16ea8d1f3", +"databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", -"generic": "70fb4728c3adcf6dbe4975d5", +"generic": "fb4b6f5101f973d16ea8d1f3", "motherduck": "901c5133ed47d4769286e860", -"mysql": "70fb4728c3adcf6dbe4975d5", +"mysql": "fb4b6f5101f973d16ea8d1f3", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", -"tsql": "70fb4728c3adcf6dbe4975d5" +"tsql": "fb4b6f5101f973d16ea8d1f3" } ], [ @@ -9218,14 +9218,14 @@ { "DuckDB": "73151c1398b30e5c5d18f4ce", "None": "73151c1398b30e5c5d18f4ce", -"bigquery": "73151c1398b30e5c5d18f4ce", +"bigquery": "893ff554934dfc6f8253597b", "databricks": "73151c1398b30e5c5d18f4ce", "duckdb": "73151c1398b30e5c5d18f4ce", "generic": "73151c1398b30e5c5d18f4ce", "motherduck": "901c5133ed47d4769286e860", "mysql": "73151c1398b30e5c5d18f4ce", -"postgres": "73151c1398b30e5c5d18f4ce", -"postgresql": "73151c1398b30e5c5d18f4ce", +"postgres": "f68b7dbd74ec7c3b9f3066ef", +"postgresql": "f68b7dbd74ec7c3b9f3066ef", "snowflake": "73151c1398b30e5c5d18f4ce", "tsql": "73151c1398b30e5c5d18f4ce" } @@ -9275,8 +9275,8 @@ "generic": "336fd1b7a1f7d8105c29280c", "motherduck": "901c5133ed47d4769286e860", "mysql": "336fd1b7a1f7d8105c29280c", -"postgres": "336fd1b7a1f7d8105c29280c", -"postgresql": "336fd1b7a1f7d8105c29280c", +"postgres": "9ea4932505663b43f527adad", +"postgresql": "9ea4932505663b43f527adad", "snowflake": "336fd1b7a1f7d8105c29280c", "tsql": "336fd1b7a1f7d8105c29280c" } @@ -9302,17 +9302,17 @@ "type", { "DuckDB": "647ac9829e390656c0783aba", -"None": "1404468a845477f54df838fc", +"None": "2df0f0517ee87dda5683d2bd", "bigquery": "2df0f0517ee87dda5683d2bd", "databricks": "2df0f0517ee87dda5683d2bd", "duckdb": "647ac9829e390656c0783aba", -"generic": "1404468a845477f54df838fc", +"generic": "2df0f0517ee87dda5683d2bd", "motherduck": "901c5133ed47d4769286e860", "mysql": "2df0f0517ee87dda5683d2bd", -"postgres": "ee5dc4b764837cf002345fb9", -"postgresql": "ee5dc4b764837cf002345fb9", +"postgres": "2df0f0517ee87dda5683d2bd", +"postgresql": "2df0f0517ee87dda5683d2bd", "snowflake": "8d1f13df6b799b7b301d2e69", -"tsql": "d1dc9a140dbe2eb1874c6ad7" +"tsql": "2df0f0517ee87dda5683d2bd" } ], [ @@ -9336,15 +9336,15 @@ "type", { "DuckDB": "6247229b9f890f25e7b3f0c5", -"None": "eb8ea3778b2debde09f2afb1", -"bigquery": "135a912ea74f76a35d9d0632", -"databricks": "eb8ea3778b2debde09f2afb1", +"None": "3fd7037beaaa396764e49e8b", +"bigquery": "3fd7037beaaa396764e49e8b", +"databricks": "3fd7037beaaa396764e49e8b", "duckdb": "6247229b9f890f25e7b3f0c5", -"generic": "eb8ea3778b2debde09f2afb1", +"generic": "3fd7037beaaa396764e49e8b", "motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", -"postgres": "6247229b9f890f25e7b3f0c5", -"postgresql": "6247229b9f890f25e7b3f0c5", +"mysql": "3fd7037beaaa396764e49e8b", +"postgres": "3fd7037beaaa396764e49e8b", +"postgresql": "3fd7037beaaa396764e49e8b", "snowflake": "8f60ea843371b982d069e65b", "tsql": "3fd7037beaaa396764e49e8b" } @@ -9405,14 +9405,14 @@ { "DuckDB": "4928771fd9e503a30457d160", "None": "4928771fd9e503a30457d160", -"bigquery": "4928771fd9e503a30457d160", +"bigquery": "67d251c042daee0bb2bf7351", "databricks": "4928771fd9e503a30457d160", "duckdb": "4928771fd9e503a30457d160", "generic": "4928771fd9e503a30457d160", "motherduck": "901c5133ed47d4769286e860", "mysql": "4928771fd9e503a30457d160", -"postgres": "4928771fd9e503a30457d160", -"postgresql": "4928771fd9e503a30457d160", +"postgres": "50ddfb168c08b09892261e82", +"postgresql": "50ddfb168c08b09892261e82", "snowflake": "4928771fd9e503a30457d160", "tsql": "4928771fd9e503a30457d160" } @@ -9428,8 +9428,8 @@ "generic": "b8321619b794db091c9e96a1", "motherduck": "901c5133ed47d4769286e860", "mysql": "b8321619b794db091c9e96a1", -"postgres": "b8321619b794db091c9e96a1", -"postgresql": "b8321619b794db091c9e96a1", +"postgres": "bce26544873d6ec9191c9a86", +"postgresql": "bce26544873d6ec9191c9a86", "snowflake": "b8321619b794db091c9e96a1", "tsql": "b8321619b794db091c9e96a1" } @@ -9440,11 +9440,11 @@ "DuckDB": "a0e5b4ce93e9317c3cea1cbf", "None": "a0e5b4ce93e9317c3cea1cbf", "bigquery": "a0e5b4ce93e9317c3cea1cbf", -"databricks": "c9d23fd01aedfa8b27cf08a6", +"databricks": "a0e5b4ce93e9317c3cea1cbf", "duckdb": "a0e5b4ce93e9317c3cea1cbf", "generic": "a0e5b4ce93e9317c3cea1cbf", "motherduck": "901c5133ed47d4769286e860", -"mysql": "c8a1fc1b91adcea5a885afe3", +"mysql": "a0e5b4ce93e9317c3cea1cbf", "postgres": "a0e5b4ce93e9317c3cea1cbf", "postgresql": "a0e5b4ce93e9317c3cea1cbf", "snowflake": "a0e5b4ce93e9317c3cea1cbf", @@ -9490,14 +9490,14 @@ { "DuckDB": "e46d4be44ab9e500e9190d26", "None": "e46d4be44ab9e500e9190d26", -"bigquery": "e6ffdece2977be55da73b59a", +"bigquery": "c50a316d091c821866e4bfba", "databricks": "e46d4be44ab9e500e9190d26", "duckdb": "e46d4be44ab9e500e9190d26", "generic": "e46d4be44ab9e500e9190d26", "motherduck": "901c5133ed47d4769286e860", "mysql": "e46d4be44ab9e500e9190d26", -"postgres": "e46d4be44ab9e500e9190d26", -"postgresql": "e46d4be44ab9e500e9190d26", +"postgres": "251aba3fef503ce64556c701", +"postgresql": "251aba3fef503ce64556c701", "snowflake": "e46d4be44ab9e500e9190d26", "tsql": "e46d4be44ab9e500e9190d26" } @@ -9513,8 +9513,8 @@ "generic": "3673b4fbbd8a7fed7120ec2b", "motherduck": "901c5133ed47d4769286e860", "mysql": "3673b4fbbd8a7fed7120ec2b", -"postgres": "3673b4fbbd8a7fed7120ec2b", -"postgresql": "3673b4fbbd8a7fed7120ec2b", +"postgres": "9aff7fcc7d550e1a8d105471", +"postgresql": "9aff7fcc7d550e1a8d105471", "snowflake": "3673b4fbbd8a7fed7120ec2b", "tsql": "3673b4fbbd8a7fed7120ec2b" } @@ -9523,17 +9523,17 @@ "type", { "DuckDB": "a3b6b25265be330ec52a3322", -"None": "a3b6b25265be330ec52a3322", -"bigquery": "204206d1d1eecf007966cc5b", -"databricks": "e3bc1b8bad22d8104417b00f", +"None": "62368fc15cefd5807807b209", +"bigquery": "c50a316d091c821866e4bfba", +"databricks": "62368fc15cefd5807807b209", "duckdb": "a3b6b25265be330ec52a3322", -"generic": "a3b6b25265be330ec52a3322", +"generic": "62368fc15cefd5807807b209", "motherduck": "901c5133ed47d4769286e860", -"mysql": "919b7ec6a7b49d8527b784cd", -"postgres": "a3b6b25265be330ec52a3322", -"postgresql": "a3b6b25265be330ec52a3322", +"mysql": "62368fc15cefd5807807b209", +"postgres": "62368fc15cefd5807807b209", +"postgresql": "62368fc15cefd5807807b209", "snowflake": "919b7ec6a7b49d8527b784cd", -"tsql": "a3b6b25265be330ec52a3322" +"tsql": "62368fc15cefd5807807b209" } ], [ @@ -9541,14 +9541,14 @@ { "DuckDB": "fe23594c781e79cbb0c8927a", "None": "fe23594c781e79cbb0c8927a", -"bigquery": "fe23594c781e79cbb0c8927a", +"bigquery": "8a3f094f18fb4047f0febf59", "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", "motherduck": "901c5133ed47d4769286e860", "mysql": "fe23594c781e79cbb0c8927a", -"postgres": "fe23594c781e79cbb0c8927a", -"postgresql": "fe23594c781e79cbb0c8927a", +"postgres": "8a3f094f18fb4047f0febf59", +"postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "fe23594c781e79cbb0c8927a" } @@ -9632,8 +9632,8 @@ "generic": "669084d1ea704ae5b8ee05c2", "motherduck": "901c5133ed47d4769286e860", "mysql": "669084d1ea704ae5b8ee05c2", -"postgres": "669084d1ea704ae5b8ee05c2", -"postgresql": "669084d1ea704ae5b8ee05c2", +"postgres": "93c1b5b5960e26479baf88d0", +"postgresql": "93c1b5b5960e26479baf88d0", "snowflake": "669084d1ea704ae5b8ee05c2", "tsql": "669084d1ea704ae5b8ee05c2" } @@ -9677,7 +9677,7 @@ { "DuckDB": "3e888d249494102b9ba328b2", "None": "3e888d249494102b9ba328b2", -"bigquery": "3e888d249494102b9ba328b2", +"bigquery": "c74425c5ce650ac69c83ca5b", "databricks": "3e888d249494102b9ba328b2", "duckdb": "3e888d249494102b9ba328b2", "generic": "3e888d249494102b9ba328b2", @@ -9728,7 +9728,7 @@ { "DuckDB": "0989b67047ee9c0a1d9f2cf4", "None": "0989b67047ee9c0a1d9f2cf4", -"bigquery": "0989b67047ee9c0a1d9f2cf4", +"bigquery": "e1c1429838b93039324a47f3", "databricks": "0989b67047ee9c0a1d9f2cf4", "duckdb": "0989b67047ee9c0a1d9f2cf4", "generic": "0989b67047ee9c0a1d9f2cf4", @@ -9762,14 +9762,14 @@ { "DuckDB": "3e611643f0beab4953c4e09b", "None": "3e611643f0beab4953c4e09b", -"bigquery": "2a0918be53051fe629d9ff8f", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "3e611643f0beab4953c4e09b", "duckdb": "3e611643f0beab4953c4e09b", "generic": "3e611643f0beab4953c4e09b", "motherduck": "901c5133ed47d4769286e860", "mysql": "3e611643f0beab4953c4e09b", -"postgres": "3e611643f0beab4953c4e09b", -"postgresql": "3e611643f0beab4953c4e09b", +"postgres": "cae477ce59c72869f37a9a40", +"postgresql": "cae477ce59c72869f37a9a40", "snowflake": "3e611643f0beab4953c4e09b", "tsql": "3e611643f0beab4953c4e09b" } @@ -9779,14 +9779,14 @@ { "DuckDB": "e46d4be44ab9e500e9190d26", "None": "e46d4be44ab9e500e9190d26", -"bigquery": "e6ffdece2977be55da73b59a", +"bigquery": "c50a316d091c821866e4bfba", "databricks": "e46d4be44ab9e500e9190d26", "duckdb": "e46d4be44ab9e500e9190d26", "generic": "e46d4be44ab9e500e9190d26", "motherduck": "901c5133ed47d4769286e860", "mysql": "e46d4be44ab9e500e9190d26", -"postgres": "e46d4be44ab9e500e9190d26", -"postgresql": "e46d4be44ab9e500e9190d26", +"postgres": "251aba3fef503ce64556c701", +"postgresql": "251aba3fef503ce64556c701", "snowflake": "e46d4be44ab9e500e9190d26", "tsql": "e46d4be44ab9e500e9190d26" } @@ -9819,8 +9819,8 @@ "generic": "e2879cf4770e7f48d2dd704a", "motherduck": "901c5133ed47d4769286e860", "mysql": "e2879cf4770e7f48d2dd704a", -"postgres": "e2879cf4770e7f48d2dd704a", -"postgresql": "e2879cf4770e7f48d2dd704a", +"postgres": "2d4aea8dec5521e258596f3d", +"postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", "tsql": "e2879cf4770e7f48d2dd704a" } @@ -9830,14 +9830,14 @@ { "DuckDB": "b6b82efd03def9465e4f97b2", "None": "b6b82efd03def9465e4f97b2", -"bigquery": "b6b82efd03def9465e4f97b2", +"bigquery": "c74425c5ce650ac69c83ca5b", "databricks": "b6b82efd03def9465e4f97b2", "duckdb": "b6b82efd03def9465e4f97b2", "generic": "b6b82efd03def9465e4f97b2", "motherduck": "901c5133ed47d4769286e860", "mysql": "b6b82efd03def9465e4f97b2", -"postgres": "b6b82efd03def9465e4f97b2", -"postgresql": "b6b82efd03def9465e4f97b2", +"postgres": "a3923c3d2c49043629c52bf6", +"postgresql": "a3923c3d2c49043629c52bf6", "snowflake": "b6b82efd03def9465e4f97b2", "tsql": "b6b82efd03def9465e4f97b2" } @@ -9853,8 +9853,8 @@ "generic": "e2879cf4770e7f48d2dd704a", "motherduck": "901c5133ed47d4769286e860", "mysql": "e2879cf4770e7f48d2dd704a", -"postgres": "e2879cf4770e7f48d2dd704a", -"postgresql": "e2879cf4770e7f48d2dd704a", +"postgres": "2d4aea8dec5521e258596f3d", +"postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", "tsql": "e2879cf4770e7f48d2dd704a" } @@ -9870,8 +9870,8 @@ "generic": "80c23e10e486925cf3b33f9d", "motherduck": "901c5133ed47d4769286e860", "mysql": "80c23e10e486925cf3b33f9d", -"postgres": "80c23e10e486925cf3b33f9d", -"postgresql": "80c23e10e486925cf3b33f9d", +"postgres": "30466b1eff038acb4463e46d", +"postgresql": "30466b1eff038acb4463e46d", "snowflake": "80c23e10e486925cf3b33f9d", "tsql": "80c23e10e486925cf3b33f9d" } @@ -9932,14 +9932,14 @@ { "DuckDB": "c4346d0257336219f1d26890", "None": "c4346d0257336219f1d26890", -"bigquery": "b5c4d5044b2a6e70633f0d0a", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "c4346d0257336219f1d26890", "duckdb": "c4346d0257336219f1d26890", "generic": "c4346d0257336219f1d26890", "motherduck": "901c5133ed47d4769286e860", "mysql": "c4346d0257336219f1d26890", -"postgres": "c4346d0257336219f1d26890", -"postgresql": "c4346d0257336219f1d26890", +"postgres": "19bdd92ea0cc5fb028657510", +"postgresql": "19bdd92ea0cc5fb028657510", "snowflake": "4e24a80b20b9dee0cbbfac17", "tsql": "c4346d0257336219f1d26890" } @@ -9983,14 +9983,14 @@ { "DuckDB": "d2c97d221599d038d9875873", "None": "d2c97d221599d038d9875873", -"bigquery": "d2c97d221599d038d9875873", +"bigquery": "55155766823b6a3a5e882fc2", "databricks": "d2c97d221599d038d9875873", "duckdb": "d2c97d221599d038d9875873", "generic": "d2c97d221599d038d9875873", "motherduck": "901c5133ed47d4769286e860", "mysql": "d2c97d221599d038d9875873", -"postgres": "d2c97d221599d038d9875873", -"postgresql": "d2c97d221599d038d9875873", +"postgres": "55155766823b6a3a5e882fc2", +"postgresql": "55155766823b6a3a5e882fc2", "snowflake": "d2c97d221599d038d9875873", "tsql": "d2c97d221599d038d9875873" } @@ -10006,8 +10006,8 @@ "generic": "d227b99a82e505d599c2ed8a", "motherduck": "901c5133ed47d4769286e860", "mysql": "d227b99a82e505d599c2ed8a", -"postgres": "d227b99a82e505d599c2ed8a", -"postgresql": "d227b99a82e505d599c2ed8a", +"postgres": "35a9c5c1419bff507cf65ae2", +"postgresql": "35a9c5c1419bff507cf65ae2", "snowflake": "d227b99a82e505d599c2ed8a", "tsql": "d227b99a82e505d599c2ed8a" } @@ -10050,17 +10050,17 @@ "type", { "DuckDB": "70fb4728c3adcf6dbe4975d5", -"None": "70fb4728c3adcf6dbe4975d5", -"bigquery": "70fb4728c3adcf6dbe4975d5", -"databricks": "70fb4728c3adcf6dbe4975d5", +"None": "fb4b6f5101f973d16ea8d1f3", +"bigquery": "fb4b6f5101f973d16ea8d1f3", +"databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", -"generic": "70fb4728c3adcf6dbe4975d5", +"generic": "fb4b6f5101f973d16ea8d1f3", "motherduck": "901c5133ed47d4769286e860", -"mysql": "70fb4728c3adcf6dbe4975d5", +"mysql": "fb4b6f5101f973d16ea8d1f3", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", -"tsql": "70fb4728c3adcf6dbe4975d5" +"tsql": "fb4b6f5101f973d16ea8d1f3" } ], [ @@ -10074,8 +10074,8 @@ "generic": "e46d4be44ab9e500e9190d26", "motherduck": "901c5133ed47d4769286e860", "mysql": "e46d4be44ab9e500e9190d26", -"postgres": "e46d4be44ab9e500e9190d26", -"postgresql": "e46d4be44ab9e500e9190d26", +"postgres": "251aba3fef503ce64556c701", +"postgresql": "251aba3fef503ce64556c701", "snowflake": "e46d4be44ab9e500e9190d26", "tsql": "e46d4be44ab9e500e9190d26" } @@ -10219,16 +10219,16 @@ [ "type", { -"DuckDB": "8698d724534891869075175e", -"None": "0017a88bb60ec3403c786ed8", +"DuckDB": "aa7afbd57560be67e5221a3b", +"None": "47e0816fecdd9e015d7aeb79", "bigquery": "47e0816fecdd9e015d7aeb79", -"databricks": "0017a88bb60ec3403c786ed8", -"duckdb": "8698d724534891869075175e", -"generic": "0017a88bb60ec3403c786ed8", +"databricks": "47e0816fecdd9e015d7aeb79", +"duckdb": "aa7afbd57560be67e5221a3b", +"generic": "47e0816fecdd9e015d7aeb79", "motherduck": "901c5133ed47d4769286e860", "mysql": "6c03a3450ea4742ee7396e3c", -"postgres": "8698d724534891869075175e", -"postgresql": "8698d724534891869075175e", +"postgres": "aa7afbd57560be67e5221a3b", +"postgresql": "aa7afbd57560be67e5221a3b", "snowflake": "e6f36041b1d9fb8fe3a18cbb", "tsql": "364031fff119e6530478e415" } @@ -10278,8 +10278,8 @@ "generic": "6f763a99e9c69109526b40c0", "motherduck": "901c5133ed47d4769286e860", "mysql": "6f763a99e9c69109526b40c0", -"postgres": "6f763a99e9c69109526b40c0", -"postgresql": "6f763a99e9c69109526b40c0", +"postgres": "5226d7e2a62c7650a87d6e84", +"postgresql": "5226d7e2a62c7650a87d6e84", "snowflake": "2705f19942a4194a72576911", "tsql": "6f763a99e9c69109526b40c0" } @@ -10312,8 +10312,8 @@ "generic": "f78e07518606fbc42e0f28fe", "motherduck": "901c5133ed47d4769286e860", "mysql": "f78e07518606fbc42e0f28fe", -"postgres": "f78e07518606fbc42e0f28fe", -"postgresql": "f78e07518606fbc42e0f28fe", +"postgres": "726bc47de786f118c37530f7", +"postgresql": "726bc47de786f118c37530f7", "snowflake": "f78e07518606fbc42e0f28fe", "tsql": "f78e07518606fbc42e0f28fe" } @@ -10355,18 +10355,18 @@ [ "type", { -"DuckDB": "f20ec02865769c13aa2d2b67", -"None": "f20ec02865769c13aa2d2b67", -"bigquery": "8575088c2f44d4349e1307a4", -"databricks": "e40ec5056d50c25089ee3934", -"duckdb": "f20ec02865769c13aa2d2b67", -"generic": "f20ec02865769c13aa2d2b67", +"DuckDB": "143fb490be175b5b233c87a4", +"None": "143fb490be175b5b233c87a4", +"bigquery": "5391753213125132f9ba8258", +"databricks": "e95f9763c510e80ddb1422a0", +"duckdb": "143fb490be175b5b233c87a4", +"generic": "143fb490be175b5b233c87a4", "motherduck": "901c5133ed47d4769286e860", -"mysql": "f20ec02865769c13aa2d2b67", -"postgres": "f20ec02865769c13aa2d2b67", -"postgresql": "f20ec02865769c13aa2d2b67", -"snowflake": "e5502f3bf9aad4d98960c7b4", -"tsql": "f20ec02865769c13aa2d2b67" +"mysql": "143fb490be175b5b233c87a4", +"postgres": "143fb490be175b5b233c87a4", +"postgresql": "143fb490be175b5b233c87a4", +"snowflake": "e6d0f723aa7559449fb75d84", +"tsql": "143fb490be175b5b233c87a4" } ], [ @@ -10431,8 +10431,8 @@ "generic": "d7bedd3c7663f140f4d10bd4", "motherduck": "901c5133ed47d4769286e860", "mysql": "d7bedd3c7663f140f4d10bd4", -"postgres": "d7bedd3c7663f140f4d10bd4", -"postgresql": "d7bedd3c7663f140f4d10bd4", +"postgres": "d7e01d31db59d1d0507cd75c", +"postgresql": "d7e01d31db59d1d0507cd75c", "snowflake": "d7bedd3c7663f140f4d10bd4", "tsql": "d7bedd3c7663f140f4d10bd4" } @@ -10442,14 +10442,14 @@ { "DuckDB": "d2c97d221599d038d9875873", "None": "d2c97d221599d038d9875873", -"bigquery": "d2c97d221599d038d9875873", +"bigquery": "55155766823b6a3a5e882fc2", "databricks": "d2c97d221599d038d9875873", "duckdb": "d2c97d221599d038d9875873", "generic": "d2c97d221599d038d9875873", "motherduck": "901c5133ed47d4769286e860", "mysql": "d2c97d221599d038d9875873", -"postgres": "d2c97d221599d038d9875873", -"postgresql": "d2c97d221599d038d9875873", +"postgres": "55155766823b6a3a5e882fc2", +"postgresql": "55155766823b6a3a5e882fc2", "snowflake": "d2c97d221599d038d9875873", "tsql": "d2c97d221599d038d9875873" } @@ -10510,7 +10510,7 @@ { "DuckDB": "af57dd2575fa5a01917ec9cf", "None": "af57dd2575fa5a01917ec9cf", -"bigquery": "af57dd2575fa5a01917ec9cf", +"bigquery": "c74425c5ce650ac69c83ca5b", "databricks": "af57dd2575fa5a01917ec9cf", "duckdb": "af57dd2575fa5a01917ec9cf", "generic": "af57dd2575fa5a01917ec9cf", @@ -10578,14 +10578,14 @@ { "DuckDB": "aab0389e2b5470136660bf8a", "None": "aab0389e2b5470136660bf8a", -"bigquery": "c9b810639795a7bfab91b98d", +"bigquery": "a428302e4848ffe877284c84", "databricks": "aab0389e2b5470136660bf8a", "duckdb": "aab0389e2b5470136660bf8a", "generic": "aab0389e2b5470136660bf8a", "motherduck": "901c5133ed47d4769286e860", "mysql": "aab0389e2b5470136660bf8a", -"postgres": "aab0389e2b5470136660bf8a", -"postgresql": "aab0389e2b5470136660bf8a", +"postgres": "f16105cbc61d547e6c08efc8", +"postgresql": "f16105cbc61d547e6c08efc8", "snowflake": "97a20344d811fb77c7690863", "tsql": "aab0389e2b5470136660bf8a" } @@ -10612,7 +10612,7 @@ { "DuckDB": "aae2b1fe6f2d9807ecde412b", "None": "aae2b1fe6f2d9807ecde412b", -"bigquery": "e1c36a5cdd2427ff2b089ac9", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "aae2b1fe6f2d9807ecde412b", "duckdb": "aae2b1fe6f2d9807ecde412b", "generic": "aae2b1fe6f2d9807ecde412b", @@ -10713,17 +10713,17 @@ "type", { "DuckDB": "5abf4ec799a2ec32fb10da7e", -"None": "5abf4ec799a2ec32fb10da7e", -"bigquery": "5abf4ec799a2ec32fb10da7e", -"databricks": "5abf4ec799a2ec32fb10da7e", +"None": "4b356542044f1267fcfc487a", +"bigquery": "a5656d70e208254d1d3e9b4f", +"databricks": "4b356542044f1267fcfc487a", "duckdb": "5abf4ec799a2ec32fb10da7e", -"generic": "5abf4ec799a2ec32fb10da7e", +"generic": "4b356542044f1267fcfc487a", "motherduck": "901c5133ed47d4769286e860", -"mysql": "5abf4ec799a2ec32fb10da7e", -"postgres": "5abf4ec799a2ec32fb10da7e", -"postgresql": "5abf4ec799a2ec32fb10da7e", +"mysql": "4b356542044f1267fcfc487a", +"postgres": "a5656d70e208254d1d3e9b4f", +"postgresql": "a5656d70e208254d1d3e9b4f", "snowflake": "a5656d70e208254d1d3e9b4f", -"tsql": "5abf4ec799a2ec32fb10da7e" +"tsql": "4b356542044f1267fcfc487a" } ], [ @@ -10731,7 +10731,7 @@ { "DuckDB": "461923a0d34defd0c726d6e5", "None": "461923a0d34defd0c726d6e5", -"bigquery": "b5c4d5044b2a6e70633f0d0a", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "461923a0d34defd0c726d6e5", "duckdb": "461923a0d34defd0c726d6e5", "generic": "461923a0d34defd0c726d6e5", @@ -10765,14 +10765,14 @@ { "DuckDB": "158229f1c3f1085b80d3190e", "None": "158229f1c3f1085b80d3190e", -"bigquery": "158229f1c3f1085b80d3190e", +"bigquery": "c74425c5ce650ac69c83ca5b", "databricks": "158229f1c3f1085b80d3190e", "duckdb": "158229f1c3f1085b80d3190e", "generic": "158229f1c3f1085b80d3190e", "motherduck": "901c5133ed47d4769286e860", "mysql": "158229f1c3f1085b80d3190e", -"postgres": "158229f1c3f1085b80d3190e", -"postgresql": "158229f1c3f1085b80d3190e", +"postgres": "7a1a9517a8cf96477b67c17e", +"postgresql": "7a1a9517a8cf96477b67c17e", "snowflake": "158229f1c3f1085b80d3190e", "tsql": "158229f1c3f1085b80d3190e" } @@ -10788,8 +10788,8 @@ "generic": "e3bc1b8bad22d8104417b00f", "motherduck": "901c5133ed47d4769286e860", "mysql": "e3bc1b8bad22d8104417b00f", -"postgres": "111af35848fe500ec2ae54b2", -"postgresql": "111af35848fe500ec2ae54b2", +"postgres": "b7d6fc051a3c1ea6deb29ff4", +"postgresql": "b7d6fc051a3c1ea6deb29ff4", "snowflake": "e3bc1b8bad22d8104417b00f", "tsql": "e3bc1b8bad22d8104417b00f" } @@ -10798,17 +10798,17 @@ "type", { "DuckDB": "70fb4728c3adcf6dbe4975d5", -"None": "70fb4728c3adcf6dbe4975d5", -"bigquery": "70fb4728c3adcf6dbe4975d5", -"databricks": "70fb4728c3adcf6dbe4975d5", +"None": "fb4b6f5101f973d16ea8d1f3", +"bigquery": "fb4b6f5101f973d16ea8d1f3", +"databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", -"generic": "70fb4728c3adcf6dbe4975d5", +"generic": "fb4b6f5101f973d16ea8d1f3", "motherduck": "901c5133ed47d4769286e860", -"mysql": "70fb4728c3adcf6dbe4975d5", +"mysql": "fb4b6f5101f973d16ea8d1f3", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", -"tsql": "70fb4728c3adcf6dbe4975d5" +"tsql": "fb4b6f5101f973d16ea8d1f3" } ], [ @@ -10816,14 +10816,14 @@ { "DuckDB": "9bb8e9bdc03c6262fc0b887e", "None": "9bb8e9bdc03c6262fc0b887e", -"bigquery": "9bb8e9bdc03c6262fc0b887e", +"bigquery": "67d251c042daee0bb2bf7351", "databricks": "9bb8e9bdc03c6262fc0b887e", "duckdb": "9bb8e9bdc03c6262fc0b887e", "generic": "9bb8e9bdc03c6262fc0b887e", "motherduck": "901c5133ed47d4769286e860", "mysql": "9bb8e9bdc03c6262fc0b887e", -"postgres": "9bb8e9bdc03c6262fc0b887e", -"postgresql": "9bb8e9bdc03c6262fc0b887e", +"postgres": "f2ce9dc78f83119cd19850e3", +"postgresql": "f2ce9dc78f83119cd19850e3", "snowflake": "9bb8e9bdc03c6262fc0b887e", "tsql": "9bb8e9bdc03c6262fc0b887e" } @@ -10832,17 +10832,17 @@ "type", { "DuckDB": "70fb4728c3adcf6dbe4975d5", -"None": "70fb4728c3adcf6dbe4975d5", -"bigquery": "70fb4728c3adcf6dbe4975d5", -"databricks": "70fb4728c3adcf6dbe4975d5", +"None": "fb4b6f5101f973d16ea8d1f3", +"bigquery": "fb4b6f5101f973d16ea8d1f3", +"databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", -"generic": "70fb4728c3adcf6dbe4975d5", +"generic": "fb4b6f5101f973d16ea8d1f3", "motherduck": "901c5133ed47d4769286e860", -"mysql": "70fb4728c3adcf6dbe4975d5", +"mysql": "fb4b6f5101f973d16ea8d1f3", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", -"tsql": "70fb4728c3adcf6dbe4975d5" +"tsql": "fb4b6f5101f973d16ea8d1f3" } ], [ @@ -10850,14 +10850,14 @@ { "DuckDB": "b69a57be16780e0a5e4acadb", "None": "b69a57be16780e0a5e4acadb", -"bigquery": "b69a57be16780e0a5e4acadb", +"bigquery": "67d251c042daee0bb2bf7351", "databricks": "b69a57be16780e0a5e4acadb", "duckdb": "b69a57be16780e0a5e4acadb", "generic": "b69a57be16780e0a5e4acadb", "motherduck": "901c5133ed47d4769286e860", "mysql": "b69a57be16780e0a5e4acadb", -"postgres": "b69a57be16780e0a5e4acadb", -"postgresql": "b69a57be16780e0a5e4acadb", +"postgres": "a7d1c1767c3d4e900c7d3cb4", +"postgresql": "a7d1c1767c3d4e900c7d3cb4", "snowflake": "b69a57be16780e0a5e4acadb", "tsql": "b69a57be16780e0a5e4acadb" } @@ -10907,8 +10907,8 @@ "generic": "f4fc387af06ec14d0ed37c37", "motherduck": "901c5133ed47d4769286e860", "mysql": "f4fc387af06ec14d0ed37c37", -"postgres": "f4fc387af06ec14d0ed37c37", -"postgresql": "f4fc387af06ec14d0ed37c37", +"postgres": "6e561b85b14713ee50331da1", +"postgresql": "6e561b85b14713ee50331da1", "snowflake": "f4fc387af06ec14d0ed37c37", "tsql": "f4fc387af06ec14d0ed37c37" } @@ -10975,8 +10975,8 @@ "generic": "279ea7f6f6a3910de04b795b", "motherduck": "901c5133ed47d4769286e860", "mysql": "279ea7f6f6a3910de04b795b", -"postgres": "279ea7f6f6a3910de04b795b", -"postgresql": "279ea7f6f6a3910de04b795b", +"postgres": "bc520aebd5872e40030ec7d9", +"postgresql": "bc520aebd5872e40030ec7d9", "snowflake": "279ea7f6f6a3910de04b795b", "tsql": "279ea7f6f6a3910de04b795b" } @@ -10986,7 +10986,7 @@ { "DuckDB": "04f0b885283ff5fc129874ad", "None": "04f0b885283ff5fc129874ad", -"bigquery": "04f0b885283ff5fc129874ad", +"bigquery": "e1c1429838b93039324a47f3", "databricks": "04f0b885283ff5fc129874ad", "duckdb": "04f0b885283ff5fc129874ad", "generic": "04f0b885283ff5fc129874ad", @@ -11001,14 +11001,14 @@ [ "type", { -"DuckDB": "c6654395c62d21b415299f72", +"DuckDB": "0712c33a016f7dd6a37ee011", "None": "c6654395c62d21b415299f72", "bigquery": "0712c33a016f7dd6a37ee011", "databricks": "c6654395c62d21b415299f72", -"duckdb": "c6654395c62d21b415299f72", +"duckdb": "0712c33a016f7dd6a37ee011", "generic": "c6654395c62d21b415299f72", "motherduck": "901c5133ed47d4769286e860", -"mysql": "2978a8fe925c7f93c6e9fcfb", +"mysql": "c6654395c62d21b415299f72", "postgres": "c6654395c62d21b415299f72", "postgresql": "c6654395c62d21b415299f72", "snowflake": "0712c33a016f7dd6a37ee011", @@ -11019,17 +11019,17 @@ "type", { "DuckDB": "70fb4728c3adcf6dbe4975d5", -"None": "70fb4728c3adcf6dbe4975d5", -"bigquery": "70fb4728c3adcf6dbe4975d5", -"databricks": "70fb4728c3adcf6dbe4975d5", +"None": "fb4b6f5101f973d16ea8d1f3", +"bigquery": "fb4b6f5101f973d16ea8d1f3", +"databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", -"generic": "70fb4728c3adcf6dbe4975d5", +"generic": "fb4b6f5101f973d16ea8d1f3", "motherduck": "901c5133ed47d4769286e860", -"mysql": "70fb4728c3adcf6dbe4975d5", +"mysql": "fb4b6f5101f973d16ea8d1f3", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", -"tsql": "70fb4728c3adcf6dbe4975d5" +"tsql": "fb4b6f5101f973d16ea8d1f3" } ], [ @@ -11094,8 +11094,8 @@ "generic": "eea9a25a6fbf8e049a445513", "motherduck": "901c5133ed47d4769286e860", "mysql": "eea9a25a6fbf8e049a445513", -"postgres": "eea9a25a6fbf8e049a445513", -"postgresql": "eea9a25a6fbf8e049a445513", +"postgres": "769f762f4c7cffb2990ddacb", +"postgresql": "769f762f4c7cffb2990ddacb", "snowflake": "eea9a25a6fbf8e049a445513", "tsql": "eea9a25a6fbf8e049a445513" } @@ -11173,14 +11173,14 @@ { "DuckDB": "9bb8e9bdc03c6262fc0b887e", "None": "9bb8e9bdc03c6262fc0b887e", -"bigquery": "9bb8e9bdc03c6262fc0b887e", +"bigquery": "67d251c042daee0bb2bf7351", "databricks": "9bb8e9bdc03c6262fc0b887e", "duckdb": "9bb8e9bdc03c6262fc0b887e", "generic": "9bb8e9bdc03c6262fc0b887e", "motherduck": "901c5133ed47d4769286e860", "mysql": "9bb8e9bdc03c6262fc0b887e", -"postgres": "9bb8e9bdc03c6262fc0b887e", -"postgresql": "9bb8e9bdc03c6262fc0b887e", +"postgres": "f2ce9dc78f83119cd19850e3", +"postgresql": "f2ce9dc78f83119cd19850e3", "snowflake": "9bb8e9bdc03c6262fc0b887e", "tsql": "9bb8e9bdc03c6262fc0b887e" } @@ -11247,8 +11247,8 @@ "generic": "bf5a83d0c02f8890941c7ec8", "motherduck": "901c5133ed47d4769286e860", "mysql": "bf5a83d0c02f8890941c7ec8", -"postgres": "bf5a83d0c02f8890941c7ec8", -"postgresql": "bf5a83d0c02f8890941c7ec8", +"postgres": "ef750421d1e193fa61992c79", +"postgresql": "ef750421d1e193fa61992c79", "snowflake": "bf5a83d0c02f8890941c7ec8", "tsql": "bf5a83d0c02f8890941c7ec8" } @@ -11257,15 +11257,15 @@ "type", { "DuckDB": "4c16382f7ad617ac3cbfa833", -"None": "0f63c150afc0bb5bbf7763ba", -"bigquery": "a4d165ed06d0b1b815192ef4", -"databricks": "0f63c150afc0bb5bbf7763ba", +"None": "accf46359aa8c74483b9a197", +"bigquery": "accf46359aa8c74483b9a197", +"databricks": "accf46359aa8c74483b9a197", "duckdb": "4c16382f7ad617ac3cbfa833", -"generic": "0f63c150afc0bb5bbf7763ba", +"generic": "accf46359aa8c74483b9a197", "motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", -"postgres": "4c16382f7ad617ac3cbfa833", -"postgresql": "4c16382f7ad617ac3cbfa833", +"mysql": "accf46359aa8c74483b9a197", +"postgres": "accf46359aa8c74483b9a197", +"postgresql": "accf46359aa8c74483b9a197", "snowflake": "303d6e0feaedc557ea64e414", "tsql": "accf46359aa8c74483b9a197" } @@ -11274,17 +11274,17 @@ "type", { "DuckDB": "f2ed52f6bf46b4db9343551c", -"None": "f2ed52f6bf46b4db9343551c", -"bigquery": "f10bdd4d40eb495cbcecd31d", -"databricks": "f2ed52f6bf46b4db9343551c", +"None": "3f998a9ab68d8b2514840489", +"bigquery": "1e75e5227bea0ae294ac7af1", +"databricks": "3f998a9ab68d8b2514840489", "duckdb": "f2ed52f6bf46b4db9343551c", -"generic": "f2ed52f6bf46b4db9343551c", +"generic": "3f998a9ab68d8b2514840489", "motherduck": "901c5133ed47d4769286e860", -"mysql": "f2ed52f6bf46b4db9343551c", -"postgres": "f2ed52f6bf46b4db9343551c", -"postgresql": "f2ed52f6bf46b4db9343551c", +"mysql": "3f998a9ab68d8b2514840489", +"postgres": "3f998a9ab68d8b2514840489", +"postgresql": "3f998a9ab68d8b2514840489", "snowflake": "f2ed52f6bf46b4db9343551c", -"tsql": "f2ed52f6bf46b4db9343551c" +"tsql": "3f998a9ab68d8b2514840489" } ], [ @@ -11292,14 +11292,14 @@ { "DuckDB": "c84c0b1d4d7caa6744130456", "None": "c84c0b1d4d7caa6744130456", -"bigquery": "c84c0b1d4d7caa6744130456", +"bigquery": "c7c38fdeaeb7858a44976f1d", "databricks": "c84c0b1d4d7caa6744130456", "duckdb": "c84c0b1d4d7caa6744130456", "generic": "c84c0b1d4d7caa6744130456", "motherduck": "901c5133ed47d4769286e860", "mysql": "c84c0b1d4d7caa6744130456", -"postgres": "c84c0b1d4d7caa6744130456", -"postgresql": "c84c0b1d4d7caa6744130456", +"postgres": "725c73e74f311417bb4f95e8", +"postgresql": "725c73e74f311417bb4f95e8", "snowflake": "c84c0b1d4d7caa6744130456", "tsql": "c84c0b1d4d7caa6744130456" } @@ -11309,14 +11309,14 @@ { "DuckDB": "aea8a680fa18770e56bf6220", "None": "aea8a680fa18770e56bf6220", -"bigquery": "aea8a680fa18770e56bf6220", +"bigquery": "c74425c5ce650ac69c83ca5b", "databricks": "aea8a680fa18770e56bf6220", "duckdb": "aea8a680fa18770e56bf6220", "generic": "aea8a680fa18770e56bf6220", "motherduck": "901c5133ed47d4769286e860", "mysql": "aea8a680fa18770e56bf6220", -"postgres": "aea8a680fa18770e56bf6220", -"postgresql": "aea8a680fa18770e56bf6220", +"postgres": "836641bf4537dec3e767931d", +"postgresql": "836641bf4537dec3e767931d", "snowflake": "aea8a680fa18770e56bf6220", "tsql": "aea8a680fa18770e56bf6220" } @@ -11332,8 +11332,8 @@ "generic": "19a1a80ffc06169f32694de8", "motherduck": "901c5133ed47d4769286e860", "mysql": "19a1a80ffc06169f32694de8", -"postgres": "19a1a80ffc06169f32694de8", -"postgresql": "19a1a80ffc06169f32694de8", +"postgres": "ab1f5f46e69284778bd66d58", +"postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", "tsql": "19a1a80ffc06169f32694de8" } @@ -11399,7 +11399,7 @@ "duckdb": "c7b697bbfa82dd2821ad64e9", "generic": "c7b697bbfa82dd2821ad64e9", "motherduck": "901c5133ed47d4769286e860", -"mysql": "dcb00b7053ffe69fe3ce99cd", +"mysql": "c7b697bbfa82dd2821ad64e9", "postgres": "c7b697bbfa82dd2821ad64e9", "postgresql": "c7b697bbfa82dd2821ad64e9", "snowflake": "c7b697bbfa82dd2821ad64e9", @@ -11451,8 +11451,8 @@ "generic": "871969531da28b86361e65d2", "motherduck": "901c5133ed47d4769286e860", "mysql": "871969531da28b86361e65d2", -"postgres": "871969531da28b86361e65d2", -"postgresql": "871969531da28b86361e65d2", +"postgres": "990e37f96d40e397fe74978c", +"postgresql": "990e37f96d40e397fe74978c", "snowflake": "871969531da28b86361e65d2", "tsql": "871969531da28b86361e65d2" } @@ -11604,8 +11604,8 @@ "generic": "fdc9a2fc03f4b67d24982d0a", "motherduck": "901c5133ed47d4769286e860", "mysql": "fdc9a2fc03f4b67d24982d0a", -"postgres": "fdc9a2fc03f4b67d24982d0a", -"postgresql": "fdc9a2fc03f4b67d24982d0a", +"postgres": "576eb7ad8e7eca2db50ec386", +"postgresql": "576eb7ad8e7eca2db50ec386", "snowflake": "fdc9a2fc03f4b67d24982d0a", "tsql": "fdc9a2fc03f4b67d24982d0a" } @@ -11614,17 +11614,17 @@ "type", { "DuckDB": "5c8b98f516cedefc237fa721", -"None": "5c8b98f516cedefc237fa721", -"bigquery": "5c8b98f516cedefc237fa721", -"databricks": "5c8b98f516cedefc237fa721", +"None": "81a45386abae5f6be0a027d0", +"bigquery": "09b7f1f9111bc2e0767cc059", +"databricks": "81a45386abae5f6be0a027d0", "duckdb": "5c8b98f516cedefc237fa721", -"generic": "5c8b98f516cedefc237fa721", +"generic": "81a45386abae5f6be0a027d0", "motherduck": "901c5133ed47d4769286e860", -"mysql": "5c8b98f516cedefc237fa721", +"mysql": "81a45386abae5f6be0a027d0", "postgres": "5c8b98f516cedefc237fa721", "postgresql": "5c8b98f516cedefc237fa721", "snowflake": "5c8b98f516cedefc237fa721", -"tsql": "5c8b98f516cedefc237fa721" +"tsql": "81a45386abae5f6be0a027d0" } ], [ @@ -11751,14 +11751,14 @@ { "DuckDB": "b561884132dbe7e42b719110", "None": "b561884132dbe7e42b719110", -"bigquery": "b1b23029bedaa16e2a6f8516", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "b561884132dbe7e42b719110", "duckdb": "b561884132dbe7e42b719110", "generic": "b561884132dbe7e42b719110", "motherduck": "901c5133ed47d4769286e860", "mysql": "b561884132dbe7e42b719110", -"postgres": "b561884132dbe7e42b719110", -"postgresql": "b561884132dbe7e42b719110", +"postgres": "5aa51d84ac5e6c7a9306f348", +"postgresql": "5aa51d84ac5e6c7a9306f348", "snowflake": "b561884132dbe7e42b719110", "tsql": "b561884132dbe7e42b719110" } @@ -11774,8 +11774,8 @@ "generic": "c6106f30f23f392e17eadef4", "motherduck": "901c5133ed47d4769286e860", "mysql": "c6106f30f23f392e17eadef4", -"postgres": "c6106f30f23f392e17eadef4", -"postgresql": "c6106f30f23f392e17eadef4", +"postgres": "f43dac38f34a1c0618a3f812", +"postgresql": "f43dac38f34a1c0618a3f812", "snowflake": "c6106f30f23f392e17eadef4", "tsql": "c6106f30f23f392e17eadef4" } @@ -11802,14 +11802,14 @@ { "DuckDB": "881106a4a0d2de0ff7c56373", "None": "881106a4a0d2de0ff7c56373", -"bigquery": "881106a4a0d2de0ff7c56373", +"bigquery": "893ff554934dfc6f8253597b", "databricks": "881106a4a0d2de0ff7c56373", "duckdb": "881106a4a0d2de0ff7c56373", "generic": "881106a4a0d2de0ff7c56373", "motherduck": "901c5133ed47d4769286e860", "mysql": "881106a4a0d2de0ff7c56373", -"postgres": "881106a4a0d2de0ff7c56373", -"postgresql": "881106a4a0d2de0ff7c56373", +"postgres": "cf167ec395fc843602e3b1e0", +"postgresql": "cf167ec395fc843602e3b1e0", "snowflake": "881106a4a0d2de0ff7c56373", "tsql": "881106a4a0d2de0ff7c56373" } @@ -11825,8 +11825,8 @@ "generic": "2c6b371b030464f525066893", "motherduck": "901c5133ed47d4769286e860", "mysql": "2c6b371b030464f525066893", -"postgres": "2c6b371b030464f525066893", -"postgresql": "2c6b371b030464f525066893", +"postgres": "3feb479faeec777c2ade8afd", +"postgresql": "3feb479faeec777c2ade8afd", "snowflake": "2c6b371b030464f525066893", "tsql": "2c6b371b030464f525066893" } @@ -11837,11 +11837,11 @@ "DuckDB": "e3d9a113277b9c69432b32d1", "None": "e3d9a113277b9c69432b32d1", "bigquery": "e3d9a113277b9c69432b32d1", -"databricks": "4cda99191af9adfbe91e70f6", +"databricks": "e3d9a113277b9c69432b32d1", "duckdb": "e3d9a113277b9c69432b32d1", "generic": "e3d9a113277b9c69432b32d1", "motherduck": "901c5133ed47d4769286e860", -"mysql": "4cda99191af9adfbe91e70f6", +"mysql": "e3d9a113277b9c69432b32d1", "postgres": "e3d9a113277b9c69432b32d1", "postgresql": "e3d9a113277b9c69432b32d1", "snowflake": "e3d9a113277b9c69432b32d1", @@ -11876,8 +11876,8 @@ "generic": "e57ac1541fb51b362cd5b470", "motherduck": "901c5133ed47d4769286e860", "mysql": "e57ac1541fb51b362cd5b470", -"postgres": "e57ac1541fb51b362cd5b470", -"postgresql": "e57ac1541fb51b362cd5b470", +"postgres": "396b2ce32efb5365b393c230", +"postgresql": "396b2ce32efb5365b393c230", "snowflake": "4b356542044f1267fcfc487a", "tsql": "e57ac1541fb51b362cd5b470" } @@ -11886,17 +11886,17 @@ "type", { "DuckDB": "5abf4ec799a2ec32fb10da7e", -"None": "5abf4ec799a2ec32fb10da7e", +"None": "de89632e277dbc8c97115419", "bigquery": "5abf4ec799a2ec32fb10da7e", -"databricks": "5abf4ec799a2ec32fb10da7e", +"databricks": "de89632e277dbc8c97115419", "duckdb": "5abf4ec799a2ec32fb10da7e", -"generic": "5abf4ec799a2ec32fb10da7e", +"generic": "de89632e277dbc8c97115419", "motherduck": "901c5133ed47d4769286e860", -"mysql": "5abf4ec799a2ec32fb10da7e", +"mysql": "de89632e277dbc8c97115419", "postgres": "5abf4ec799a2ec32fb10da7e", "postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "a5656d70e208254d1d3e9b4f", -"tsql": "5abf4ec799a2ec32fb10da7e" +"tsql": "de89632e277dbc8c97115419" } ], [ @@ -11909,7 +11909,7 @@ "duckdb": "e060b5a5a8e9774b580d58d3", "generic": "e060b5a5a8e9774b580d58d3", "motherduck": "901c5133ed47d4769286e860", -"mysql": "319a9fbb02ed503eb114164b", +"mysql": "e060b5a5a8e9774b580d58d3", "postgres": "e060b5a5a8e9774b580d58d3", "postgresql": "e060b5a5a8e9774b580d58d3", "snowflake": "e060b5a5a8e9774b580d58d3", @@ -11937,17 +11937,17 @@ "type", { "DuckDB": "919b7ec6a7b49d8527b784cd", -"None": "919b7ec6a7b49d8527b784cd", -"bigquery": "204206d1d1eecf007966cc5b", -"databricks": "e3bc1b8bad22d8104417b00f", +"None": "558da38ec4d9d5d6c72f60cf", +"bigquery": "c50a316d091c821866e4bfba", +"databricks": "558da38ec4d9d5d6c72f60cf", "duckdb": "919b7ec6a7b49d8527b784cd", -"generic": "919b7ec6a7b49d8527b784cd", +"generic": "558da38ec4d9d5d6c72f60cf", "motherduck": "901c5133ed47d4769286e860", -"mysql": "919b7ec6a7b49d8527b784cd", -"postgres": "a3b6b25265be330ec52a3322", -"postgresql": "a3b6b25265be330ec52a3322", +"mysql": "558da38ec4d9d5d6c72f60cf", +"postgres": "558da38ec4d9d5d6c72f60cf", +"postgresql": "558da38ec4d9d5d6c72f60cf", "snowflake": "919b7ec6a7b49d8527b784cd", -"tsql": "919b7ec6a7b49d8527b784cd" +"tsql": "558da38ec4d9d5d6c72f60cf" } ], [ @@ -11995,8 +11995,8 @@ "generic": "e2879cf4770e7f48d2dd704a", "motherduck": "901c5133ed47d4769286e860", "mysql": "e2879cf4770e7f48d2dd704a", -"postgres": "e2879cf4770e7f48d2dd704a", -"postgresql": "e2879cf4770e7f48d2dd704a", +"postgres": "2d4aea8dec5521e258596f3d", +"postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", "tsql": "e2879cf4770e7f48d2dd704a" } @@ -12029,8 +12029,8 @@ "generic": "19a1a80ffc06169f32694de8", "motherduck": "901c5133ed47d4769286e860", "mysql": "19a1a80ffc06169f32694de8", -"postgres": "19a1a80ffc06169f32694de8", -"postgresql": "19a1a80ffc06169f32694de8", +"postgres": "ab1f5f46e69284778bd66d58", +"postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", "tsql": "19a1a80ffc06169f32694de8" } @@ -12073,17 +12073,17 @@ "type", { "DuckDB": "70fb4728c3adcf6dbe4975d5", -"None": "70fb4728c3adcf6dbe4975d5", -"bigquery": "70fb4728c3adcf6dbe4975d5", -"databricks": "70fb4728c3adcf6dbe4975d5", +"None": "fb4b6f5101f973d16ea8d1f3", +"bigquery": "fb4b6f5101f973d16ea8d1f3", +"databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", -"generic": "70fb4728c3adcf6dbe4975d5", +"generic": "fb4b6f5101f973d16ea8d1f3", "motherduck": "901c5133ed47d4769286e860", -"mysql": "70fb4728c3adcf6dbe4975d5", +"mysql": "fb4b6f5101f973d16ea8d1f3", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", -"tsql": "70fb4728c3adcf6dbe4975d5" +"tsql": "fb4b6f5101f973d16ea8d1f3" } ], [ @@ -12091,14 +12091,14 @@ { "DuckDB": "aab0389e2b5470136660bf8a", "None": "aab0389e2b5470136660bf8a", -"bigquery": "c9b810639795a7bfab91b98d", +"bigquery": "a428302e4848ffe877284c84", "databricks": "aab0389e2b5470136660bf8a", "duckdb": "aab0389e2b5470136660bf8a", "generic": "aab0389e2b5470136660bf8a", "motherduck": "901c5133ed47d4769286e860", "mysql": "aab0389e2b5470136660bf8a", -"postgres": "aab0389e2b5470136660bf8a", -"postgresql": "aab0389e2b5470136660bf8a", +"postgres": "f16105cbc61d547e6c08efc8", +"postgresql": "f16105cbc61d547e6c08efc8", "snowflake": "97a20344d811fb77c7690863", "tsql": "aab0389e2b5470136660bf8a" } @@ -12125,14 +12125,14 @@ { "DuckDB": "776e4bf23c8ef10ffaceade2", "None": "776e4bf23c8ef10ffaceade2", -"bigquery": "776e4bf23c8ef10ffaceade2", +"bigquery": "c7c38fdeaeb7858a44976f1d", "databricks": "776e4bf23c8ef10ffaceade2", "duckdb": "776e4bf23c8ef10ffaceade2", "generic": "776e4bf23c8ef10ffaceade2", "motherduck": "901c5133ed47d4769286e860", "mysql": "776e4bf23c8ef10ffaceade2", -"postgres": "776e4bf23c8ef10ffaceade2", -"postgresql": "776e4bf23c8ef10ffaceade2", +"postgres": "5d06b10843e3e6487fb9906a", +"postgresql": "5d06b10843e3e6487fb9906a", "snowflake": "776e4bf23c8ef10ffaceade2", "tsql": "776e4bf23c8ef10ffaceade2" } @@ -12165,8 +12165,8 @@ "generic": "a3acd7ba35e81a800f967bd3", "motherduck": "901c5133ed47d4769286e860", "mysql": "a3acd7ba35e81a800f967bd3", -"postgres": "a3acd7ba35e81a800f967bd3", -"postgresql": "a3acd7ba35e81a800f967bd3", +"postgres": "42d4a23d9a52e79d0c3458d3", +"postgresql": "42d4a23d9a52e79d0c3458d3", "snowflake": "a3acd7ba35e81a800f967bd3", "tsql": "a3acd7ba35e81a800f967bd3" } @@ -12176,14 +12176,14 @@ { "DuckDB": "95050a5e73c2bff179a785b7", "None": "95050a5e73c2bff179a785b7", -"bigquery": "95050a5e73c2bff179a785b7", +"bigquery": "837468174ef3e5cb4e4b4070", "databricks": "95050a5e73c2bff179a785b7", "duckdb": "95050a5e73c2bff179a785b7", "generic": "95050a5e73c2bff179a785b7", "motherduck": "901c5133ed47d4769286e860", "mysql": "95050a5e73c2bff179a785b7", -"postgres": "95050a5e73c2bff179a785b7", -"postgresql": "95050a5e73c2bff179a785b7", +"postgres": "e197cb69fc5e5754a25d4038", +"postgresql": "e197cb69fc5e5754a25d4038", "snowflake": "95050a5e73c2bff179a785b7", "tsql": "95050a5e73c2bff179a785b7" } @@ -12208,17 +12208,17 @@ [ "type", { -"DuckDB": "89cc3662f602a13aef39eb80", -"None": "f2bbf59a9c02f59ca89e66b6", -"bigquery": "f2bbf59a9c02f59ca89e66b6", -"databricks": "f2bbf59a9c02f59ca89e66b6", -"duckdb": "89cc3662f602a13aef39eb80", -"generic": "f2bbf59a9c02f59ca89e66b6", +"DuckDB": "01e3dfc024c37a8d62c7f810", +"None": "3a4d8bca5124769db23a43df", +"bigquery": "3a4d8bca5124769db23a43df", +"databricks": "3a4d8bca5124769db23a43df", +"duckdb": "01e3dfc024c37a8d62c7f810", +"generic": "3a4d8bca5124769db23a43df", "motherduck": "901c5133ed47d4769286e860", "mysql": "6c03a3450ea4742ee7396e3c", -"postgres": "89cc3662f602a13aef39eb80", -"postgresql": "89cc3662f602a13aef39eb80", -"snowflake": "6f88ef12d95c26bf8f0bd9e5", +"postgres": "01e3dfc024c37a8d62c7f810", +"postgresql": "01e3dfc024c37a8d62c7f810", +"snowflake": "db03b5ccf153c75e43846a6a", "tsql": "ffadc5a4dbb5d511f5cde8d7" } ], @@ -12227,14 +12227,14 @@ { "DuckDB": "aab0389e2b5470136660bf8a", "None": "aab0389e2b5470136660bf8a", -"bigquery": "c9b810639795a7bfab91b98d", +"bigquery": "a428302e4848ffe877284c84", "databricks": "aab0389e2b5470136660bf8a", "duckdb": "aab0389e2b5470136660bf8a", "generic": "aab0389e2b5470136660bf8a", "motherduck": "901c5133ed47d4769286e860", "mysql": "aab0389e2b5470136660bf8a", -"postgres": "aab0389e2b5470136660bf8a", -"postgresql": "aab0389e2b5470136660bf8a", +"postgres": "f16105cbc61d547e6c08efc8", +"postgresql": "f16105cbc61d547e6c08efc8", "snowflake": "97a20344d811fb77c7690863", "tsql": "aab0389e2b5470136660bf8a" } @@ -12301,8 +12301,8 @@ "generic": "279ea7f6f6a3910de04b795b", "motherduck": "901c5133ed47d4769286e860", "mysql": "279ea7f6f6a3910de04b795b", -"postgres": "279ea7f6f6a3910de04b795b", -"postgresql": "279ea7f6f6a3910de04b795b", +"postgres": "bc520aebd5872e40030ec7d9", +"postgresql": "bc520aebd5872e40030ec7d9", "snowflake": "279ea7f6f6a3910de04b795b", "tsql": "279ea7f6f6a3910de04b795b" } @@ -12312,7 +12312,7 @@ { "DuckDB": "461923a0d34defd0c726d6e5", "None": "461923a0d34defd0c726d6e5", -"bigquery": "b5c4d5044b2a6e70633f0d0a", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "461923a0d34defd0c726d6e5", "duckdb": "461923a0d34defd0c726d6e5", "generic": "461923a0d34defd0c726d6e5", @@ -12328,17 +12328,17 @@ "type", { "DuckDB": "84fca9ac893777959cfd715c", -"None": "84fca9ac893777959cfd715c", -"bigquery": "f28a96f0eb38ad8874f6ec5d", -"databricks": "84fca9ac893777959cfd715c", +"None": "e57ac1541fb51b362cd5b470", +"bigquery": "e57ac1541fb51b362cd5b470", +"databricks": "e57ac1541fb51b362cd5b470", "duckdb": "84fca9ac893777959cfd715c", -"generic": "84fca9ac893777959cfd715c", +"generic": "e57ac1541fb51b362cd5b470", "motherduck": "901c5133ed47d4769286e860", -"mysql": "84fca9ac893777959cfd715c", -"postgres": "84fca9ac893777959cfd715c", -"postgresql": "84fca9ac893777959cfd715c", +"mysql": "e57ac1541fb51b362cd5b470", +"postgres": "e57ac1541fb51b362cd5b470", +"postgresql": "e57ac1541fb51b362cd5b470", "snowflake": "84fca9ac893777959cfd715c", -"tsql": "6349bc82604b762638e35788" +"tsql": "e57ac1541fb51b362cd5b470" } ], [ @@ -12385,7 +12385,7 @@ "duckdb": "8305d75697b54f509bbc0f53", "generic": "80723d96ed6ed21034faa9bd", "motherduck": "901c5133ed47d4769286e860", -"mysql": "f80980deec760e5b50e05376", +"mysql": "6c03a3450ea4742ee7396e3c", "postgres": "8305d75697b54f509bbc0f53", "postgresql": "8305d75697b54f509bbc0f53", "snowflake": "240e7de80b095b23fdb1fd92", @@ -12397,7 +12397,7 @@ { "DuckDB": "d41d40c68507f4d02e348833", "None": "d41d40c68507f4d02e348833", -"bigquery": "e1c36a5cdd2427ff2b089ac9", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "d41d40c68507f4d02e348833", "duckdb": "d41d40c68507f4d02e348833", "generic": "d41d40c68507f4d02e348833", @@ -12413,17 +12413,17 @@ "type", { "DuckDB": "2c44dc35fd1ef8701101802e", -"None": "2c44dc35fd1ef8701101802e", -"bigquery": "a428302e4848ffe877284c84", -"databricks": "2c44dc35fd1ef8701101802e", +"None": "8441d00030df06ad6a5534d3", +"bigquery": "c9b810639795a7bfab91b98d", +"databricks": "8441d00030df06ad6a5534d3", "duckdb": "2c44dc35fd1ef8701101802e", -"generic": "2c44dc35fd1ef8701101802e", +"generic": "8441d00030df06ad6a5534d3", "motherduck": "901c5133ed47d4769286e860", -"mysql": "2c44dc35fd1ef8701101802e", -"postgres": "2c44dc35fd1ef8701101802e", -"postgresql": "2c44dc35fd1ef8701101802e", +"mysql": "8441d00030df06ad6a5534d3", +"postgres": "8441d00030df06ad6a5534d3", +"postgresql": "8441d00030df06ad6a5534d3", "snowflake": "652ff8b21f638eac8fdc46c5", -"tsql": "2c44dc35fd1ef8701101802e" +"tsql": "8441d00030df06ad6a5534d3" } ], [ @@ -12431,14 +12431,14 @@ { "DuckDB": "599b3293ed620803ce2401b5", "None": "599b3293ed620803ce2401b5", -"bigquery": "599b3293ed620803ce2401b5", +"bigquery": "c7c38fdeaeb7858a44976f1d", "databricks": "599b3293ed620803ce2401b5", "duckdb": "599b3293ed620803ce2401b5", "generic": "599b3293ed620803ce2401b5", "motherduck": "901c5133ed47d4769286e860", "mysql": "599b3293ed620803ce2401b5", -"postgres": "599b3293ed620803ce2401b5", -"postgresql": "599b3293ed620803ce2401b5", +"postgres": "363a6b7f05b0dac69e1e6547", +"postgresql": "363a6b7f05b0dac69e1e6547", "snowflake": "599b3293ed620803ce2401b5", "tsql": "599b3293ed620803ce2401b5" } @@ -12497,16 +12497,16 @@ [ "type", { -"DuckDB": "8731ee5ed03816bf6b4a8464", +"DuckDB": "9c4e957c3856f1159283313a", "None": "8731ee5ed03816bf6b4a8464", "bigquery": "657a69dabb6a9b2c7b834d41", "databricks": "606d813c3f1dfcf4921045c8", -"duckdb": "8731ee5ed03816bf6b4a8464", +"duckdb": "9c4e957c3856f1159283313a", "generic": "8731ee5ed03816bf6b4a8464", "motherduck": "901c5133ed47d4769286e860", "mysql": "b9f1a78bcda3bc968346cc10", -"postgres": "8731ee5ed03816bf6b4a8464", -"postgresql": "8731ee5ed03816bf6b4a8464", +"postgres": "9c4e957c3856f1159283313a", +"postgresql": "9c4e957c3856f1159283313a", "snowflake": "cd2eb5ae8b54388fde5d5737", "tsql": "8619573249fac6f54d0bc771" } @@ -12539,8 +12539,8 @@ "generic": "19a1a80ffc06169f32694de8", "motherduck": "901c5133ed47d4769286e860", "mysql": "19a1a80ffc06169f32694de8", -"postgres": "19a1a80ffc06169f32694de8", -"postgresql": "19a1a80ffc06169f32694de8", +"postgres": "ab1f5f46e69284778bd66d58", +"postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", "tsql": "19a1a80ffc06169f32694de8" } @@ -12565,18 +12565,18 @@ [ "type", { -"DuckDB": "9ebaaf01ad47894d796ac621", -"None": "00caaa5ad0660c8df2165071", +"DuckDB": "0ceb777141e874f9277f0f53", +"None": "18cb3eafdbb91b315055426b", "bigquery": "18cb3eafdbb91b315055426b", -"databricks": "7321b96df327a001959868b6", -"duckdb": "9ebaaf01ad47894d796ac621", -"generic": "00caaa5ad0660c8df2165071", +"databricks": "c77c5b64930f0a65094fd575", +"duckdb": "0ceb777141e874f9277f0f53", +"generic": "18cb3eafdbb91b315055426b", "motherduck": "901c5133ed47d4769286e860", -"mysql": "00caaa5ad0660c8df2165071", -"postgres": "00caaa5ad0660c8df2165071", -"postgresql": "00caaa5ad0660c8df2165071", +"mysql": "18cb3eafdbb91b315055426b", +"postgres": "18cb3eafdbb91b315055426b", +"postgresql": "18cb3eafdbb91b315055426b", "snowflake": "d5bfcc48faca639b47d37309", -"tsql": "00caaa5ad0660c8df2165071" +"tsql": "18cb3eafdbb91b315055426b" } ], [ @@ -12600,34 +12600,34 @@ "type", { "DuckDB": "7a79790250ede37cbc407ce6", -"None": "4425df9f96e51bc0984eab91", -"bigquery": "135a912ea74f76a35d9d0632", -"databricks": "4425df9f96e51bc0984eab91", +"None": "398f4aeaabc39054d76e214e", +"bigquery": "398f4aeaabc39054d76e214e", +"databricks": "398f4aeaabc39054d76e214e", "duckdb": "7a79790250ede37cbc407ce6", -"generic": "4425df9f96e51bc0984eab91", +"generic": "398f4aeaabc39054d76e214e", "motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", -"postgres": "7a79790250ede37cbc407ce6", -"postgresql": "7a79790250ede37cbc407ce6", -"snowflake": "061ecb71b8d0a25345bb4dc5", -"tsql": "d1dc9a140dbe2eb1874c6ad7" +"mysql": "398f4aeaabc39054d76e214e", +"postgres": "398f4aeaabc39054d76e214e", +"postgresql": "398f4aeaabc39054d76e214e", +"snowflake": "398f4aeaabc39054d76e214e", +"tsql": "398f4aeaabc39054d76e214e" } ], [ "type", { "DuckDB": "c50a316d091c821866e4bfba", -"None": "c50a316d091c821866e4bfba", -"bigquery": "c50a316d091c821866e4bfba", -"databricks": "c50a316d091c821866e4bfba", +"None": "e6ffdece2977be55da73b59a", +"bigquery": "e6ffdece2977be55da73b59a", +"databricks": "e6ffdece2977be55da73b59a", "duckdb": "c50a316d091c821866e4bfba", -"generic": "c50a316d091c821866e4bfba", +"generic": "e6ffdece2977be55da73b59a", "motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"mysql": "e6ffdece2977be55da73b59a", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", -"tsql": "c50a316d091c821866e4bfba" +"tsql": "e6ffdece2977be55da73b59a" } ], [ @@ -12641,8 +12641,8 @@ "generic": "d227b99a82e505d599c2ed8a", "motherduck": "901c5133ed47d4769286e860", "mysql": "d227b99a82e505d599c2ed8a", -"postgres": "d227b99a82e505d599c2ed8a", -"postgresql": "d227b99a82e505d599c2ed8a", +"postgres": "35a9c5c1419bff507cf65ae2", +"postgresql": "35a9c5c1419bff507cf65ae2", "snowflake": "d227b99a82e505d599c2ed8a", "tsql": "d227b99a82e505d599c2ed8a" } @@ -12652,7 +12652,7 @@ { "DuckDB": "4ed568ea1adb2c8a7141be20", "None": "b981394a3f4d961a44e73868", -"bigquery": "b981394a3f4d961a44e73868", +"bigquery": "135a912ea74f76a35d9d0632", "databricks": "b981394a3f4d961a44e73868", "duckdb": "4ed568ea1adb2c8a7141be20", "generic": "b981394a3f4d961a44e73868", @@ -12702,32 +12702,32 @@ "type", { "DuckDB": "f46cf4b4e131aa122b5b33d6", -"None": "f46cf4b4e131aa122b5b33d6", -"bigquery": "f46cf4b4e131aa122b5b33d6", -"databricks": "f46cf4b4e131aa122b5b33d6", +"None": "f774d5e8feadbf11edaa26b1", +"bigquery": "9bafb08d41fd1c208b5deca4", +"databricks": "f774d5e8feadbf11edaa26b1", "duckdb": "f46cf4b4e131aa122b5b33d6", -"generic": "f46cf4b4e131aa122b5b33d6", +"generic": "f774d5e8feadbf11edaa26b1", "motherduck": "901c5133ed47d4769286e860", -"mysql": "f46cf4b4e131aa122b5b33d6", +"mysql": "f774d5e8feadbf11edaa26b1", "postgres": "f46cf4b4e131aa122b5b33d6", "postgresql": "f46cf4b4e131aa122b5b33d6", "snowflake": "f46cf4b4e131aa122b5b33d6", -"tsql": "f46cf4b4e131aa122b5b33d6" +"tsql": "f774d5e8feadbf11edaa26b1" } ], [ "type", { -"DuckDB": "4172129e8d84a0da597bc612", +"DuckDB": "0da0fad3107f7a26744c9411", "None": "4172129e8d84a0da597bc612", "bigquery": "d138b3b3355094a33a14355a", "databricks": "4172129e8d84a0da597bc612", -"duckdb": "4172129e8d84a0da597bc612", +"duckdb": "0da0fad3107f7a26744c9411", "generic": "4172129e8d84a0da597bc612", "motherduck": "901c5133ed47d4769286e860", "mysql": "6c03a3450ea4742ee7396e3c", -"postgres": "4172129e8d84a0da597bc612", -"postgresql": "4172129e8d84a0da597bc612", +"postgres": "0da0fad3107f7a26744c9411", +"postgresql": "0da0fad3107f7a26744c9411", "snowflake": "e21aafb74f6e5c5e181abd5e", "tsql": "2e261f2f5a01e0e0f4a34a7a" } @@ -12769,18 +12769,18 @@ [ "type", { -"DuckDB": "67b955e24db58c73ccf37441", -"None": "7390a6531648a24d91197039", +"DuckDB": "8a72102ccfafa4befa09244b", +"None": "41beda08d4df864cb9dd8402", "bigquery": "41beda08d4df864cb9dd8402", "databricks": "41beda08d4df864cb9dd8402", -"duckdb": "67b955e24db58c73ccf37441", -"generic": "7390a6531648a24d91197039", +"duckdb": "8a72102ccfafa4befa09244b", +"generic": "41beda08d4df864cb9dd8402", "motherduck": "901c5133ed47d4769286e860", "mysql": "41beda08d4df864cb9dd8402", -"postgres": "4d6c7e32aac20e9e4fb2369e", -"postgresql": "4d6c7e32aac20e9e4fb2369e", +"postgres": "41beda08d4df864cb9dd8402", +"postgresql": "41beda08d4df864cb9dd8402", "snowflake": "bae18d740565e7e0e416c8f3", -"tsql": "762b596d819e7050cff38fb8" +"tsql": "41beda08d4df864cb9dd8402" } ], [ @@ -12788,14 +12788,14 @@ { "DuckDB": "c4346d0257336219f1d26890", "None": "c4346d0257336219f1d26890", -"bigquery": "b5c4d5044b2a6e70633f0d0a", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "c4346d0257336219f1d26890", "duckdb": "c4346d0257336219f1d26890", "generic": "c4346d0257336219f1d26890", "motherduck": "901c5133ed47d4769286e860", "mysql": "c4346d0257336219f1d26890", -"postgres": "c4346d0257336219f1d26890", -"postgresql": "c4346d0257336219f1d26890", +"postgres": "19bdd92ea0cc5fb028657510", +"postgresql": "19bdd92ea0cc5fb028657510", "snowflake": "4e24a80b20b9dee0cbbfac17", "tsql": "c4346d0257336219f1d26890" } @@ -12862,8 +12862,8 @@ "generic": "336fd1b7a1f7d8105c29280c", "motherduck": "901c5133ed47d4769286e860", "mysql": "336fd1b7a1f7d8105c29280c", -"postgres": "336fd1b7a1f7d8105c29280c", -"postgresql": "336fd1b7a1f7d8105c29280c", +"postgres": "9ea4932505663b43f527adad", +"postgresql": "9ea4932505663b43f527adad", "snowflake": "336fd1b7a1f7d8105c29280c", "tsql": "336fd1b7a1f7d8105c29280c" } @@ -12879,8 +12879,8 @@ "generic": "0960c90223e1eca15361360f", "motherduck": "901c5133ed47d4769286e860", "mysql": "0960c90223e1eca15361360f", -"postgres": "0960c90223e1eca15361360f", -"postgresql": "0960c90223e1eca15361360f", +"postgres": "b72367ce0764c938c620a44c", +"postgresql": "b72367ce0764c938c620a44c", "snowflake": "0960c90223e1eca15361360f", "tsql": "0960c90223e1eca15361360f" } @@ -12924,14 +12924,14 @@ { "DuckDB": "fe23594c781e79cbb0c8927a", "None": "fe23594c781e79cbb0c8927a", -"bigquery": "fe23594c781e79cbb0c8927a", +"bigquery": "8a3f094f18fb4047f0febf59", "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", "motherduck": "901c5133ed47d4769286e860", "mysql": "fe23594c781e79cbb0c8927a", -"postgres": "fe23594c781e79cbb0c8927a", -"postgresql": "fe23594c781e79cbb0c8927a", +"postgres": "8a3f094f18fb4047f0febf59", +"postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "fe23594c781e79cbb0c8927a" } @@ -12946,7 +12946,7 @@ "duckdb": "66f7ca02161349d637fa7d4e", "generic": "66f7ca02161349d637fa7d4e", "motherduck": "901c5133ed47d4769286e860", -"mysql": "d90a78129f7050a540984f6f", +"mysql": "66f7ca02161349d637fa7d4e", "postgres": "66f7ca02161349d637fa7d4e", "postgresql": "66f7ca02161349d637fa7d4e", "snowflake": "66f7ca02161349d637fa7d4e", @@ -12956,18 +12956,18 @@ [ "type", { -"DuckDB": "6b7315cdb8da9cc8682fe582", -"None": "5db36cde08c21101926533c6", -"bigquery": "be1298bd832c06e2fbed16cc", -"databricks": "68c9a65b028d0f4e1e431d2b", -"duckdb": "6b7315cdb8da9cc8682fe582", -"generic": "5db36cde08c21101926533c6", +"DuckDB": "20949d5f5c24adb922f1df86", +"None": "8c977bea8b7e2befaacba9a7", +"bigquery": "8c977bea8b7e2befaacba9a7", +"databricks": "8c977bea8b7e2befaacba9a7", +"duckdb": "20949d5f5c24adb922f1df86", +"generic": "8c977bea8b7e2befaacba9a7", "motherduck": "901c5133ed47d4769286e860", -"mysql": "3a28322a1d9fe5f73ddf21a6", -"postgres": "627a2125b73642795b93f3ec", -"postgresql": "627a2125b73642795b93f3ec", +"mysql": "8c977bea8b7e2befaacba9a7", +"postgres": "8c977bea8b7e2befaacba9a7", +"postgresql": "8c977bea8b7e2befaacba9a7", "snowflake": "c990ce1e11ec35da455f1547", -"tsql": "5db36cde08c21101926533c6" +"tsql": "8c977bea8b7e2befaacba9a7" } ], [ @@ -12981,8 +12981,8 @@ "generic": "26e466cea9d3c66c226e9bbb", "motherduck": "901c5133ed47d4769286e860", "mysql": "26e466cea9d3c66c226e9bbb", -"postgres": "26e466cea9d3c66c226e9bbb", -"postgresql": "26e466cea9d3c66c226e9bbb", +"postgres": "a1fa261c560b019889bf4835", +"postgresql": "a1fa261c560b019889bf4835", "snowflake": "26e466cea9d3c66c226e9bbb", "tsql": "26e466cea9d3c66c226e9bbb" } @@ -12991,34 +12991,34 @@ "type", { "DuckDB": "31efc7b307bffec917380e8e", -"None": "31efc7b307bffec917380e8e", -"bigquery": "31efc7b307bffec917380e8e", -"databricks": "31efc7b307bffec917380e8e", +"None": "ed874ecba919fbd97b7f6780", +"bigquery": "ed874ecba919fbd97b7f6780", +"databricks": "ed874ecba919fbd97b7f6780", "duckdb": "31efc7b307bffec917380e8e", -"generic": "31efc7b307bffec917380e8e", +"generic": "ed874ecba919fbd97b7f6780", "motherduck": "901c5133ed47d4769286e860", -"mysql": "31efc7b307bffec917380e8e", +"mysql": "ed874ecba919fbd97b7f6780", "postgres": "31efc7b307bffec917380e8e", "postgresql": "31efc7b307bffec917380e8e", "snowflake": "42d4a23d9a52e79d0c3458d3", -"tsql": "31efc7b307bffec917380e8e" +"tsql": "ed874ecba919fbd97b7f6780" } ], [ "type", { "DuckDB": "2f9ac8bd6831b78665301f10", -"None": "2f9ac8bd6831b78665301f10", -"bigquery": "2f9ac8bd6831b78665301f10", -"databricks": "2f9ac8bd6831b78665301f10", +"None": "c557f7ce51bc180332354184", +"bigquery": "09b7f1f9111bc2e0767cc059", +"databricks": "c557f7ce51bc180332354184", "duckdb": "2f9ac8bd6831b78665301f10", -"generic": "2f9ac8bd6831b78665301f10", +"generic": "c557f7ce51bc180332354184", "motherduck": "901c5133ed47d4769286e860", -"mysql": "2f9ac8bd6831b78665301f10", +"mysql": "c557f7ce51bc180332354184", "postgres": "2f9ac8bd6831b78665301f10", "postgresql": "2f9ac8bd6831b78665301f10", "snowflake": "2f9ac8bd6831b78665301f10", -"tsql": "2f9ac8bd6831b78665301f10" +"tsql": "c557f7ce51bc180332354184" } ], [ @@ -13168,8 +13168,8 @@ "generic": "de89632e277dbc8c97115419", "motherduck": "901c5133ed47d4769286e860", "mysql": "de89632e277dbc8c97115419", -"postgres": "de89632e277dbc8c97115419", -"postgresql": "de89632e277dbc8c97115419", +"postgres": "5abf4ec799a2ec32fb10da7e", +"postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", "tsql": "de89632e277dbc8c97115419" } @@ -13178,16 +13178,16 @@ "type", { "DuckDB": "7a79790250ede37cbc407ce6", -"None": "4425df9f96e51bc0984eab91", -"bigquery": "135a912ea74f76a35d9d0632", -"databricks": "4425df9f96e51bc0984eab91", +"None": "45809055304b61252670382f", +"bigquery": "45809055304b61252670382f", +"databricks": "45809055304b61252670382f", "duckdb": "7a79790250ede37cbc407ce6", -"generic": "4425df9f96e51bc0984eab91", +"generic": "45809055304b61252670382f", "motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", -"postgres": "7a79790250ede37cbc407ce6", -"postgresql": "7a79790250ede37cbc407ce6", -"snowflake": "061ecb71b8d0a25345bb4dc5", +"mysql": "45809055304b61252670382f", +"postgres": "45809055304b61252670382f", +"postgresql": "45809055304b61252670382f", +"snowflake": "45809055304b61252670382f", "tsql": "45809055304b61252670382f" } ], @@ -13201,7 +13201,7 @@ "duckdb": "c7b697bbfa82dd2821ad64e9", "generic": "c7b697bbfa82dd2821ad64e9", "motherduck": "901c5133ed47d4769286e860", -"mysql": "dcb00b7053ffe69fe3ce99cd", +"mysql": "c7b697bbfa82dd2821ad64e9", "postgres": "c7b697bbfa82dd2821ad64e9", "postgresql": "c7b697bbfa82dd2821ad64e9", "snowflake": "c7b697bbfa82dd2821ad64e9", @@ -13270,8 +13270,8 @@ "generic": "18db5afe2c1e80d1ee652b54", "motherduck": "901c5133ed47d4769286e860", "mysql": "18db5afe2c1e80d1ee652b54", -"postgres": "18db5afe2c1e80d1ee652b54", -"postgresql": "18db5afe2c1e80d1ee652b54", +"postgres": "47d64a5994d42a2b577166ea", +"postgresql": "47d64a5994d42a2b577166ea", "snowflake": "18db5afe2c1e80d1ee652b54", "tsql": "18db5afe2c1e80d1ee652b54" } @@ -13366,14 +13366,14 @@ { "DuckDB": "e46d4be44ab9e500e9190d26", "None": "e46d4be44ab9e500e9190d26", -"bigquery": "e6ffdece2977be55da73b59a", +"bigquery": "c50a316d091c821866e4bfba", "databricks": "e46d4be44ab9e500e9190d26", "duckdb": "e46d4be44ab9e500e9190d26", "generic": "e46d4be44ab9e500e9190d26", "motherduck": "901c5133ed47d4769286e860", "mysql": "e46d4be44ab9e500e9190d26", -"postgres": "e46d4be44ab9e500e9190d26", -"postgresql": "e46d4be44ab9e500e9190d26", +"postgres": "251aba3fef503ce64556c701", +"postgresql": "251aba3fef503ce64556c701", "snowflake": "e46d4be44ab9e500e9190d26", "tsql": "e46d4be44ab9e500e9190d26" } @@ -13383,7 +13383,7 @@ { "DuckDB": "d0c95c7a10940244afe0eb6c", "None": "d0c95c7a10940244afe0eb6c", -"bigquery": "23ee196b7bd6039855a1460f", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "d0c95c7a10940244afe0eb6c", "duckdb": "d0c95c7a10940244afe0eb6c", "generic": "d0c95c7a10940244afe0eb6c", @@ -13400,7 +13400,7 @@ { "DuckDB": "5c28019dce48a74708c90a76", "None": "5c28019dce48a74708c90a76", -"bigquery": "f28a96f0eb38ad8874f6ec5d", +"bigquery": "5c28019dce48a74708c90a76", "databricks": "5c28019dce48a74708c90a76", "duckdb": "5c28019dce48a74708c90a76", "generic": "5c28019dce48a74708c90a76", @@ -13457,8 +13457,8 @@ "generic": "2705f19942a4194a72576911", "motherduck": "901c5133ed47d4769286e860", "mysql": "2705f19942a4194a72576911", -"postgres": "2705f19942a4194a72576911", -"postgresql": "2705f19942a4194a72576911", +"postgres": "5226d7e2a62c7650a87d6e84", +"postgresql": "5226d7e2a62c7650a87d6e84", "snowflake": "2705f19942a4194a72576911", "tsql": "2705f19942a4194a72576911" } @@ -13485,14 +13485,14 @@ { "DuckDB": "de89632e277dbc8c97115419", "None": "de89632e277dbc8c97115419", -"bigquery": "de89632e277dbc8c97115419", +"bigquery": "5abf4ec799a2ec32fb10da7e", "databricks": "de89632e277dbc8c97115419", "duckdb": "de89632e277dbc8c97115419", "generic": "de89632e277dbc8c97115419", "motherduck": "901c5133ed47d4769286e860", "mysql": "de89632e277dbc8c97115419", -"postgres": "de89632e277dbc8c97115419", -"postgresql": "de89632e277dbc8c97115419", +"postgres": "5abf4ec799a2ec32fb10da7e", +"postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", "tsql": "de89632e277dbc8c97115419" } @@ -13525,8 +13525,8 @@ "generic": "19a1a80ffc06169f32694de8", "motherduck": "901c5133ed47d4769286e860", "mysql": "19a1a80ffc06169f32694de8", -"postgres": "19a1a80ffc06169f32694de8", -"postgresql": "19a1a80ffc06169f32694de8", +"postgres": "ab1f5f46e69284778bd66d58", +"postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", "tsql": "19a1a80ffc06169f32694de8" } @@ -13570,14 +13570,14 @@ { "DuckDB": "181c436b3522e70edf3adf58", "None": "181c436b3522e70edf3adf58", -"bigquery": "181c436b3522e70edf3adf58", +"bigquery": "837468174ef3e5cb4e4b4070", "databricks": "181c436b3522e70edf3adf58", "duckdb": "181c436b3522e70edf3adf58", "generic": "181c436b3522e70edf3adf58", "motherduck": "901c5133ed47d4769286e860", "mysql": "181c436b3522e70edf3adf58", -"postgres": "181c436b3522e70edf3adf58", -"postgresql": "181c436b3522e70edf3adf58", +"postgres": "7560dc4d0678d613b6142654", +"postgresql": "7560dc4d0678d613b6142654", "snowflake": "181c436b3522e70edf3adf58", "tsql": "181c436b3522e70edf3adf58" } @@ -13585,18 +13585,18 @@ [ "type", { -"DuckDB": "24c3d9dcce7ee2c5b2bac1df", -"None": "547c73a68a3263d4ca786f4d", -"bigquery": "547c73a68a3263d4ca786f4d", -"databricks": "3ad35a92f1fdd0560316b488", -"duckdb": "24c3d9dcce7ee2c5b2bac1df", -"generic": "547c73a68a3263d4ca786f4d", +"DuckDB": "2bd744f24899a7b211c526d9", +"None": "2bd744f24899a7b211c526d9", +"bigquery": "2bd744f24899a7b211c526d9", +"databricks": "2bd744f24899a7b211c526d9", +"duckdb": "2bd744f24899a7b211c526d9", +"generic": "2bd744f24899a7b211c526d9", "motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", -"postgres": "24c3d9dcce7ee2c5b2bac1df", -"postgresql": "24c3d9dcce7ee2c5b2bac1df", -"snowflake": "0591bc1bf9f4f62b80103d53", -"tsql": "d1dc9a140dbe2eb1874c6ad7" +"mysql": "2bd744f24899a7b211c526d9", +"postgres": "2bd744f24899a7b211c526d9", +"postgresql": "2bd744f24899a7b211c526d9", +"snowflake": "2bd744f24899a7b211c526d9", +"tsql": "2bd744f24899a7b211c526d9" } ], [ @@ -13636,14 +13636,14 @@ [ "type", { -"DuckDB": "285056becb5d467811be7cd6", +"DuckDB": "0712c33a016f7dd6a37ee011", "None": "285056becb5d467811be7cd6", "bigquery": "0712c33a016f7dd6a37ee011", "databricks": "285056becb5d467811be7cd6", -"duckdb": "285056becb5d467811be7cd6", +"duckdb": "0712c33a016f7dd6a37ee011", "generic": "285056becb5d467811be7cd6", "motherduck": "901c5133ed47d4769286e860", -"mysql": "fc10fc558d906a80a6404b7f", +"mysql": "285056becb5d467811be7cd6", "postgres": "285056becb5d467811be7cd6", "postgresql": "285056becb5d467811be7cd6", "snowflake": "0712c33a016f7dd6a37ee011", @@ -13655,14 +13655,14 @@ { "DuckDB": "fe23594c781e79cbb0c8927a", "None": "fe23594c781e79cbb0c8927a", -"bigquery": "fe23594c781e79cbb0c8927a", +"bigquery": "8a3f094f18fb4047f0febf59", "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", "motherduck": "901c5133ed47d4769286e860", "mysql": "fe23594c781e79cbb0c8927a", -"postgres": "fe23594c781e79cbb0c8927a", -"postgresql": "fe23594c781e79cbb0c8927a", +"postgres": "8a3f094f18fb4047f0febf59", +"postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "fe23594c781e79cbb0c8927a" } @@ -13672,14 +13672,14 @@ { "DuckDB": "fe23594c781e79cbb0c8927a", "None": "fe23594c781e79cbb0c8927a", -"bigquery": "fe23594c781e79cbb0c8927a", +"bigquery": "8a3f094f18fb4047f0febf59", "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", "motherduck": "901c5133ed47d4769286e860", "mysql": "fe23594c781e79cbb0c8927a", -"postgres": "fe23594c781e79cbb0c8927a", -"postgresql": "fe23594c781e79cbb0c8927a", +"postgres": "8a3f094f18fb4047f0febf59", +"postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "fe23594c781e79cbb0c8927a" } @@ -13688,17 +13688,17 @@ "type", { "DuckDB": "a3b6b25265be330ec52a3322", -"None": "a3b6b25265be330ec52a3322", -"bigquery": "204206d1d1eecf007966cc5b", -"databricks": "e3bc1b8bad22d8104417b00f", +"None": "62368fc15cefd5807807b209", +"bigquery": "c50a316d091c821866e4bfba", +"databricks": "62368fc15cefd5807807b209", "duckdb": "a3b6b25265be330ec52a3322", -"generic": "a3b6b25265be330ec52a3322", +"generic": "62368fc15cefd5807807b209", "motherduck": "901c5133ed47d4769286e860", -"mysql": "919b7ec6a7b49d8527b784cd", -"postgres": "a3b6b25265be330ec52a3322", -"postgresql": "a3b6b25265be330ec52a3322", +"mysql": "62368fc15cefd5807807b209", +"postgres": "62368fc15cefd5807807b209", +"postgresql": "62368fc15cefd5807807b209", "snowflake": "919b7ec6a7b49d8527b784cd", -"tsql": "a3b6b25265be330ec52a3322" +"tsql": "62368fc15cefd5807807b209" } ], [ @@ -13706,14 +13706,14 @@ { "DuckDB": "4d0f0db1a550ed1e0da1a043", "None": "4d0f0db1a550ed1e0da1a043", -"bigquery": "4d0f0db1a550ed1e0da1a043", +"bigquery": "837468174ef3e5cb4e4b4070", "databricks": "4d0f0db1a550ed1e0da1a043", "duckdb": "4d0f0db1a550ed1e0da1a043", "generic": "4d0f0db1a550ed1e0da1a043", "motherduck": "901c5133ed47d4769286e860", "mysql": "4d0f0db1a550ed1e0da1a043", -"postgres": "4d0f0db1a550ed1e0da1a043", -"postgresql": "4d0f0db1a550ed1e0da1a043", +"postgres": "e47320d1030c166a05193bc4", +"postgresql": "e47320d1030c166a05193bc4", "snowflake": "4d0f0db1a550ed1e0da1a043", "tsql": "4d0f0db1a550ed1e0da1a043" } @@ -13722,17 +13722,17 @@ "type", { "DuckDB": "6f4372a332076048e56d5076", -"None": "6f4372a332076048e56d5076", -"bigquery": "74a30f6aa9def0b58afc616c", -"databricks": "6f4372a332076048e56d5076", +"None": "fa78cfa46a78783d069e2fc5", +"bigquery": "b0477b36767a62145aa9158c", +"databricks": "fa78cfa46a78783d069e2fc5", "duckdb": "6f4372a332076048e56d5076", -"generic": "6f4372a332076048e56d5076", +"generic": "fa78cfa46a78783d069e2fc5", "motherduck": "901c5133ed47d4769286e860", -"mysql": "6f4372a332076048e56d5076", +"mysql": "fa78cfa46a78783d069e2fc5", "postgres": "6f4372a332076048e56d5076", "postgresql": "6f4372a332076048e56d5076", "snowflake": "daeeb0f00b3ee4211b8da00f", -"tsql": "6f4372a332076048e56d5076" +"tsql": "fa78cfa46a78783d069e2fc5" } ], [ @@ -13740,7 +13740,7 @@ { "DuckDB": "62368fc15cefd5807807b209", "None": "62368fc15cefd5807807b209", -"bigquery": "e6ffdece2977be55da73b59a", +"bigquery": "c50a316d091c821866e4bfba", "databricks": "62368fc15cefd5807807b209", "duckdb": "62368fc15cefd5807807b209", "generic": "62368fc15cefd5807807b209", @@ -13773,17 +13773,17 @@ "type", { "DuckDB": "087a9417cb511ff7bcfd6ccb", -"None": "383f8922f4b6f167d384037d", -"bigquery": "383f8922f4b6f167d384037d", -"databricks": "383f8922f4b6f167d384037d", +"None": "2e8f6dfec4597fb9373329e7", +"bigquery": "2e8f6dfec4597fb9373329e7", +"databricks": "2e8f6dfec4597fb9373329e7", "duckdb": "087a9417cb511ff7bcfd6ccb", -"generic": "383f8922f4b6f167d384037d", +"generic": "2e8f6dfec4597fb9373329e7", "motherduck": "901c5133ed47d4769286e860", -"mysql": "383f8922f4b6f167d384037d", +"mysql": "2e8f6dfec4597fb9373329e7", "postgres": "383f8922f4b6f167d384037d", "postgresql": "383f8922f4b6f167d384037d", "snowflake": "383f8922f4b6f167d384037d", -"tsql": "383f8922f4b6f167d384037d" +"tsql": "2e8f6dfec4597fb9373329e7" } ], [ @@ -13791,7 +13791,7 @@ { "DuckDB": "e08a957777bd278f921ca0ee", "None": "e08a957777bd278f921ca0ee", -"bigquery": "b1b23029bedaa16e2a6f8516", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "e08a957777bd278f921ca0ee", "duckdb": "e08a957777bd278f921ca0ee", "generic": "e08a957777bd278f921ca0ee", @@ -13842,7 +13842,7 @@ { "DuckDB": "e08a957777bd278f921ca0ee", "None": "e08a957777bd278f921ca0ee", -"bigquery": "b1b23029bedaa16e2a6f8516", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "e08a957777bd278f921ca0ee", "duckdb": "e08a957777bd278f921ca0ee", "generic": "e08a957777bd278f921ca0ee", @@ -13860,11 +13860,11 @@ "DuckDB": "3533f91995bca270f191503d", "None": "3533f91995bca270f191503d", "bigquery": "3533f91995bca270f191503d", -"databricks": "fcfd1f4cfe959c3d88cf43c4", +"databricks": "3533f91995bca270f191503d", "duckdb": "3533f91995bca270f191503d", "generic": "3533f91995bca270f191503d", "motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"mysql": "3533f91995bca270f191503d", "postgres": "3533f91995bca270f191503d", "postgresql": "3533f91995bca270f191503d", "snowflake": "3533f91995bca270f191503d", @@ -13899,8 +13899,8 @@ "generic": "eea9a25a6fbf8e049a445513", "motherduck": "901c5133ed47d4769286e860", "mysql": "eea9a25a6fbf8e049a445513", -"postgres": "eea9a25a6fbf8e049a445513", -"postgresql": "eea9a25a6fbf8e049a445513", +"postgres": "769f762f4c7cffb2990ddacb", +"postgresql": "769f762f4c7cffb2990ddacb", "snowflake": "eea9a25a6fbf8e049a445513", "tsql": "eea9a25a6fbf8e049a445513" } @@ -13932,7 +13932,7 @@ "duckdb": "110327dc45eac0e8d54a96dc", "generic": "110327dc45eac0e8d54a96dc", "motherduck": "901c5133ed47d4769286e860", -"mysql": "1a804f317a3e9a0016925b04", +"mysql": "110327dc45eac0e8d54a96dc", "postgres": "110327dc45eac0e8d54a96dc", "postgresql": "110327dc45eac0e8d54a96dc", "snowflake": "110327dc45eac0e8d54a96dc", @@ -13961,14 +13961,14 @@ { "DuckDB": "e46d4be44ab9e500e9190d26", "None": "e46d4be44ab9e500e9190d26", -"bigquery": "e6ffdece2977be55da73b59a", +"bigquery": "c50a316d091c821866e4bfba", "databricks": "e46d4be44ab9e500e9190d26", "duckdb": "e46d4be44ab9e500e9190d26", "generic": "e46d4be44ab9e500e9190d26", "motherduck": "901c5133ed47d4769286e860", "mysql": "e46d4be44ab9e500e9190d26", -"postgres": "e46d4be44ab9e500e9190d26", -"postgresql": "e46d4be44ab9e500e9190d26", +"postgres": "251aba3fef503ce64556c701", +"postgresql": "251aba3fef503ce64556c701", "snowflake": "e46d4be44ab9e500e9190d26", "tsql": "e46d4be44ab9e500e9190d26" } @@ -14000,7 +14000,7 @@ "duckdb": "e060b5a5a8e9774b580d58d3", "generic": "e060b5a5a8e9774b580d58d3", "motherduck": "901c5133ed47d4769286e860", -"mysql": "319a9fbb02ed503eb114164b", +"mysql": "e060b5a5a8e9774b580d58d3", "postgres": "e060b5a5a8e9774b580d58d3", "postgresql": "e060b5a5a8e9774b580d58d3", "snowflake": "e060b5a5a8e9774b580d58d3", @@ -14010,14 +14010,14 @@ [ "type", { -"DuckDB": "c6654395c62d21b415299f72", +"DuckDB": "0712c33a016f7dd6a37ee011", "None": "c6654395c62d21b415299f72", "bigquery": "0712c33a016f7dd6a37ee011", "databricks": "c6654395c62d21b415299f72", -"duckdb": "c6654395c62d21b415299f72", +"duckdb": "0712c33a016f7dd6a37ee011", "generic": "c6654395c62d21b415299f72", "motherduck": "901c5133ed47d4769286e860", -"mysql": "2978a8fe925c7f93c6e9fcfb", +"mysql": "c6654395c62d21b415299f72", "postgres": "c6654395c62d21b415299f72", "postgresql": "c6654395c62d21b415299f72", "snowflake": "0712c33a016f7dd6a37ee011", @@ -14028,17 +14028,17 @@ "type", { "DuckDB": "b0fba0f56f0cfe71621226b5", -"None": "b0fba0f56f0cfe71621226b5", -"bigquery": "f7fffd42e662ab4655dceb3b", -"databricks": "94b22f27d5086eaa9bb3eff5", +"None": "71ad5cceb4fd8e86446cf461", +"bigquery": "71ad5cceb4fd8e86446cf461", +"databricks": "71ad5cceb4fd8e86446cf461", "duckdb": "b0fba0f56f0cfe71621226b5", -"generic": "b0fba0f56f0cfe71621226b5", +"generic": "71ad5cceb4fd8e86446cf461", "motherduck": "901c5133ed47d4769286e860", -"mysql": "b0fba0f56f0cfe71621226b5", -"postgres": "b0fba0f56f0cfe71621226b5", -"postgresql": "b0fba0f56f0cfe71621226b5", +"mysql": "71ad5cceb4fd8e86446cf461", +"postgres": "71ad5cceb4fd8e86446cf461", +"postgresql": "71ad5cceb4fd8e86446cf461", "snowflake": "ffdce71ffba2fc524a0e9de5", -"tsql": "b0fba0f56f0cfe71621226b5" +"tsql": "71ad5cceb4fd8e86446cf461" } ], [ @@ -14052,8 +14052,8 @@ "generic": "d227b99a82e505d599c2ed8a", "motherduck": "901c5133ed47d4769286e860", "mysql": "d227b99a82e505d599c2ed8a", -"postgres": "d227b99a82e505d599c2ed8a", -"postgresql": "d227b99a82e505d599c2ed8a", +"postgres": "35a9c5c1419bff507cf65ae2", +"postgresql": "35a9c5c1419bff507cf65ae2", "snowflake": "d227b99a82e505d599c2ed8a", "tsql": "d227b99a82e505d599c2ed8a" } @@ -14136,7 +14136,7 @@ "duckdb": "e8cd7a4caa15ec2b6f990a1f", "generic": "1757eadf86c2a8956b520868", "motherduck": "901c5133ed47d4769286e860", -"mysql": "3688fe25b8bdff7eb938e6f9", +"mysql": "6c03a3450ea4742ee7396e3c", "postgres": "e8cd7a4caa15ec2b6f990a1f", "postgresql": "e8cd7a4caa15ec2b6f990a1f", "snowflake": "fe491937bc4ec7b061f2be43", @@ -14154,8 +14154,8 @@ "generic": "c6106f30f23f392e17eadef4", "motherduck": "901c5133ed47d4769286e860", "mysql": "c6106f30f23f392e17eadef4", -"postgres": "c6106f30f23f392e17eadef4", -"postgresql": "c6106f30f23f392e17eadef4", +"postgres": "f43dac38f34a1c0618a3f812", +"postgresql": "f43dac38f34a1c0618a3f812", "snowflake": "c6106f30f23f392e17eadef4", "tsql": "c6106f30f23f392e17eadef4" } @@ -14165,7 +14165,7 @@ { "DuckDB": "eb9c0f675dd300eb82500b21", "None": "eb9c0f675dd300eb82500b21", -"bigquery": "b5c4d5044b2a6e70633f0d0a", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "eb9c0f675dd300eb82500b21", "duckdb": "eb9c0f675dd300eb82500b21", "generic": "eb9c0f675dd300eb82500b21", @@ -14199,7 +14199,7 @@ { "DuckDB": "461923a0d34defd0c726d6e5", "None": "461923a0d34defd0c726d6e5", -"bigquery": "b5c4d5044b2a6e70633f0d0a", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "461923a0d34defd0c726d6e5", "duckdb": "461923a0d34defd0c726d6e5", "generic": "461923a0d34defd0c726d6e5", @@ -14215,17 +14215,17 @@ "type", { "DuckDB": "f2ed52f6bf46b4db9343551c", -"None": "f2ed52f6bf46b4db9343551c", -"bigquery": "f10bdd4d40eb495cbcecd31d", -"databricks": "f2ed52f6bf46b4db9343551c", +"None": "3f998a9ab68d8b2514840489", +"bigquery": "1e75e5227bea0ae294ac7af1", +"databricks": "3f998a9ab68d8b2514840489", "duckdb": "f2ed52f6bf46b4db9343551c", -"generic": "f2ed52f6bf46b4db9343551c", +"generic": "3f998a9ab68d8b2514840489", "motherduck": "901c5133ed47d4769286e860", -"mysql": "f2ed52f6bf46b4db9343551c", -"postgres": "f2ed52f6bf46b4db9343551c", -"postgresql": "f2ed52f6bf46b4db9343551c", +"mysql": "3f998a9ab68d8b2514840489", +"postgres": "3f998a9ab68d8b2514840489", +"postgresql": "3f998a9ab68d8b2514840489", "snowflake": "f2ed52f6bf46b4db9343551c", -"tsql": "f2ed52f6bf46b4db9343551c" +"tsql": "3f998a9ab68d8b2514840489" } ], [ @@ -14239,8 +14239,8 @@ "generic": "0960c90223e1eca15361360f", "motherduck": "901c5133ed47d4769286e860", "mysql": "0960c90223e1eca15361360f", -"postgres": "0960c90223e1eca15361360f", -"postgresql": "0960c90223e1eca15361360f", +"postgres": "b72367ce0764c938c620a44c", +"postgresql": "b72367ce0764c938c620a44c", "snowflake": "0960c90223e1eca15361360f", "tsql": "0960c90223e1eca15361360f" } @@ -14256,8 +14256,8 @@ "generic": "e2879cf4770e7f48d2dd704a", "motherduck": "901c5133ed47d4769286e860", "mysql": "e2879cf4770e7f48d2dd704a", -"postgres": "e2879cf4770e7f48d2dd704a", -"postgresql": "e2879cf4770e7f48d2dd704a", +"postgres": "2d4aea8dec5521e258596f3d", +"postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", "tsql": "e2879cf4770e7f48d2dd704a" } @@ -14335,7 +14335,7 @@ { "DuckDB": "f01435c3d6e460ffd86479dc", "None": "f01435c3d6e460ffd86479dc", -"bigquery": "99d6d9358db2ff349db17bb6", +"bigquery": "01d1b160c1768fa1cd4b2e45", "databricks": "72d1587b51c517b53f52a2d8", "duckdb": "f01435c3d6e460ffd86479dc", "generic": "f01435c3d6e460ffd86479dc", @@ -14358,8 +14358,8 @@ "generic": "5d25d5e13f81700741695ed4", "motherduck": "901c5133ed47d4769286e860", "mysql": "5d25d5e13f81700741695ed4", -"postgres": "5d25d5e13f81700741695ed4", -"postgresql": "5d25d5e13f81700741695ed4", +"postgres": "62131fb5b065f5de2e74a0f1", +"postgresql": "62131fb5b065f5de2e74a0f1", "snowflake": "5d25d5e13f81700741695ed4", "tsql": "5d25d5e13f81700741695ed4" } @@ -14368,17 +14368,17 @@ "type", { "DuckDB": "3e2cff9b165a6eaf1c043756", -"None": "77fe8e560c0d80d58e64a3ac", -"bigquery": "77fe8e560c0d80d58e64a3ac", -"databricks": "a8b5c14299c94dad25520df6", +"None": "ea6e350a4a788066ff386c50", +"bigquery": "80ea7a56a9546d00afc1689a", +"databricks": "ea6e350a4a788066ff386c50", "duckdb": "3e2cff9b165a6eaf1c043756", -"generic": "77fe8e560c0d80d58e64a3ac", +"generic": "ea6e350a4a788066ff386c50", "motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", -"postgres": "545b06c80ff7b812dafe5136", -"postgresql": "545b06c80ff7b812dafe5136", +"mysql": "ea6e350a4a788066ff386c50", +"postgres": "ea6e350a4a788066ff386c50", +"postgresql": "ea6e350a4a788066ff386c50", "snowflake": "2a7a3265ae4026b435644d0e", -"tsql": "d1dc9a140dbe2eb1874c6ad7" +"tsql": "ea6e350a4a788066ff386c50" } ], [ @@ -14443,8 +14443,8 @@ "generic": "669084d1ea704ae5b8ee05c2", "motherduck": "901c5133ed47d4769286e860", "mysql": "669084d1ea704ae5b8ee05c2", -"postgres": "669084d1ea704ae5b8ee05c2", -"postgresql": "669084d1ea704ae5b8ee05c2", +"postgres": "93c1b5b5960e26479baf88d0", +"postgresql": "93c1b5b5960e26479baf88d0", "snowflake": "669084d1ea704ae5b8ee05c2", "tsql": "669084d1ea704ae5b8ee05c2" } @@ -14454,14 +14454,14 @@ { "DuckDB": "38b2ec94bf02be40007ce525", "None": "38b2ec94bf02be40007ce525", -"bigquery": "38b2ec94bf02be40007ce525", +"bigquery": "850deea990b52b82b36a9848", "databricks": "38b2ec94bf02be40007ce525", "duckdb": "38b2ec94bf02be40007ce525", "generic": "38b2ec94bf02be40007ce525", "motherduck": "901c5133ed47d4769286e860", "mysql": "38b2ec94bf02be40007ce525", -"postgres": "38b2ec94bf02be40007ce525", -"postgresql": "38b2ec94bf02be40007ce525", +"postgres": "850deea990b52b82b36a9848", +"postgresql": "850deea990b52b82b36a9848", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "38b2ec94bf02be40007ce525" } @@ -14494,8 +14494,8 @@ "generic": "66078c43faec49916ed11546", "motherduck": "901c5133ed47d4769286e860", "mysql": "66078c43faec49916ed11546", -"postgres": "66078c43faec49916ed11546", -"postgresql": "66078c43faec49916ed11546", +"postgres": "48a21c33337c0a03f7f0c746", +"postgresql": "48a21c33337c0a03f7f0c746", "snowflake": "66078c43faec49916ed11546", "tsql": "66078c43faec49916ed11546" } @@ -14522,7 +14522,7 @@ { "DuckDB": "aae2b1fe6f2d9807ecde412b", "None": "aae2b1fe6f2d9807ecde412b", -"bigquery": "e1c36a5cdd2427ff2b089ac9", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "aae2b1fe6f2d9807ecde412b", "duckdb": "aae2b1fe6f2d9807ecde412b", "generic": "aae2b1fe6f2d9807ecde412b", @@ -14647,8 +14647,8 @@ "generic": "054898738769701df3abbf2e", "motherduck": "901c5133ed47d4769286e860", "mysql": "054898738769701df3abbf2e", -"postgres": "054898738769701df3abbf2e", -"postgresql": "054898738769701df3abbf2e", +"postgres": "16c4b7f3bdc7c21a00a78838", +"postgresql": "16c4b7f3bdc7c21a00a78838", "snowflake": "054898738769701df3abbf2e", "tsql": "054898738769701df3abbf2e" } @@ -14664,8 +14664,8 @@ "generic": "b0b09c4a6f7570ece56f59ce", "motherduck": "901c5133ed47d4769286e860", "mysql": "355e032cc5c0f24695836b3d", -"postgres": "b0b09c4a6f7570ece56f59ce", -"postgresql": "b0b09c4a6f7570ece56f59ce", +"postgres": "111af35848fe500ec2ae54b2", +"postgresql": "111af35848fe500ec2ae54b2", "snowflake": "355e032cc5c0f24695836b3d", "tsql": "b0b09c4a6f7570ece56f59ce" } @@ -14715,8 +14715,8 @@ "generic": "19a1a80ffc06169f32694de8", "motherduck": "901c5133ed47d4769286e860", "mysql": "19a1a80ffc06169f32694de8", -"postgres": "19a1a80ffc06169f32694de8", -"postgresql": "19a1a80ffc06169f32694de8", +"postgres": "ab1f5f46e69284778bd66d58", +"postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", "tsql": "19a1a80ffc06169f32694de8" } @@ -14817,8 +14817,8 @@ "generic": "3c0719ca5c83e0a20d0b0b42", "motherduck": "901c5133ed47d4769286e860", "mysql": "3c0719ca5c83e0a20d0b0b42", -"postgres": "3c0719ca5c83e0a20d0b0b42", -"postgresql": "3c0719ca5c83e0a20d0b0b42", +"postgres": "42d4a23d9a52e79d0c3458d3", +"postgresql": "42d4a23d9a52e79d0c3458d3", "snowflake": "a3acd7ba35e81a800f967bd3", "tsql": "3c0719ca5c83e0a20d0b0b42" } @@ -14828,7 +14828,7 @@ { "DuckDB": "aae2b1fe6f2d9807ecde412b", "None": "aae2b1fe6f2d9807ecde412b", -"bigquery": "e1c36a5cdd2427ff2b089ac9", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "aae2b1fe6f2d9807ecde412b", "duckdb": "aae2b1fe6f2d9807ecde412b", "generic": "aae2b1fe6f2d9807ecde412b", @@ -14861,34 +14861,34 @@ "type", { "DuckDB": "919b7ec6a7b49d8527b784cd", -"None": "919b7ec6a7b49d8527b784cd", -"bigquery": "204206d1d1eecf007966cc5b", -"databricks": "e3bc1b8bad22d8104417b00f", +"None": "558da38ec4d9d5d6c72f60cf", +"bigquery": "c50a316d091c821866e4bfba", +"databricks": "558da38ec4d9d5d6c72f60cf", "duckdb": "919b7ec6a7b49d8527b784cd", -"generic": "919b7ec6a7b49d8527b784cd", +"generic": "558da38ec4d9d5d6c72f60cf", "motherduck": "901c5133ed47d4769286e860", -"mysql": "919b7ec6a7b49d8527b784cd", -"postgres": "a3b6b25265be330ec52a3322", -"postgresql": "a3b6b25265be330ec52a3322", +"mysql": "558da38ec4d9d5d6c72f60cf", +"postgres": "558da38ec4d9d5d6c72f60cf", +"postgresql": "558da38ec4d9d5d6c72f60cf", "snowflake": "919b7ec6a7b49d8527b784cd", -"tsql": "919b7ec6a7b49d8527b784cd" +"tsql": "558da38ec4d9d5d6c72f60cf" } ], [ "type", { "DuckDB": "a428302e4848ffe877284c84", -"None": "a428302e4848ffe877284c84", -"bigquery": "a428302e4848ffe877284c84", -"databricks": "a428302e4848ffe877284c84", +"None": "c9b810639795a7bfab91b98d", +"bigquery": "c9b810639795a7bfab91b98d", +"databricks": "c9b810639795a7bfab91b98d", "duckdb": "a428302e4848ffe877284c84", -"generic": "a428302e4848ffe877284c84", +"generic": "c9b810639795a7bfab91b98d", "motherduck": "901c5133ed47d4769286e860", -"mysql": "a428302e4848ffe877284c84", +"mysql": "c9b810639795a7bfab91b98d", "postgres": "a428302e4848ffe877284c84", "postgresql": "a428302e4848ffe877284c84", "snowflake": "652ff8b21f638eac8fdc46c5", -"tsql": "a428302e4848ffe877284c84" +"tsql": "c9b810639795a7bfab91b98d" } ], [ @@ -14913,14 +14913,14 @@ { "DuckDB": "4b356542044f1267fcfc487a", "None": "4b356542044f1267fcfc487a", -"bigquery": "4b356542044f1267fcfc487a", +"bigquery": "a5656d70e208254d1d3e9b4f", "databricks": "4b356542044f1267fcfc487a", "duckdb": "4b356542044f1267fcfc487a", "generic": "4b356542044f1267fcfc487a", "motherduck": "901c5133ed47d4769286e860", "mysql": "4b356542044f1267fcfc487a", -"postgres": "4b356542044f1267fcfc487a", -"postgresql": "4b356542044f1267fcfc487a", +"postgres": "a5656d70e208254d1d3e9b4f", +"postgresql": "a5656d70e208254d1d3e9b4f", "snowflake": "4b356542044f1267fcfc487a", "tsql": "4b356542044f1267fcfc487a" } @@ -14998,14 +14998,14 @@ { "DuckDB": "5d25d5e13f81700741695ed4", "None": "5d25d5e13f81700741695ed4", -"bigquery": "23ee196b7bd6039855a1460f", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "5d25d5e13f81700741695ed4", "duckdb": "5d25d5e13f81700741695ed4", "generic": "5d25d5e13f81700741695ed4", "motherduck": "901c5133ed47d4769286e860", "mysql": "5d25d5e13f81700741695ed4", -"postgres": "5d25d5e13f81700741695ed4", -"postgresql": "5d25d5e13f81700741695ed4", +"postgres": "62131fb5b065f5de2e74a0f1", +"postgresql": "62131fb5b065f5de2e74a0f1", "snowflake": "5d25d5e13f81700741695ed4", "tsql": "5d25d5e13f81700741695ed4" } @@ -15032,14 +15032,14 @@ { "DuckDB": "b561884132dbe7e42b719110", "None": "b561884132dbe7e42b719110", -"bigquery": "b1b23029bedaa16e2a6f8516", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "b561884132dbe7e42b719110", "duckdb": "b561884132dbe7e42b719110", "generic": "b561884132dbe7e42b719110", "motherduck": "901c5133ed47d4769286e860", "mysql": "b561884132dbe7e42b719110", -"postgres": "b561884132dbe7e42b719110", -"postgresql": "b561884132dbe7e42b719110", +"postgres": "5aa51d84ac5e6c7a9306f348", +"postgresql": "5aa51d84ac5e6c7a9306f348", "snowflake": "b561884132dbe7e42b719110", "tsql": "b561884132dbe7e42b719110" } @@ -15047,14 +15047,14 @@ [ "type", { -"DuckDB": "c6654395c62d21b415299f72", +"DuckDB": "0712c33a016f7dd6a37ee011", "None": "c6654395c62d21b415299f72", "bigquery": "0712c33a016f7dd6a37ee011", "databricks": "c6654395c62d21b415299f72", -"duckdb": "c6654395c62d21b415299f72", +"duckdb": "0712c33a016f7dd6a37ee011", "generic": "c6654395c62d21b415299f72", "motherduck": "901c5133ed47d4769286e860", -"mysql": "2978a8fe925c7f93c6e9fcfb", +"mysql": "c6654395c62d21b415299f72", "postgres": "c6654395c62d21b415299f72", "postgresql": "c6654395c62d21b415299f72", "snowflake": "0712c33a016f7dd6a37ee011", @@ -15066,7 +15066,7 @@ { "DuckDB": "2ffee1a4ca7dc4a711fc0d27", "None": "2ffee1a4ca7dc4a711fc0d27", -"bigquery": "2ffee1a4ca7dc4a711fc0d27", +"bigquery": "c74425c5ce650ac69c83ca5b", "databricks": "2ffee1a4ca7dc4a711fc0d27", "duckdb": "2ffee1a4ca7dc4a711fc0d27", "generic": "2ffee1a4ca7dc4a711fc0d27", @@ -15106,8 +15106,8 @@ "generic": "de89632e277dbc8c97115419", "motherduck": "901c5133ed47d4769286e860", "mysql": "de89632e277dbc8c97115419", -"postgres": "de89632e277dbc8c97115419", -"postgresql": "de89632e277dbc8c97115419", +"postgres": "5abf4ec799a2ec32fb10da7e", +"postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", "tsql": "de89632e277dbc8c97115419" } @@ -15135,11 +15135,11 @@ "DuckDB": "9c2cff8be7225cf79883ca65", "None": "9c2cff8be7225cf79883ca65", "bigquery": "9c2cff8be7225cf79883ca65", -"databricks": "901ac89f8d9ec18bd3a61e4b", +"databricks": "9c2cff8be7225cf79883ca65", "duckdb": "9c2cff8be7225cf79883ca65", "generic": "9c2cff8be7225cf79883ca65", "motherduck": "901c5133ed47d4769286e860", -"mysql": "901ac89f8d9ec18bd3a61e4b", +"mysql": "9c2cff8be7225cf79883ca65", "postgres": "9c2cff8be7225cf79883ca65", "postgresql": "9c2cff8be7225cf79883ca65", "snowflake": "9c2cff8be7225cf79883ca65", @@ -15157,8 +15157,8 @@ "generic": "ab62f43260f32e2cf62666fc", "motherduck": "901c5133ed47d4769286e860", "mysql": "ab62f43260f32e2cf62666fc", -"postgres": "ab62f43260f32e2cf62666fc", -"postgresql": "ab62f43260f32e2cf62666fc", +"postgres": "9ea4932505663b43f527adad", +"postgresql": "9ea4932505663b43f527adad", "snowflake": "ab62f43260f32e2cf62666fc", "tsql": "ab62f43260f32e2cf62666fc" } @@ -15185,7 +15185,7 @@ { "DuckDB": "97e5335218efead1a4226b8d", "None": "97e5335218efead1a4226b8d", -"bigquery": "97e5335218efead1a4226b8d", +"bigquery": "c74425c5ce650ac69c83ca5b", "databricks": "97e5335218efead1a4226b8d", "duckdb": "97e5335218efead1a4226b8d", "generic": "97e5335218efead1a4226b8d", @@ -15202,14 +15202,14 @@ { "DuckDB": "ee47e1424cb01deb0cf51760", "None": "ee47e1424cb01deb0cf51760", -"bigquery": "ee47e1424cb01deb0cf51760", +"bigquery": "893ff554934dfc6f8253597b", "databricks": "ee47e1424cb01deb0cf51760", "duckdb": "ee47e1424cb01deb0cf51760", "generic": "ee47e1424cb01deb0cf51760", "motherduck": "901c5133ed47d4769286e860", "mysql": "ee47e1424cb01deb0cf51760", -"postgres": "ee47e1424cb01deb0cf51760", -"postgresql": "ee47e1424cb01deb0cf51760", +"postgres": "3c1521654088dd54d0dc9ead", +"postgresql": "3c1521654088dd54d0dc9ead", "snowflake": "ee47e1424cb01deb0cf51760", "tsql": "ee47e1424cb01deb0cf51760" } @@ -15219,7 +15219,7 @@ { "DuckDB": "2d4aea8dec5521e258596f3d", "None": "2d4aea8dec5521e258596f3d", -"bigquery": "2d4aea8dec5521e258596f3d", +"bigquery": "a428302e4848ffe877284c84", "databricks": "2d4aea8dec5521e258596f3d", "duckdb": "2d4aea8dec5521e258596f3d", "generic": "2d4aea8dec5521e258596f3d", @@ -15269,17 +15269,17 @@ "type", { "DuckDB": "19bdd92ea0cc5fb028657510", -"None": "78b2cf275c640af54feeca31", -"bigquery": "1e75e5227bea0ae294ac7af1", -"databricks": "1e75e5227bea0ae294ac7af1", +"None": "8075663faec01d3abb4fc153", +"bigquery": "8075663faec01d3abb4fc153", +"databricks": "8075663faec01d3abb4fc153", "duckdb": "19bdd92ea0cc5fb028657510", -"generic": "78b2cf275c640af54feeca31", +"generic": "8075663faec01d3abb4fc153", "motherduck": "901c5133ed47d4769286e860", -"mysql": "19bdd92ea0cc5fb028657510", -"postgres": "78b2cf275c640af54feeca31", -"postgresql": "78b2cf275c640af54feeca31", +"mysql": "8075663faec01d3abb4fc153", +"postgres": "8075663faec01d3abb4fc153", +"postgresql": "8075663faec01d3abb4fc153", "snowflake": "8ff614b7306c0edbe36ce00b", -"tsql": "78b2cf275c640af54feeca31" +"tsql": "8075663faec01d3abb4fc153" } ], [ @@ -15287,14 +15287,14 @@ { "DuckDB": "ee47e1424cb01deb0cf51760", "None": "ee47e1424cb01deb0cf51760", -"bigquery": "ee47e1424cb01deb0cf51760", +"bigquery": "893ff554934dfc6f8253597b", "databricks": "ee47e1424cb01deb0cf51760", "duckdb": "ee47e1424cb01deb0cf51760", "generic": "ee47e1424cb01deb0cf51760", "motherduck": "901c5133ed47d4769286e860", "mysql": "ee47e1424cb01deb0cf51760", -"postgres": "ee47e1424cb01deb0cf51760", -"postgresql": "ee47e1424cb01deb0cf51760", +"postgres": "3c1521654088dd54d0dc9ead", +"postgresql": "3c1521654088dd54d0dc9ead", "snowflake": "ee47e1424cb01deb0cf51760", "tsql": "ee47e1424cb01deb0cf51760" } @@ -15321,14 +15321,14 @@ { "DuckDB": "fe23594c781e79cbb0c8927a", "None": "fe23594c781e79cbb0c8927a", -"bigquery": "fe23594c781e79cbb0c8927a", +"bigquery": "8a3f094f18fb4047f0febf59", "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", "motherduck": "901c5133ed47d4769286e860", "mysql": "fe23594c781e79cbb0c8927a", -"postgres": "fe23594c781e79cbb0c8927a", -"postgresql": "fe23594c781e79cbb0c8927a", +"postgres": "8a3f094f18fb4047f0febf59", +"postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "fe23594c781e79cbb0c8927a" } @@ -15344,8 +15344,8 @@ "generic": "61d08c878cc506dd91ca4586", "motherduck": "901c5133ed47d4769286e860", "mysql": "c100cdbd268164cf084295be", -"postgres": "61d08c878cc506dd91ca4586", -"postgresql": "61d08c878cc506dd91ca4586", +"postgres": "111af35848fe500ec2ae54b2", +"postgresql": "111af35848fe500ec2ae54b2", "snowflake": "c100cdbd268164cf084295be", "tsql": "61d08c878cc506dd91ca4586" } @@ -15457,7 +15457,7 @@ { "DuckDB": "5c28019dce48a74708c90a76", "None": "5c28019dce48a74708c90a76", -"bigquery": "f28a96f0eb38ad8874f6ec5d", +"bigquery": "5c28019dce48a74708c90a76", "databricks": "5c28019dce48a74708c90a76", "duckdb": "5c28019dce48a74708c90a76", "generic": "5c28019dce48a74708c90a76", @@ -15497,8 +15497,8 @@ "generic": "669084d1ea704ae5b8ee05c2", "motherduck": "901c5133ed47d4769286e860", "mysql": "669084d1ea704ae5b8ee05c2", -"postgres": "669084d1ea704ae5b8ee05c2", -"postgresql": "669084d1ea704ae5b8ee05c2", +"postgres": "93c1b5b5960e26479baf88d0", +"postgresql": "93c1b5b5960e26479baf88d0", "snowflake": "669084d1ea704ae5b8ee05c2", "tsql": "669084d1ea704ae5b8ee05c2" } @@ -15531,8 +15531,8 @@ "generic": "19a1a80ffc06169f32694de8", "motherduck": "901c5133ed47d4769286e860", "mysql": "19a1a80ffc06169f32694de8", -"postgres": "19a1a80ffc06169f32694de8", -"postgresql": "19a1a80ffc06169f32694de8", +"postgres": "ab1f5f46e69284778bd66d58", +"postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", "tsql": "19a1a80ffc06169f32694de8" } @@ -15558,15 +15558,15 @@ "type", { "DuckDB": "c0eec31620dfc2ac0028173d", -"None": "7f902bdd94be27212c27de4d", -"bigquery": "02e0b2ed25f32a8d09350a23", -"databricks": "7f902bdd94be27212c27de4d", +"None": "bf34c743a59d7c3f212cf17b", +"bigquery": "bf34c743a59d7c3f212cf17b", +"databricks": "bf34c743a59d7c3f212cf17b", "duckdb": "c0eec31620dfc2ac0028173d", -"generic": "7f902bdd94be27212c27de4d", +"generic": "bf34c743a59d7c3f212cf17b", "motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", -"postgres": "6d562c54c27c6810625f7693", -"postgresql": "6d562c54c27c6810625f7693", +"mysql": "bf34c743a59d7c3f212cf17b", +"postgres": "759df854baf67e256ec19adb", +"postgresql": "759df854baf67e256ec19adb", "snowflake": "354dee69d4f0c63cf79046ac", "tsql": "bf34c743a59d7c3f212cf17b" } @@ -15615,7 +15615,7 @@ "duckdb": "a766fea06fc070b352976b0a", "generic": "a766fea06fc070b352976b0a", "motherduck": "901c5133ed47d4769286e860", -"mysql": "e4f0244beaa01667e80482b6", +"mysql": "a766fea06fc070b352976b0a", "postgres": "a766fea06fc070b352976b0a", "postgresql": "a766fea06fc070b352976b0a", "snowflake": "a766fea06fc070b352976b0a", @@ -15650,8 +15650,8 @@ "generic": "3efe8bb57c98fcbe74096b3c", "motherduck": "901c5133ed47d4769286e860", "mysql": "1d8b945fd793ecdcacb90c21", -"postgres": "3efe8bb57c98fcbe74096b3c", -"postgresql": "3efe8bb57c98fcbe74096b3c", +"postgres": "111af35848fe500ec2ae54b2", +"postgresql": "111af35848fe500ec2ae54b2", "snowflake": "1d8b945fd793ecdcacb90c21", "tsql": "3efe8bb57c98fcbe74096b3c" } @@ -15660,17 +15660,17 @@ "type", { "DuckDB": "c50a316d091c821866e4bfba", -"None": "c50a316d091c821866e4bfba", -"bigquery": "c50a316d091c821866e4bfba", -"databricks": "c50a316d091c821866e4bfba", +"None": "e6ffdece2977be55da73b59a", +"bigquery": "e6ffdece2977be55da73b59a", +"databricks": "e6ffdece2977be55da73b59a", "duckdb": "c50a316d091c821866e4bfba", -"generic": "c50a316d091c821866e4bfba", +"generic": "e6ffdece2977be55da73b59a", "motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"mysql": "e6ffdece2977be55da73b59a", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", -"tsql": "c50a316d091c821866e4bfba" +"tsql": "e6ffdece2977be55da73b59a" } ], [ @@ -15684,8 +15684,8 @@ "generic": "b8321619b794db091c9e96a1", "motherduck": "901c5133ed47d4769286e860", "mysql": "b8321619b794db091c9e96a1", -"postgres": "b8321619b794db091c9e96a1", -"postgresql": "b8321619b794db091c9e96a1", +"postgres": "bce26544873d6ec9191c9a86", +"postgresql": "bce26544873d6ec9191c9a86", "snowflake": "b8321619b794db091c9e96a1", "tsql": "b8321619b794db091c9e96a1" } @@ -15694,17 +15694,17 @@ "type", { "DuckDB": "c50a316d091c821866e4bfba", -"None": "c50a316d091c821866e4bfba", -"bigquery": "c50a316d091c821866e4bfba", -"databricks": "c50a316d091c821866e4bfba", +"None": "e6ffdece2977be55da73b59a", +"bigquery": "e6ffdece2977be55da73b59a", +"databricks": "e6ffdece2977be55da73b59a", "duckdb": "c50a316d091c821866e4bfba", -"generic": "c50a316d091c821866e4bfba", +"generic": "e6ffdece2977be55da73b59a", "motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"mysql": "e6ffdece2977be55da73b59a", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", -"tsql": "c50a316d091c821866e4bfba" +"tsql": "e6ffdece2977be55da73b59a" } ], [ @@ -15746,7 +15746,7 @@ { "DuckDB": "07c9e779f7ae9c58f4f0c902", "None": "07c9e779f7ae9c58f4f0c902", -"bigquery": "07c9e779f7ae9c58f4f0c902", +"bigquery": "c74425c5ce650ac69c83ca5b", "databricks": "07c9e779f7ae9c58f4f0c902", "duckdb": "07c9e779f7ae9c58f4f0c902", "generic": "07c9e779f7ae9c58f4f0c902", @@ -15780,7 +15780,7 @@ { "DuckDB": "5c28019dce48a74708c90a76", "None": "5c28019dce48a74708c90a76", -"bigquery": "f28a96f0eb38ad8874f6ec5d", +"bigquery": "5c28019dce48a74708c90a76", "databricks": "5c28019dce48a74708c90a76", "duckdb": "5c28019dce48a74708c90a76", "generic": "5c28019dce48a74708c90a76", @@ -15814,7 +15814,7 @@ { "DuckDB": "e248573edaaaa875cb926c45", "None": "e248573edaaaa875cb926c45", -"bigquery": "2a0918be53051fe629d9ff8f", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "e248573edaaaa875cb926c45", "duckdb": "e248573edaaaa875cb926c45", "generic": "e248573edaaaa875cb926c45", @@ -15848,7 +15848,7 @@ { "DuckDB": "d44c023c720f385769f2ab81", "None": "d44c023c720f385769f2ab81", -"bigquery": "d44c023c720f385769f2ab81", +"bigquery": "c114794e288b3464c914fd5c", "databricks": "d44c023c720f385769f2ab81", "duckdb": "d44c023c720f385769f2ab81", "generic": "d44c023c720f385769f2ab81", @@ -15934,11 +15934,11 @@ "DuckDB": "d34f84c1b71b9c9c0c20abb0", "None": "d34f84c1b71b9c9c0c20abb0", "bigquery": "d34f84c1b71b9c9c0c20abb0", -"databricks": "c662d9470aa4255dfb493df1", +"databricks": "d34f84c1b71b9c9c0c20abb0", "duckdb": "d34f84c1b71b9c9c0c20abb0", "generic": "d34f84c1b71b9c9c0c20abb0", "motherduck": "901c5133ed47d4769286e860", -"mysql": "c662d9470aa4255dfb493df1", +"mysql": "d34f84c1b71b9c9c0c20abb0", "postgres": "d34f84c1b71b9c9c0c20abb0", "postgresql": "d34f84c1b71b9c9c0c20abb0", "snowflake": "d34f84c1b71b9c9c0c20abb0", @@ -15949,17 +15949,17 @@ "type", { "DuckDB": "c50a316d091c821866e4bfba", -"None": "c50a316d091c821866e4bfba", -"bigquery": "c50a316d091c821866e4bfba", -"databricks": "c50a316d091c821866e4bfba", +"None": "e6ffdece2977be55da73b59a", +"bigquery": "e6ffdece2977be55da73b59a", +"databricks": "e6ffdece2977be55da73b59a", "duckdb": "c50a316d091c821866e4bfba", -"generic": "c50a316d091c821866e4bfba", +"generic": "e6ffdece2977be55da73b59a", "motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"mysql": "e6ffdece2977be55da73b59a", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", -"tsql": "c50a316d091c821866e4bfba" +"tsql": "e6ffdece2977be55da73b59a" } ], [ @@ -16035,7 +16035,7 @@ { "DuckDB": "2d4aea8dec5521e258596f3d", "None": "2d4aea8dec5521e258596f3d", -"bigquery": "2d4aea8dec5521e258596f3d", +"bigquery": "a428302e4848ffe877284c84", "databricks": "2d4aea8dec5521e258596f3d", "duckdb": "2d4aea8dec5521e258596f3d", "generic": "2d4aea8dec5521e258596f3d", @@ -16050,16 +16050,16 @@ [ "type", { -"DuckDB": "b4d65f2c9d0405e88d28938b", +"DuckDB": "0712c33a016f7dd6a37ee011", "None": "ba98b19402ac6379a4172de9", "bigquery": "ba98b19402ac6379a4172de9", "databricks": "ba98b19402ac6379a4172de9", -"duckdb": "b4d65f2c9d0405e88d28938b", +"duckdb": "0712c33a016f7dd6a37ee011", "generic": "ba98b19402ac6379a4172de9", "motherduck": "901c5133ed47d4769286e860", "mysql": "ba98b19402ac6379a4172de9", -"postgres": "ba98b19402ac6379a4172de9", -"postgresql": "ba98b19402ac6379a4172de9", +"postgres": "3aa01d5ccf0d0b7e5c540297", +"postgresql": "3aa01d5ccf0d0b7e5c540297", "snowflake": "ba98b19402ac6379a4172de9", "tsql": "ba98b19402ac6379a4172de9" } @@ -16069,14 +16069,14 @@ { "DuckDB": "e46d4be44ab9e500e9190d26", "None": "e46d4be44ab9e500e9190d26", -"bigquery": "e6ffdece2977be55da73b59a", +"bigquery": "c50a316d091c821866e4bfba", "databricks": "e46d4be44ab9e500e9190d26", "duckdb": "e46d4be44ab9e500e9190d26", "generic": "e46d4be44ab9e500e9190d26", "motherduck": "901c5133ed47d4769286e860", "mysql": "e46d4be44ab9e500e9190d26", -"postgres": "e46d4be44ab9e500e9190d26", -"postgresql": "e46d4be44ab9e500e9190d26", +"postgres": "251aba3fef503ce64556c701", +"postgresql": "251aba3fef503ce64556c701", "snowflake": "e46d4be44ab9e500e9190d26", "tsql": "e46d4be44ab9e500e9190d26" } @@ -16085,17 +16085,17 @@ "type", { "DuckDB": "6c58c6a62958e64e6a26bf9c", -"None": "6c58c6a62958e64e6a26bf9c", -"bigquery": "204206d1d1eecf007966cc5b", -"databricks": "6c58c6a62958e64e6a26bf9c", +"None": "9572f907cb1c76265b3b608f", +"bigquery": "9572f907cb1c76265b3b608f", +"databricks": "9572f907cb1c76265b3b608f", "duckdb": "6c58c6a62958e64e6a26bf9c", -"generic": "6c58c6a62958e64e6a26bf9c", +"generic": "9572f907cb1c76265b3b608f", "motherduck": "901c5133ed47d4769286e860", -"mysql": "6c58c6a62958e64e6a26bf9c", -"postgres": "571886088d0423ed1a0f1696", -"postgresql": "571886088d0423ed1a0f1696", +"mysql": "9572f907cb1c76265b3b608f", +"postgres": "9572f907cb1c76265b3b608f", +"postgresql": "9572f907cb1c76265b3b608f", "snowflake": "6c58c6a62958e64e6a26bf9c", -"tsql": "919b7ec6a7b49d8527b784cd" +"tsql": "9572f907cb1c76265b3b608f" } ], [ @@ -16154,7 +16154,7 @@ { "DuckDB": "5d25d5e13f81700741695ed4", "None": "5d25d5e13f81700741695ed4", -"bigquery": "23ee196b7bd6039855a1460f", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "5d25d5e13f81700741695ed4", "duckdb": "5d25d5e13f81700741695ed4", "generic": "5d25d5e13f81700741695ed4", @@ -16255,17 +16255,17 @@ "type", { "DuckDB": "9f4b51f000ffbe30f6b7d826", -"None": "bacfe825b19ef52bc792ab22", +"None": "00714a615c9e7a090fb122c5", "bigquery": "00714a615c9e7a090fb122c5", "databricks": "00714a615c9e7a090fb122c5", "duckdb": "9f4b51f000ffbe30f6b7d826", -"generic": "bacfe825b19ef52bc792ab22", +"generic": "00714a615c9e7a090fb122c5", "motherduck": "901c5133ed47d4769286e860", "mysql": "00714a615c9e7a090fb122c5", -"postgres": "bacfe825b19ef52bc792ab22", -"postgresql": "bacfe825b19ef52bc792ab22", +"postgres": "30d01707c0b130f64a4d8414", +"postgresql": "30d01707c0b130f64a4d8414", "snowflake": "bacfe825b19ef52bc792ab22", -"tsql": "bacfe825b19ef52bc792ab22" +"tsql": "00714a615c9e7a090fb122c5" } ], [ @@ -16322,16 +16322,16 @@ [ "type", { -"DuckDB": "8698d724534891869075175e", -"None": "0017a88bb60ec3403c786ed8", +"DuckDB": "29525edc1ad146bad4372f59", +"None": "2795066c9a71fa3e3780c73b", "bigquery": "2795066c9a71fa3e3780c73b", -"databricks": "0017a88bb60ec3403c786ed8", -"duckdb": "8698d724534891869075175e", -"generic": "0017a88bb60ec3403c786ed8", +"databricks": "2795066c9a71fa3e3780c73b", +"duckdb": "29525edc1ad146bad4372f59", +"generic": "2795066c9a71fa3e3780c73b", "motherduck": "901c5133ed47d4769286e860", "mysql": "6c03a3450ea4742ee7396e3c", -"postgres": "8698d724534891869075175e", -"postgresql": "8698d724534891869075175e", +"postgres": "29525edc1ad146bad4372f59", +"postgresql": "29525edc1ad146bad4372f59", "snowflake": "b3b752c645651b849e23d39a", "tsql": "d1dc9a140dbe2eb1874c6ad7" } @@ -16347,8 +16347,8 @@ "generic": "b561884132dbe7e42b719110", "motherduck": "901c5133ed47d4769286e860", "mysql": "b561884132dbe7e42b719110", -"postgres": "b561884132dbe7e42b719110", -"postgresql": "b561884132dbe7e42b719110", +"postgres": "5aa51d84ac5e6c7a9306f348", +"postgresql": "5aa51d84ac5e6c7a9306f348", "snowflake": "b561884132dbe7e42b719110", "tsql": "b561884132dbe7e42b719110" } @@ -16409,14 +16409,14 @@ { "DuckDB": "242bf50062e95bc8534ca887", "None": "242bf50062e95bc8534ca887", -"bigquery": "242bf50062e95bc8534ca887", +"bigquery": "67d251c042daee0bb2bf7351", "databricks": "242bf50062e95bc8534ca887", "duckdb": "242bf50062e95bc8534ca887", "generic": "242bf50062e95bc8534ca887", "motherduck": "901c5133ed47d4769286e860", "mysql": "242bf50062e95bc8534ca887", -"postgres": "242bf50062e95bc8534ca887", -"postgresql": "242bf50062e95bc8534ca887", +"postgres": "3f86c2b62e6298676c7001fd", +"postgresql": "3f86c2b62e6298676c7001fd", "snowflake": "242bf50062e95bc8534ca887", "tsql": "242bf50062e95bc8534ca887" } @@ -16432,8 +16432,8 @@ "generic": "5d25d5e13f81700741695ed4", "motherduck": "901c5133ed47d4769286e860", "mysql": "5d25d5e13f81700741695ed4", -"postgres": "5d25d5e13f81700741695ed4", -"postgresql": "5d25d5e13f81700741695ed4", +"postgres": "62131fb5b065f5de2e74a0f1", +"postgresql": "62131fb5b065f5de2e74a0f1", "snowflake": "5d25d5e13f81700741695ed4", "tsql": "5d25d5e13f81700741695ed4" } @@ -16442,33 +16442,33 @@ "type", { "DuckDB": "13f1401e7cd410600a4c9485", -"None": "13f1401e7cd410600a4c9485", -"bigquery": "74a30f6aa9def0b58afc616c", -"databricks": "13f1401e7cd410600a4c9485", +"None": "75c45736873ac5c99a62bc20", +"bigquery": "a686bb6db17ab079cfc79224", +"databricks": "75c45736873ac5c99a62bc20", "duckdb": "13f1401e7cd410600a4c9485", -"generic": "13f1401e7cd410600a4c9485", +"generic": "75c45736873ac5c99a62bc20", "motherduck": "901c5133ed47d4769286e860", -"mysql": "13f1401e7cd410600a4c9485", +"mysql": "75c45736873ac5c99a62bc20", "postgres": "13f1401e7cd410600a4c9485", "postgresql": "13f1401e7cd410600a4c9485", "snowflake": "9b5f44707b5f4764b82741be", -"tsql": "13f1401e7cd410600a4c9485" +"tsql": "75c45736873ac5c99a62bc20" } ], [ "type", { -"DuckDB": "6f851103102e7b986535376f", -"None": "6f851103102e7b986535376f", -"bigquery": "58b7e0908df666938398205f", -"databricks": "6f851103102e7b986535376f", -"duckdb": "6f851103102e7b986535376f", -"generic": "6f851103102e7b986535376f", +"DuckDB": "bf0a539a5f35525b6b4067c5", +"None": "8e2423c700938b33a6fea361", +"bigquery": "126d23e6f3408f484fe7bd92", +"databricks": "8e2423c700938b33a6fea361", +"duckdb": "bf0a539a5f35525b6b4067c5", +"generic": "8e2423c700938b33a6fea361", "motherduck": "901c5133ed47d4769286e860", -"mysql": "1a853242fb56557e2865a86a", -"postgres": "6f851103102e7b986535376f", -"postgresql": "6f851103102e7b986535376f", -"snowflake": "87800133cac38d9119180c0a", +"mysql": "64442499eea1962913b864ba", +"postgres": "fac7e3f22dda1f3edd9dee28", +"postgresql": "fac7e3f22dda1f3edd9dee28", +"snowflake": "fe64d523696db51fa15d432e", "tsql": "0e9ef5373e5d52970e8e1656" } ], @@ -16568,8 +16568,8 @@ "generic": "5d25d5e13f81700741695ed4", "motherduck": "901c5133ed47d4769286e860", "mysql": "5d25d5e13f81700741695ed4", -"postgres": "5d25d5e13f81700741695ed4", -"postgresql": "5d25d5e13f81700741695ed4", +"postgres": "62131fb5b065f5de2e74a0f1", +"postgresql": "62131fb5b065f5de2e74a0f1", "snowflake": "5d25d5e13f81700741695ed4", "tsql": "5d25d5e13f81700741695ed4" } @@ -16630,14 +16630,14 @@ { "DuckDB": "fe23594c781e79cbb0c8927a", "None": "fe23594c781e79cbb0c8927a", -"bigquery": "fe23594c781e79cbb0c8927a", +"bigquery": "8a3f094f18fb4047f0febf59", "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", "motherduck": "901c5133ed47d4769286e860", "mysql": "fe23594c781e79cbb0c8927a", -"postgres": "fe23594c781e79cbb0c8927a", -"postgresql": "fe23594c781e79cbb0c8927a", +"postgres": "8a3f094f18fb4047f0febf59", +"postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "fe23594c781e79cbb0c8927a" } @@ -16647,7 +16647,7 @@ { "DuckDB": "100c78a43fb3cbda50787a10", "None": "100c78a43fb3cbda50787a10", -"bigquery": "100c78a43fb3cbda50787a10", +"bigquery": "8d325bf2397f80882c281b76", "databricks": "100c78a43fb3cbda50787a10", "duckdb": "100c78a43fb3cbda50787a10", "generic": "100c78a43fb3cbda50787a10", @@ -16664,14 +16664,14 @@ { "DuckDB": "aab0389e2b5470136660bf8a", "None": "aab0389e2b5470136660bf8a", -"bigquery": "c9b810639795a7bfab91b98d", +"bigquery": "a428302e4848ffe877284c84", "databricks": "aab0389e2b5470136660bf8a", "duckdb": "aab0389e2b5470136660bf8a", "generic": "aab0389e2b5470136660bf8a", "motherduck": "901c5133ed47d4769286e860", "mysql": "aab0389e2b5470136660bf8a", -"postgres": "aab0389e2b5470136660bf8a", -"postgresql": "aab0389e2b5470136660bf8a", +"postgres": "f16105cbc61d547e6c08efc8", +"postgresql": "f16105cbc61d547e6c08efc8", "snowflake": "97a20344d811fb77c7690863", "tsql": "aab0389e2b5470136660bf8a" } @@ -16681,14 +16681,14 @@ { "DuckDB": "624eb95d280fa0653ec73f7f", "None": "624eb95d280fa0653ec73f7f", -"bigquery": "624eb95d280fa0653ec73f7f", +"bigquery": "837468174ef3e5cb4e4b4070", "databricks": "624eb95d280fa0653ec73f7f", "duckdb": "624eb95d280fa0653ec73f7f", "generic": "624eb95d280fa0653ec73f7f", "motherduck": "901c5133ed47d4769286e860", "mysql": "624eb95d280fa0653ec73f7f", -"postgres": "624eb95d280fa0653ec73f7f", -"postgresql": "624eb95d280fa0653ec73f7f", +"postgres": "9dd376ce83bd3444e52fdb88", +"postgresql": "9dd376ce83bd3444e52fdb88", "snowflake": "624eb95d280fa0653ec73f7f", "tsql": "624eb95d280fa0653ec73f7f" } @@ -16698,14 +16698,14 @@ { "DuckDB": "6b04f574865e71733e09ba1d", "None": "6b04f574865e71733e09ba1d", -"bigquery": "6b04f574865e71733e09ba1d", +"bigquery": "893ff554934dfc6f8253597b", "databricks": "6b04f574865e71733e09ba1d", "duckdb": "6b04f574865e71733e09ba1d", "generic": "6b04f574865e71733e09ba1d", "motherduck": "901c5133ed47d4769286e860", "mysql": "6b04f574865e71733e09ba1d", -"postgres": "6b04f574865e71733e09ba1d", -"postgresql": "6b04f574865e71733e09ba1d", +"postgres": "e4d1b966e983269b60041cc7", +"postgresql": "e4d1b966e983269b60041cc7", "snowflake": "6b04f574865e71733e09ba1d", "tsql": "6b04f574865e71733e09ba1d" } @@ -16823,8 +16823,8 @@ "generic": "fe23594c781e79cbb0c8927a", "motherduck": "901c5133ed47d4769286e860", "mysql": "fe23594c781e79cbb0c8927a", -"postgres": "fe23594c781e79cbb0c8927a", -"postgresql": "fe23594c781e79cbb0c8927a", +"postgres": "b0ce99394388f977f67fd1df", +"postgresql": "b0ce99394388f977f67fd1df", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "fe23594c781e79cbb0c8927a" } @@ -16834,14 +16834,14 @@ { "DuckDB": "de89632e277dbc8c97115419", "None": "de89632e277dbc8c97115419", -"bigquery": "de89632e277dbc8c97115419", +"bigquery": "5abf4ec799a2ec32fb10da7e", "databricks": "de89632e277dbc8c97115419", "duckdb": "de89632e277dbc8c97115419", "generic": "de89632e277dbc8c97115419", "motherduck": "901c5133ed47d4769286e860", "mysql": "de89632e277dbc8c97115419", -"postgres": "de89632e277dbc8c97115419", -"postgresql": "de89632e277dbc8c97115419", +"postgres": "5abf4ec799a2ec32fb10da7e", +"postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", "tsql": "de89632e277dbc8c97115419" } @@ -16868,7 +16868,7 @@ { "DuckDB": "aae2b1fe6f2d9807ecde412b", "None": "aae2b1fe6f2d9807ecde412b", -"bigquery": "e1c36a5cdd2427ff2b089ac9", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "aae2b1fe6f2d9807ecde412b", "duckdb": "aae2b1fe6f2d9807ecde412b", "generic": "aae2b1fe6f2d9807ecde412b", @@ -16884,17 +16884,17 @@ "type", { "DuckDB": "16adcdd909f51e3f33d7cdde", -"None": "1f3277ab2fff19e327e21fe0", -"bigquery": "1f3277ab2fff19e327e21fe0", -"databricks": "1f3277ab2fff19e327e21fe0", +"None": "ac92c18bc03647c2ff622e35", +"bigquery": "ac92c18bc03647c2ff622e35", +"databricks": "ac92c18bc03647c2ff622e35", "duckdb": "16adcdd909f51e3f33d7cdde", -"generic": "1f3277ab2fff19e327e21fe0", +"generic": "ac92c18bc03647c2ff622e35", "motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", -"postgres": "de3f085b785f38a4bda19034", -"postgresql": "de3f085b785f38a4bda19034", +"mysql": "ac92c18bc03647c2ff622e35", +"postgres": "ac92c18bc03647c2ff622e35", +"postgresql": "ac92c18bc03647c2ff622e35", "snowflake": "42ed29994060215ec30494f1", -"tsql": "d1dc9a140dbe2eb1874c6ad7" +"tsql": "ac92c18bc03647c2ff622e35" } ], [ @@ -16919,14 +16919,14 @@ { "DuckDB": "3a7ffeb8f81df6ccd57f3689", "None": "3a7ffeb8f81df6ccd57f3689", -"bigquery": "3a7ffeb8f81df6ccd57f3689", +"bigquery": "67d251c042daee0bb2bf7351", "databricks": "3a7ffeb8f81df6ccd57f3689", "duckdb": "3a7ffeb8f81df6ccd57f3689", "generic": "3a7ffeb8f81df6ccd57f3689", "motherduck": "901c5133ed47d4769286e860", "mysql": "3a7ffeb8f81df6ccd57f3689", -"postgres": "3a7ffeb8f81df6ccd57f3689", -"postgresql": "3a7ffeb8f81df6ccd57f3689", +"postgres": "062e51ec86ccd542eb78500d", +"postgresql": "062e51ec86ccd542eb78500d", "snowflake": "3a7ffeb8f81df6ccd57f3689", "tsql": "3a7ffeb8f81df6ccd57f3689" } @@ -16993,8 +16993,8 @@ "generic": "19a1a80ffc06169f32694de8", "motherduck": "901c5133ed47d4769286e860", "mysql": "19a1a80ffc06169f32694de8", -"postgres": "19a1a80ffc06169f32694de8", -"postgresql": "19a1a80ffc06169f32694de8", +"postgres": "ab1f5f46e69284778bd66d58", +"postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", "tsql": "19a1a80ffc06169f32694de8" } @@ -17055,14 +17055,14 @@ { "DuckDB": "dbf9ebbc7016f5ac5df2817e", "None": "dbf9ebbc7016f5ac5df2817e", -"bigquery": "dbf9ebbc7016f5ac5df2817e", +"bigquery": "837468174ef3e5cb4e4b4070", "databricks": "dbf9ebbc7016f5ac5df2817e", "duckdb": "dbf9ebbc7016f5ac5df2817e", "generic": "dbf9ebbc7016f5ac5df2817e", "motherduck": "901c5133ed47d4769286e860", "mysql": "dbf9ebbc7016f5ac5df2817e", -"postgres": "dbf9ebbc7016f5ac5df2817e", -"postgresql": "dbf9ebbc7016f5ac5df2817e", +"postgres": "4cccf86e62d627a7a8b57726", +"postgresql": "4cccf86e62d627a7a8b57726", "snowflake": "dbf9ebbc7016f5ac5df2817e", "tsql": "dbf9ebbc7016f5ac5df2817e" } @@ -17072,14 +17072,14 @@ { "DuckDB": "aab0389e2b5470136660bf8a", "None": "aab0389e2b5470136660bf8a", -"bigquery": "c9b810639795a7bfab91b98d", +"bigquery": "a428302e4848ffe877284c84", "databricks": "aab0389e2b5470136660bf8a", "duckdb": "aab0389e2b5470136660bf8a", "generic": "aab0389e2b5470136660bf8a", "motherduck": "901c5133ed47d4769286e860", "mysql": "aab0389e2b5470136660bf8a", -"postgres": "aab0389e2b5470136660bf8a", -"postgresql": "aab0389e2b5470136660bf8a", +"postgres": "f16105cbc61d547e6c08efc8", +"postgresql": "f16105cbc61d547e6c08efc8", "snowflake": "97a20344d811fb77c7690863", "tsql": "aab0389e2b5470136660bf8a" } @@ -17155,33 +17155,33 @@ [ "type", { -"DuckDB": "51b0e597909858f7f7aa4571", -"None": "51b0e597909858f7f7aa4571", +"DuckDB": "9efea5795b256d49cab6a366", +"None": "9efea5795b256d49cab6a366", "bigquery": "deb9d53b04b7825e460d826a", -"databricks": "846f4ee64c34aab0cdabb9a0", -"duckdb": "51b0e597909858f7f7aa4571", -"generic": "51b0e597909858f7f7aa4571", +"databricks": "d0f04a0ad4aff66d31330f7a", +"duckdb": "9efea5795b256d49cab6a366", +"generic": "9efea5795b256d49cab6a366", "motherduck": "901c5133ed47d4769286e860", -"mysql": "51b0e597909858f7f7aa4571", -"postgres": "51b0e597909858f7f7aa4571", -"postgresql": "51b0e597909858f7f7aa4571", +"mysql": "9efea5795b256d49cab6a366", +"postgres": "9efea5795b256d49cab6a366", +"postgresql": "9efea5795b256d49cab6a366", "snowflake": "50d14e3a5691e6cb8689aa29", -"tsql": "51b0e597909858f7f7aa4571" +"tsql": "9efea5795b256d49cab6a366" } ], [ "type", { -"DuckDB": "8698d724534891869075175e", -"None": "0017a88bb60ec3403c786ed8", +"DuckDB": "29525edc1ad146bad4372f59", +"None": "2795066c9a71fa3e3780c73b", "bigquery": "2795066c9a71fa3e3780c73b", -"databricks": "0017a88bb60ec3403c786ed8", -"duckdb": "8698d724534891869075175e", -"generic": "0017a88bb60ec3403c786ed8", +"databricks": "2795066c9a71fa3e3780c73b", +"duckdb": "29525edc1ad146bad4372f59", +"generic": "2795066c9a71fa3e3780c73b", "motherduck": "901c5133ed47d4769286e860", "mysql": "6c03a3450ea4742ee7396e3c", -"postgres": "8698d724534891869075175e", -"postgresql": "8698d724534891869075175e", +"postgres": "29525edc1ad146bad4372f59", +"postgresql": "29525edc1ad146bad4372f59", "snowflake": "b3b752c645651b849e23d39a", "tsql": "d1dc9a140dbe2eb1874c6ad7" } @@ -17191,14 +17191,14 @@ { "DuckDB": "881106a4a0d2de0ff7c56373", "None": "881106a4a0d2de0ff7c56373", -"bigquery": "881106a4a0d2de0ff7c56373", +"bigquery": "893ff554934dfc6f8253597b", "databricks": "881106a4a0d2de0ff7c56373", "duckdb": "881106a4a0d2de0ff7c56373", "generic": "881106a4a0d2de0ff7c56373", "motherduck": "901c5133ed47d4769286e860", "mysql": "881106a4a0d2de0ff7c56373", -"postgres": "881106a4a0d2de0ff7c56373", -"postgresql": "881106a4a0d2de0ff7c56373", +"postgres": "cf167ec395fc843602e3b1e0", +"postgresql": "cf167ec395fc843602e3b1e0", "snowflake": "881106a4a0d2de0ff7c56373", "tsql": "881106a4a0d2de0ff7c56373" } @@ -17214,8 +17214,8 @@ "generic": "669084d1ea704ae5b8ee05c2", "motherduck": "901c5133ed47d4769286e860", "mysql": "669084d1ea704ae5b8ee05c2", -"postgres": "669084d1ea704ae5b8ee05c2", -"postgresql": "669084d1ea704ae5b8ee05c2", +"postgres": "93c1b5b5960e26479baf88d0", +"postgresql": "93c1b5b5960e26479baf88d0", "snowflake": "669084d1ea704ae5b8ee05c2", "tsql": "669084d1ea704ae5b8ee05c2" } @@ -17231,8 +17231,8 @@ "generic": "013a8446f043affffcf194f8", "motherduck": "901c5133ed47d4769286e860", "mysql": "013a8446f043affffcf194f8", -"postgres": "013a8446f043affffcf194f8", -"postgresql": "013a8446f043affffcf194f8", +"postgres": "b0477b36767a62145aa9158c", +"postgresql": "b0477b36767a62145aa9158c", "snowflake": "97a20344d811fb77c7690863", "tsql": "013a8446f043affffcf194f8" } @@ -17248,8 +17248,8 @@ "generic": "804fec6876d0b43935790142", "motherduck": "901c5133ed47d4769286e860", "mysql": "804fec6876d0b43935790142", -"postgres": "804fec6876d0b43935790142", -"postgresql": "804fec6876d0b43935790142", +"postgres": "191fc59c754706512337edc1", +"postgresql": "191fc59c754706512337edc1", "snowflake": "804fec6876d0b43935790142", "tsql": "804fec6876d0b43935790142" } @@ -17327,14 +17327,14 @@ { "DuckDB": "fe23594c781e79cbb0c8927a", "None": "fe23594c781e79cbb0c8927a", -"bigquery": "fe23594c781e79cbb0c8927a", +"bigquery": "8a3f094f18fb4047f0febf59", "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", "motherduck": "901c5133ed47d4769286e860", "mysql": "fe23594c781e79cbb0c8927a", -"postgres": "fe23594c781e79cbb0c8927a", -"postgresql": "fe23594c781e79cbb0c8927a", +"postgres": "8a3f094f18fb4047f0febf59", +"postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "fe23594c781e79cbb0c8927a" } @@ -17461,18 +17461,18 @@ [ "type", { -"DuckDB": "f0c468b3e6d6df98b4358aff", -"None": "752ae7fc618de4a182e18bd3", -"bigquery": "752ae7fc618de4a182e18bd3", -"databricks": "aa5bc52ba6ea9e0a428084da", -"duckdb": "f0c468b3e6d6df98b4358aff", -"generic": "752ae7fc618de4a182e18bd3", +"DuckDB": "a8f9cb9212d342eb9d2854d0", +"None": "3213ca7d92e0eff2f419451a", +"bigquery": "3213ca7d92e0eff2f419451a", +"databricks": "3213ca7d92e0eff2f419451a", +"duckdb": "a8f9cb9212d342eb9d2854d0", +"generic": "3213ca7d92e0eff2f419451a", "motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", -"postgres": "f0c468b3e6d6df98b4358aff", -"postgresql": "f0c468b3e6d6df98b4358aff", +"mysql": "3213ca7d92e0eff2f419451a", +"postgres": "3213ca7d92e0eff2f419451a", +"postgresql": "3213ca7d92e0eff2f419451a", "snowflake": "6dc518f5329c32ce13bfa506", -"tsql": "d1dc9a140dbe2eb1874c6ad7" +"tsql": "3213ca7d92e0eff2f419451a" } ], [ @@ -17480,14 +17480,14 @@ { "DuckDB": "9829fa9011a92bb968bb5996", "None": "9829fa9011a92bb968bb5996", -"bigquery": "9829fa9011a92bb968bb5996", +"bigquery": "c74425c5ce650ac69c83ca5b", "databricks": "9829fa9011a92bb968bb5996", "duckdb": "9829fa9011a92bb968bb5996", "generic": "9829fa9011a92bb968bb5996", "motherduck": "901c5133ed47d4769286e860", "mysql": "9829fa9011a92bb968bb5996", -"postgres": "9829fa9011a92bb968bb5996", -"postgresql": "9829fa9011a92bb968bb5996", +"postgres": "4e937b76a5c3e503c1fd6e22", +"postgresql": "4e937b76a5c3e503c1fd6e22", "snowflake": "9829fa9011a92bb968bb5996", "tsql": "9829fa9011a92bb968bb5996" } @@ -17531,14 +17531,14 @@ { "DuckDB": "18db5afe2c1e80d1ee652b54", "None": "18db5afe2c1e80d1ee652b54", -"bigquery": "e1c36a5cdd2427ff2b089ac9", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "18db5afe2c1e80d1ee652b54", "duckdb": "18db5afe2c1e80d1ee652b54", "generic": "18db5afe2c1e80d1ee652b54", "motherduck": "901c5133ed47d4769286e860", "mysql": "18db5afe2c1e80d1ee652b54", -"postgres": "18db5afe2c1e80d1ee652b54", -"postgresql": "18db5afe2c1e80d1ee652b54", +"postgres": "47d64a5994d42a2b577166ea", +"postgresql": "47d64a5994d42a2b577166ea", "snowflake": "18db5afe2c1e80d1ee652b54", "tsql": "18db5afe2c1e80d1ee652b54" } @@ -17547,17 +17547,17 @@ "type", { "DuckDB": "c50a316d091c821866e4bfba", -"None": "c50a316d091c821866e4bfba", -"bigquery": "c50a316d091c821866e4bfba", -"databricks": "c50a316d091c821866e4bfba", +"None": "e6ffdece2977be55da73b59a", +"bigquery": "e6ffdece2977be55da73b59a", +"databricks": "e6ffdece2977be55da73b59a", "duckdb": "c50a316d091c821866e4bfba", -"generic": "c50a316d091c821866e4bfba", +"generic": "e6ffdece2977be55da73b59a", "motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"mysql": "e6ffdece2977be55da73b59a", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", -"tsql": "c50a316d091c821866e4bfba" +"tsql": "e6ffdece2977be55da73b59a" } ], [ @@ -17571,8 +17571,8 @@ "generic": "19a1a80ffc06169f32694de8", "motherduck": "901c5133ed47d4769286e860", "mysql": "19a1a80ffc06169f32694de8", -"postgres": "19a1a80ffc06169f32694de8", -"postgresql": "19a1a80ffc06169f32694de8", +"postgres": "ab1f5f46e69284778bd66d58", +"postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", "tsql": "19a1a80ffc06169f32694de8" } @@ -17599,14 +17599,14 @@ { "DuckDB": "837168a49b84e7ed918ea148", "None": "837168a49b84e7ed918ea148", -"bigquery": "837168a49b84e7ed918ea148", +"bigquery": "67d251c042daee0bb2bf7351", "databricks": "837168a49b84e7ed918ea148", "duckdb": "837168a49b84e7ed918ea148", "generic": "837168a49b84e7ed918ea148", "motherduck": "901c5133ed47d4769286e860", "mysql": "837168a49b84e7ed918ea148", -"postgres": "837168a49b84e7ed918ea148", -"postgresql": "837168a49b84e7ed918ea148", +"postgres": "c28e9468b9f7a7f4e67d5b30", +"postgresql": "c28e9468b9f7a7f4e67d5b30", "snowflake": "837168a49b84e7ed918ea148", "tsql": "837168a49b84e7ed918ea148" } @@ -17667,14 +17667,14 @@ { "DuckDB": "08736485547ba339930697e9", "None": "08736485547ba339930697e9", -"bigquery": "08736485547ba339930697e9", +"bigquery": "837468174ef3e5cb4e4b4070", "databricks": "08736485547ba339930697e9", "duckdb": "08736485547ba339930697e9", "generic": "08736485547ba339930697e9", "motherduck": "901c5133ed47d4769286e860", "mysql": "08736485547ba339930697e9", -"postgres": "08736485547ba339930697e9", -"postgresql": "08736485547ba339930697e9", +"postgres": "eecd452b4f5f3f4133d0bd0a", +"postgresql": "eecd452b4f5f3f4133d0bd0a", "snowflake": "08736485547ba339930697e9", "tsql": "08736485547ba339930697e9" } @@ -17701,7 +17701,7 @@ { "DuckDB": "461923a0d34defd0c726d6e5", "None": "461923a0d34defd0c726d6e5", -"bigquery": "b5c4d5044b2a6e70633f0d0a", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "461923a0d34defd0c726d6e5", "duckdb": "461923a0d34defd0c726d6e5", "generic": "461923a0d34defd0c726d6e5", @@ -17718,14 +17718,14 @@ { "DuckDB": "624eb95d280fa0653ec73f7f", "None": "624eb95d280fa0653ec73f7f", -"bigquery": "624eb95d280fa0653ec73f7f", +"bigquery": "837468174ef3e5cb4e4b4070", "databricks": "624eb95d280fa0653ec73f7f", "duckdb": "624eb95d280fa0653ec73f7f", "generic": "624eb95d280fa0653ec73f7f", "motherduck": "901c5133ed47d4769286e860", "mysql": "624eb95d280fa0653ec73f7f", -"postgres": "624eb95d280fa0653ec73f7f", -"postgresql": "624eb95d280fa0653ec73f7f", +"postgres": "9dd376ce83bd3444e52fdb88", +"postgresql": "9dd376ce83bd3444e52fdb88", "snowflake": "624eb95d280fa0653ec73f7f", "tsql": "624eb95d280fa0653ec73f7f" } @@ -17734,17 +17734,17 @@ "type", { "DuckDB": "70fb4728c3adcf6dbe4975d5", -"None": "70fb4728c3adcf6dbe4975d5", -"bigquery": "70fb4728c3adcf6dbe4975d5", -"databricks": "70fb4728c3adcf6dbe4975d5", +"None": "fb4b6f5101f973d16ea8d1f3", +"bigquery": "fb4b6f5101f973d16ea8d1f3", +"databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", -"generic": "70fb4728c3adcf6dbe4975d5", +"generic": "fb4b6f5101f973d16ea8d1f3", "motherduck": "901c5133ed47d4769286e860", -"mysql": "70fb4728c3adcf6dbe4975d5", +"mysql": "fb4b6f5101f973d16ea8d1f3", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", -"tsql": "70fb4728c3adcf6dbe4975d5" +"tsql": "fb4b6f5101f973d16ea8d1f3" } ], [ @@ -17758,8 +17758,8 @@ "generic": "bce60580ef3a1dc4ce569fff", "motherduck": "901c5133ed47d4769286e860", "mysql": "bce60580ef3a1dc4ce569fff", -"postgres": "bce60580ef3a1dc4ce569fff", -"postgresql": "bce60580ef3a1dc4ce569fff", +"postgres": "eee236ea1427bdddbaf088ac", +"postgresql": "eee236ea1427bdddbaf088ac", "snowflake": "bce60580ef3a1dc4ce569fff", "tsql": "bce60580ef3a1dc4ce569fff" } @@ -17785,17 +17785,17 @@ "type", { "DuckDB": "858855737713d0cff353f8ac", -"None": "858855737713d0cff353f8ac", -"bigquery": "858855737713d0cff353f8ac", -"databricks": "858855737713d0cff353f8ac", +"None": "50b29536a9929d11dbcb949a", +"bigquery": "8b8d5f75ba6dec68b4cebc6b", +"databricks": "50b29536a9929d11dbcb949a", "duckdb": "858855737713d0cff353f8ac", -"generic": "858855737713d0cff353f8ac", +"generic": "50b29536a9929d11dbcb949a", "motherduck": "901c5133ed47d4769286e860", -"mysql": "858855737713d0cff353f8ac", -"postgres": "858855737713d0cff353f8ac", -"postgresql": "858855737713d0cff353f8ac", +"mysql": "50b29536a9929d11dbcb949a", +"postgres": "74a30f6aa9def0b58afc616c", +"postgresql": "74a30f6aa9def0b58afc616c", "snowflake": "858855737713d0cff353f8ac", -"tsql": "858855737713d0cff353f8ac" +"tsql": "50b29536a9929d11dbcb949a" } ], [ @@ -17803,7 +17803,7 @@ { "DuckDB": "62368fc15cefd5807807b209", "None": "62368fc15cefd5807807b209", -"bigquery": "e6ffdece2977be55da73b59a", +"bigquery": "c50a316d091c821866e4bfba", "databricks": "62368fc15cefd5807807b209", "duckdb": "62368fc15cefd5807807b209", "generic": "62368fc15cefd5807807b209", @@ -17887,17 +17887,17 @@ "type", { "DuckDB": "d81d8c19074af0e39239164d", -"None": "1fec266134381c760a254d16", -"bigquery": "1fec266134381c760a254d16", -"databricks": "1fec266134381c760a254d16", +"None": "a52676af0c6b750acc33f021", +"bigquery": "1e75e5227bea0ae294ac7af1", +"databricks": "a52676af0c6b750acc33f021", "duckdb": "d81d8c19074af0e39239164d", -"generic": "1fec266134381c760a254d16", +"generic": "a52676af0c6b750acc33f021", "motherduck": "901c5133ed47d4769286e860", -"mysql": "1fec266134381c760a254d16", -"postgres": "1fec266134381c760a254d16", -"postgresql": "1fec266134381c760a254d16", +"mysql": "a52676af0c6b750acc33f021", +"postgres": "a52676af0c6b750acc33f021", +"postgresql": "a52676af0c6b750acc33f021", "snowflake": "1fec266134381c760a254d16", -"tsql": "1fec266134381c760a254d16" +"tsql": "a52676af0c6b750acc33f021" } ], [ @@ -17928,8 +17928,8 @@ "generic": "b561884132dbe7e42b719110", "motherduck": "901c5133ed47d4769286e860", "mysql": "b561884132dbe7e42b719110", -"postgres": "b561884132dbe7e42b719110", -"postgresql": "b561884132dbe7e42b719110", +"postgres": "5aa51d84ac5e6c7a9306f348", +"postgresql": "5aa51d84ac5e6c7a9306f348", "snowflake": "b561884132dbe7e42b719110", "tsql": "b561884132dbe7e42b719110" } @@ -17955,17 +17955,17 @@ "type", { "DuckDB": "a428302e4848ffe877284c84", -"None": "a428302e4848ffe877284c84", -"bigquery": "a428302e4848ffe877284c84", -"databricks": "a428302e4848ffe877284c84", +"None": "c9b810639795a7bfab91b98d", +"bigquery": "c9b810639795a7bfab91b98d", +"databricks": "c9b810639795a7bfab91b98d", "duckdb": "a428302e4848ffe877284c84", -"generic": "a428302e4848ffe877284c84", +"generic": "c9b810639795a7bfab91b98d", "motherduck": "901c5133ed47d4769286e860", -"mysql": "a428302e4848ffe877284c84", +"mysql": "c9b810639795a7bfab91b98d", "postgres": "a428302e4848ffe877284c84", "postgresql": "a428302e4848ffe877284c84", "snowflake": "652ff8b21f638eac8fdc46c5", -"tsql": "a428302e4848ffe877284c84" +"tsql": "c9b810639795a7bfab91b98d" } ], [ @@ -17973,7 +17973,7 @@ { "DuckDB": "47d74d1b45fedd02ec465220", "None": "47d74d1b45fedd02ec465220", -"bigquery": "47d74d1b45fedd02ec465220", +"bigquery": "15362056f760cad102922b12", "databricks": "47d74d1b45fedd02ec465220", "duckdb": "47d74d1b45fedd02ec465220", "generic": "47d74d1b45fedd02ec465220", @@ -18006,17 +18006,17 @@ "type", { "DuckDB": "1879bc874141d9160106e020", -"None": "c513dcc8ed8dfd8935374a47", -"bigquery": "c513dcc8ed8dfd8935374a47", -"databricks": "c513dcc8ed8dfd8935374a47", +"None": "072b6126112a6217e028fc8f", +"bigquery": "072b6126112a6217e028fc8f", +"databricks": "072b6126112a6217e028fc8f", "duckdb": "1879bc874141d9160106e020", -"generic": "c513dcc8ed8dfd8935374a47", +"generic": "072b6126112a6217e028fc8f", "motherduck": "901c5133ed47d4769286e860", -"mysql": "c513dcc8ed8dfd8935374a47", +"mysql": "072b6126112a6217e028fc8f", "postgres": "c513dcc8ed8dfd8935374a47", "postgresql": "c513dcc8ed8dfd8935374a47", "snowflake": "c513dcc8ed8dfd8935374a47", -"tsql": "c513dcc8ed8dfd8935374a47" +"tsql": "072b6126112a6217e028fc8f" } ], [ @@ -18030,8 +18030,8 @@ "generic": "2705f19942a4194a72576911", "motherduck": "901c5133ed47d4769286e860", "mysql": "2705f19942a4194a72576911", -"postgres": "2705f19942a4194a72576911", -"postgresql": "2705f19942a4194a72576911", +"postgres": "5226d7e2a62c7650a87d6e84", +"postgresql": "5226d7e2a62c7650a87d6e84", "snowflake": "2705f19942a4194a72576911", "tsql": "2705f19942a4194a72576911" } @@ -18056,16 +18056,16 @@ [ "type", { -"DuckDB": "c43574432afc90bcba6944b2", -"None": "7040afbb3a088baf55e328b2", -"bigquery": "ba611057434e43344935471b", -"databricks": "ba611057434e43344935471b", -"duckdb": "c43574432afc90bcba6944b2", -"generic": "7040afbb3a088baf55e328b2", +"DuckDB": "83f3a2696c58aa8d709a227a", +"None": "3866c29c7e0df841d464b98c", +"bigquery": "3866c29c7e0df841d464b98c", +"databricks": "3866c29c7e0df841d464b98c", +"duckdb": "83f3a2696c58aa8d709a227a", +"generic": "3866c29c7e0df841d464b98c", "motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", -"postgres": "1b3243409566f14d81c11fff", -"postgresql": "1b3243409566f14d81c11fff", +"mysql": "3866c29c7e0df841d464b98c", +"postgres": "3866c29c7e0df841d464b98c", +"postgresql": "3866c29c7e0df841d464b98c", "snowflake": "06a9af03923de91e5e27860a", "tsql": "3866c29c7e0df841d464b98c" } @@ -18074,17 +18074,17 @@ "type", { "DuckDB": "d229e289e63364bf0d56e920", -"None": "28c3447161aa1c0a550659b8", -"bigquery": "28c3447161aa1c0a550659b8", -"databricks": "b8e50cd0369cf0810e7054be", +"None": "5b121b9b6ea0450c8d33ee34", +"bigquery": "ded028ebfb27c12284a8381a", +"databricks": "5b121b9b6ea0450c8d33ee34", "duckdb": "d229e289e63364bf0d56e920", -"generic": "28c3447161aa1c0a550659b8", +"generic": "5b121b9b6ea0450c8d33ee34", "motherduck": "901c5133ed47d4769286e860", -"mysql": "28c3447161aa1c0a550659b8", -"postgres": "28c3447161aa1c0a550659b8", -"postgresql": "28c3447161aa1c0a550659b8", +"mysql": "5b121b9b6ea0450c8d33ee34", +"postgres": "5b121b9b6ea0450c8d33ee34", +"postgresql": "5b121b9b6ea0450c8d33ee34", "snowflake": "d90e8915a295b2cd5e690bd1", -"tsql": "28c3447161aa1c0a550659b8" +"tsql": "5b121b9b6ea0450c8d33ee34" } ], [ @@ -18098,8 +18098,8 @@ "generic": "a3acd7ba35e81a800f967bd3", "motherduck": "901c5133ed47d4769286e860", "mysql": "a3acd7ba35e81a800f967bd3", -"postgres": "a3acd7ba35e81a800f967bd3", -"postgresql": "a3acd7ba35e81a800f967bd3", +"postgres": "42d4a23d9a52e79d0c3458d3", +"postgresql": "42d4a23d9a52e79d0c3458d3", "snowflake": "a3acd7ba35e81a800f967bd3", "tsql": "a3acd7ba35e81a800f967bd3" } @@ -18115,8 +18115,8 @@ "generic": "c6106f30f23f392e17eadef4", "motherduck": "901c5133ed47d4769286e860", "mysql": "c6106f30f23f392e17eadef4", -"postgres": "c6106f30f23f392e17eadef4", -"postgresql": "c6106f30f23f392e17eadef4", +"postgres": "f43dac38f34a1c0618a3f812", +"postgresql": "f43dac38f34a1c0618a3f812", "snowflake": "c6106f30f23f392e17eadef4", "tsql": "c6106f30f23f392e17eadef4" } @@ -18132,8 +18132,8 @@ "generic": "804fec6876d0b43935790142", "motherduck": "901c5133ed47d4769286e860", "mysql": "804fec6876d0b43935790142", -"postgres": "804fec6876d0b43935790142", -"postgresql": "804fec6876d0b43935790142", +"postgres": "191fc59c754706512337edc1", +"postgresql": "191fc59c754706512337edc1", "snowflake": "804fec6876d0b43935790142", "tsql": "804fec6876d0b43935790142" } @@ -18166,8 +18166,8 @@ "generic": "7b87db889221e05e13ec3845", "motherduck": "901c5133ed47d4769286e860", "mysql": "7b87db889221e05e13ec3845", -"postgres": "7b87db889221e05e13ec3845", -"postgresql": "7b87db889221e05e13ec3845", +"postgres": "07fffe8743dfd4e88e6b3f18", +"postgresql": "07fffe8743dfd4e88e6b3f18", "snowflake": "7b87db889221e05e13ec3845", "tsql": "7b87db889221e05e13ec3845" } @@ -18183,8 +18183,8 @@ "generic": "19a1a80ffc06169f32694de8", "motherduck": "901c5133ed47d4769286e860", "mysql": "19a1a80ffc06169f32694de8", -"postgres": "19a1a80ffc06169f32694de8", -"postgresql": "19a1a80ffc06169f32694de8", +"postgres": "ab1f5f46e69284778bd66d58", +"postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", "tsql": "19a1a80ffc06169f32694de8" } @@ -18203,7 +18203,7 @@ "postgres": "c69a366e715a81bca598d475", "postgresql": "c69a366e715a81bca598d475", "snowflake": "f41d94b0bed9910074641da6", -"tsql": "c69a366e715a81bca598d475" +"tsql": "c036322107e1c935c7bc6edd" } ], [ @@ -18211,7 +18211,7 @@ { "DuckDB": "558da38ec4d9d5d6c72f60cf", "None": "558da38ec4d9d5d6c72f60cf", -"bigquery": "e6ffdece2977be55da73b59a", +"bigquery": "c50a316d091c821866e4bfba", "databricks": "558da38ec4d9d5d6c72f60cf", "duckdb": "558da38ec4d9d5d6c72f60cf", "generic": "558da38ec4d9d5d6c72f60cf", @@ -18251,8 +18251,8 @@ "generic": "279ea7f6f6a3910de04b795b", "motherduck": "901c5133ed47d4769286e860", "mysql": "279ea7f6f6a3910de04b795b", -"postgres": "279ea7f6f6a3910de04b795b", -"postgresql": "279ea7f6f6a3910de04b795b", +"postgres": "bc520aebd5872e40030ec7d9", +"postgresql": "bc520aebd5872e40030ec7d9", "snowflake": "279ea7f6f6a3910de04b795b", "tsql": "279ea7f6f6a3910de04b795b" } @@ -18263,11 +18263,11 @@ "DuckDB": "92e8caae709d951bb8ed59cb", "None": "92e8caae709d951bb8ed59cb", "bigquery": "92e8caae709d951bb8ed59cb", -"databricks": "1758ac00dad1bae84521b3fb", +"databricks": "92e8caae709d951bb8ed59cb", "duckdb": "92e8caae709d951bb8ed59cb", "generic": "92e8caae709d951bb8ed59cb", "motherduck": "901c5133ed47d4769286e860", -"mysql": "1758ac00dad1bae84521b3fb", +"mysql": "92e8caae709d951bb8ed59cb", "postgres": "92e8caae709d951bb8ed59cb", "postgresql": "92e8caae709d951bb8ed59cb", "snowflake": "92e8caae709d951bb8ed59cb", @@ -18302,8 +18302,8 @@ "generic": "a686bb6db17ab079cfc79224", "motherduck": "901c5133ed47d4769286e860", "mysql": "a686bb6db17ab079cfc79224", -"postgres": "a686bb6db17ab079cfc79224", -"postgresql": "a686bb6db17ab079cfc79224", +"postgres": "74a30f6aa9def0b58afc616c", +"postgresql": "74a30f6aa9def0b58afc616c", "snowflake": "1096c3c8687c8e8eb538ff74", "tsql": "a686bb6db17ab079cfc79224" } @@ -18319,8 +18319,8 @@ "generic": "56f06885daf94b4fcc0a624b", "motherduck": "901c5133ed47d4769286e860", "mysql": "56f06885daf94b4fcc0a624b", -"postgres": "56f06885daf94b4fcc0a624b", -"postgresql": "56f06885daf94b4fcc0a624b", +"postgres": "d8c1f10bbe9d46ecb6ee0bc7", +"postgresql": "d8c1f10bbe9d46ecb6ee0bc7", "snowflake": "56f06885daf94b4fcc0a624b", "tsql": "56f06885daf94b4fcc0a624b" } @@ -18370,8 +18370,8 @@ "generic": "e57ac1541fb51b362cd5b470", "motherduck": "901c5133ed47d4769286e860", "mysql": "e57ac1541fb51b362cd5b470", -"postgres": "e57ac1541fb51b362cd5b470", -"postgresql": "e57ac1541fb51b362cd5b470", +"postgres": "396b2ce32efb5365b393c230", +"postgresql": "396b2ce32efb5365b393c230", "snowflake": "4b356542044f1267fcfc487a", "tsql": "e57ac1541fb51b362cd5b470" } @@ -18404,8 +18404,8 @@ "generic": "669084d1ea704ae5b8ee05c2", "motherduck": "901c5133ed47d4769286e860", "mysql": "669084d1ea704ae5b8ee05c2", -"postgres": "669084d1ea704ae5b8ee05c2", -"postgresql": "669084d1ea704ae5b8ee05c2", +"postgres": "93c1b5b5960e26479baf88d0", +"postgresql": "93c1b5b5960e26479baf88d0", "snowflake": "669084d1ea704ae5b8ee05c2", "tsql": "669084d1ea704ae5b8ee05c2" } @@ -18421,8 +18421,8 @@ "generic": "d2c97d221599d038d9875873", "motherduck": "901c5133ed47d4769286e860", "mysql": "d2c97d221599d038d9875873", -"postgres": "d2c97d221599d038d9875873", -"postgresql": "d2c97d221599d038d9875873", +"postgres": "55155766823b6a3a5e882fc2", +"postgresql": "55155766823b6a3a5e882fc2", "snowflake": "d2c97d221599d038d9875873", "tsql": "d2c97d221599d038d9875873" } @@ -18455,8 +18455,8 @@ "generic": "4c78bdde0b2dfd77ab71c9e8", "motherduck": "901c5133ed47d4769286e860", "mysql": "4c78bdde0b2dfd77ab71c9e8", -"postgres": "4c78bdde0b2dfd77ab71c9e8", -"postgresql": "4c78bdde0b2dfd77ab71c9e8", +"postgres": "e802515d969e770f686478b5", +"postgresql": "e802515d969e770f686478b5", "snowflake": "4c78bdde0b2dfd77ab71c9e8", "tsql": "4c78bdde0b2dfd77ab71c9e8" } @@ -18499,17 +18499,17 @@ "type", { "DuckDB": "f46cf4b4e131aa122b5b33d6", -"None": "f46cf4b4e131aa122b5b33d6", -"bigquery": "f46cf4b4e131aa122b5b33d6", -"databricks": "f46cf4b4e131aa122b5b33d6", +"None": "f774d5e8feadbf11edaa26b1", +"bigquery": "9bafb08d41fd1c208b5deca4", +"databricks": "f774d5e8feadbf11edaa26b1", "duckdb": "f46cf4b4e131aa122b5b33d6", -"generic": "f46cf4b4e131aa122b5b33d6", +"generic": "f774d5e8feadbf11edaa26b1", "motherduck": "901c5133ed47d4769286e860", -"mysql": "f46cf4b4e131aa122b5b33d6", +"mysql": "f774d5e8feadbf11edaa26b1", "postgres": "f46cf4b4e131aa122b5b33d6", "postgresql": "f46cf4b4e131aa122b5b33d6", "snowflake": "f46cf4b4e131aa122b5b33d6", -"tsql": "f46cf4b4e131aa122b5b33d6" +"tsql": "f774d5e8feadbf11edaa26b1" } ], [ @@ -18534,14 +18534,14 @@ { "DuckDB": "6eb49ceb88fcb2297831ca05", "None": "6eb49ceb88fcb2297831ca05", -"bigquery": "6eb49ceb88fcb2297831ca05", +"bigquery": "c74425c5ce650ac69c83ca5b", "databricks": "6eb49ceb88fcb2297831ca05", "duckdb": "6eb49ceb88fcb2297831ca05", "generic": "6eb49ceb88fcb2297831ca05", "motherduck": "901c5133ed47d4769286e860", "mysql": "6eb49ceb88fcb2297831ca05", -"postgres": "6eb49ceb88fcb2297831ca05", -"postgresql": "6eb49ceb88fcb2297831ca05", +"postgres": "623e22ea8921c9e57a15475b", +"postgresql": "623e22ea8921c9e57a15475b", "snowflake": "6eb49ceb88fcb2297831ca05", "tsql": "6eb49ceb88fcb2297831ca05" } @@ -18574,8 +18574,8 @@ "generic": "e2879cf4770e7f48d2dd704a", "motherduck": "901c5133ed47d4769286e860", "mysql": "e2879cf4770e7f48d2dd704a", -"postgres": "e2879cf4770e7f48d2dd704a", -"postgresql": "e2879cf4770e7f48d2dd704a", +"postgres": "2d4aea8dec5521e258596f3d", +"postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", "tsql": "e2879cf4770e7f48d2dd704a" } @@ -18584,17 +18584,17 @@ "type", { "DuckDB": "1558a2a87f510cca4baaad5b", -"None": "1558a2a87f510cca4baaad5b", -"bigquery": "1558a2a87f510cca4baaad5b", -"databricks": "1558a2a87f510cca4baaad5b", +"None": "5865b96f5136d463181f63e4", +"bigquery": "5865b96f5136d463181f63e4", +"databricks": "5865b96f5136d463181f63e4", "duckdb": "1558a2a87f510cca4baaad5b", -"generic": "1558a2a87f510cca4baaad5b", +"generic": "5865b96f5136d463181f63e4", "motherduck": "901c5133ed47d4769286e860", -"mysql": "1558a2a87f510cca4baaad5b", +"mysql": "5865b96f5136d463181f63e4", "postgres": "1558a2a87f510cca4baaad5b", "postgresql": "1558a2a87f510cca4baaad5b", "snowflake": "67b5874a2c569226c490211f", -"tsql": "1558a2a87f510cca4baaad5b" +"tsql": "5865b96f5136d463181f63e4" } ], [ @@ -18873,17 +18873,17 @@ "type", { "DuckDB": "70fb4728c3adcf6dbe4975d5", -"None": "70fb4728c3adcf6dbe4975d5", -"bigquery": "70fb4728c3adcf6dbe4975d5", -"databricks": "70fb4728c3adcf6dbe4975d5", +"None": "fb4b6f5101f973d16ea8d1f3", +"bigquery": "fb4b6f5101f973d16ea8d1f3", +"databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", -"generic": "70fb4728c3adcf6dbe4975d5", +"generic": "fb4b6f5101f973d16ea8d1f3", "motherduck": "901c5133ed47d4769286e860", -"mysql": "70fb4728c3adcf6dbe4975d5", +"mysql": "fb4b6f5101f973d16ea8d1f3", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", -"tsql": "70fb4728c3adcf6dbe4975d5" +"tsql": "fb4b6f5101f973d16ea8d1f3" } ], [ @@ -18999,8 +18999,8 @@ "generic": "aed0419e7f2ce7c0a8892248", "motherduck": "901c5133ed47d4769286e860", "mysql": "aed0419e7f2ce7c0a8892248", -"postgres": "19e127c23b4598727a2d8ceb", -"postgresql": "19e127c23b4598727a2d8ceb", +"postgres": "111af35848fe500ec2ae54b2", +"postgresql": "111af35848fe500ec2ae54b2", "snowflake": "aed0419e7f2ce7c0a8892248", "tsql": "aed0419e7f2ce7c0a8892248" } @@ -19033,8 +19033,8 @@ "generic": "d08ef9483feed34fc0ee4319", "motherduck": "901c5133ed47d4769286e860", "mysql": "d08ef9483feed34fc0ee4319", -"postgres": "d08ef9483feed34fc0ee4319", -"postgresql": "d08ef9483feed34fc0ee4319", +"postgres": "f7598fbf399afabe1e01a358", +"postgresql": "f7598fbf399afabe1e01a358", "snowflake": "d08ef9483feed34fc0ee4319", "tsql": "d08ef9483feed34fc0ee4319" } @@ -19101,8 +19101,8 @@ "generic": "aed0419e7f2ce7c0a8892248", "motherduck": "901c5133ed47d4769286e860", "mysql": "aed0419e7f2ce7c0a8892248", -"postgres": "19e127c23b4598727a2d8ceb", -"postgresql": "19e127c23b4598727a2d8ceb", +"postgres": "111af35848fe500ec2ae54b2", +"postgresql": "111af35848fe500ec2ae54b2", "snowflake": "aed0419e7f2ce7c0a8892248", "tsql": "aed0419e7f2ce7c0a8892248" } @@ -19128,17 +19128,17 @@ "type", { "DuckDB": "7b2fd741638cbd96bb38d7e0", -"None": "7b2fd741638cbd96bb38d7e0", -"bigquery": "7b2fd741638cbd96bb38d7e0", -"databricks": "4fb572024031cb7631777be1", +"None": "1e765712a4c0f6d3649350c6", +"bigquery": "1e765712a4c0f6d3649350c6", +"databricks": "1e765712a4c0f6d3649350c6", "duckdb": "7b2fd741638cbd96bb38d7e0", -"generic": "7b2fd741638cbd96bb38d7e0", +"generic": "1e765712a4c0f6d3649350c6", "motherduck": "901c5133ed47d4769286e860", -"mysql": "7b2fd741638cbd96bb38d7e0", -"postgres": "7b2fd741638cbd96bb38d7e0", -"postgresql": "7b2fd741638cbd96bb38d7e0", +"mysql": "1e765712a4c0f6d3649350c6", +"postgres": "1e765712a4c0f6d3649350c6", +"postgresql": "1e765712a4c0f6d3649350c6", "snowflake": "7b2fd741638cbd96bb38d7e0", -"tsql": "7b2fd741638cbd96bb38d7e0" +"tsql": "1e765712a4c0f6d3649350c6" } ], [ @@ -19151,7 +19151,7 @@ "duckdb": "a766fea06fc070b352976b0a", "generic": "a766fea06fc070b352976b0a", "motherduck": "901c5133ed47d4769286e860", -"mysql": "e4f0244beaa01667e80482b6", +"mysql": "a766fea06fc070b352976b0a", "postgres": "a766fea06fc070b352976b0a", "postgresql": "a766fea06fc070b352976b0a", "snowflake": "a766fea06fc070b352976b0a", @@ -19181,11 +19181,11 @@ "DuckDB": "d34f84c1b71b9c9c0c20abb0", "None": "d34f84c1b71b9c9c0c20abb0", "bigquery": "d34f84c1b71b9c9c0c20abb0", -"databricks": "c662d9470aa4255dfb493df1", +"databricks": "d34f84c1b71b9c9c0c20abb0", "duckdb": "d34f84c1b71b9c9c0c20abb0", "generic": "d34f84c1b71b9c9c0c20abb0", "motherduck": "901c5133ed47d4769286e860", -"mysql": "c662d9470aa4255dfb493df1", +"mysql": "d34f84c1b71b9c9c0c20abb0", "postgres": "d34f84c1b71b9c9c0c20abb0", "postgresql": "d34f84c1b71b9c9c0c20abb0", "snowflake": "d34f84c1b71b9c9c0c20abb0", @@ -19198,11 +19198,11 @@ "DuckDB": "14cd6e78d75f61b2bd1bbd71", "None": "14cd6e78d75f61b2bd1bbd71", "bigquery": "14cd6e78d75f61b2bd1bbd71", -"databricks": "73504f31156e8e661c1fb26c", +"databricks": "14cd6e78d75f61b2bd1bbd71", "duckdb": "14cd6e78d75f61b2bd1bbd71", "generic": "14cd6e78d75f61b2bd1bbd71", "motherduck": "901c5133ed47d4769286e860", -"mysql": "73504f31156e8e661c1fb26c", +"mysql": "14cd6e78d75f61b2bd1bbd71", "postgres": "14cd6e78d75f61b2bd1bbd71", "postgresql": "14cd6e78d75f61b2bd1bbd71", "snowflake": "14cd6e78d75f61b2bd1bbd71", @@ -19248,7 +19248,7 @@ { "DuckDB": "e5199d22ebbd1383c1e821df", "None": "e5199d22ebbd1383c1e821df", -"bigquery": "e5199d22ebbd1383c1e821df", +"bigquery": "c74425c5ce650ac69c83ca5b", "databricks": "e5199d22ebbd1383c1e821df", "duckdb": "e5199d22ebbd1383c1e821df", "generic": "e5199d22ebbd1383c1e821df", @@ -19271,8 +19271,8 @@ "generic": "336fd1b7a1f7d8105c29280c", "motherduck": "901c5133ed47d4769286e860", "mysql": "336fd1b7a1f7d8105c29280c", -"postgres": "336fd1b7a1f7d8105c29280c", -"postgresql": "336fd1b7a1f7d8105c29280c", +"postgres": "9ea4932505663b43f527adad", +"postgresql": "9ea4932505663b43f527adad", "snowflake": "336fd1b7a1f7d8105c29280c", "tsql": "336fd1b7a1f7d8105c29280c" } @@ -19349,17 +19349,17 @@ "type", { "DuckDB": "314ed506f24e92300a425780", -"None": "314ed506f24e92300a425780", -"bigquery": "314ed506f24e92300a425780", -"databricks": "314ed506f24e92300a425780", +"None": "9a66317fb8d040bf991502c0", +"bigquery": "9bafb08d41fd1c208b5deca4", +"databricks": "9a66317fb8d040bf991502c0", "duckdb": "314ed506f24e92300a425780", -"generic": "314ed506f24e92300a425780", +"generic": "9a66317fb8d040bf991502c0", "motherduck": "901c5133ed47d4769286e860", -"mysql": "314ed506f24e92300a425780", +"mysql": "9a66317fb8d040bf991502c0", "postgres": "314ed506f24e92300a425780", "postgresql": "314ed506f24e92300a425780", "snowflake": "314ed506f24e92300a425780", -"tsql": "314ed506f24e92300a425780" +"tsql": "9a66317fb8d040bf991502c0" } ], [ @@ -19400,17 +19400,17 @@ "type", { "DuckDB": "087a9417cb511ff7bcfd6ccb", -"None": "383f8922f4b6f167d384037d", -"bigquery": "383f8922f4b6f167d384037d", -"databricks": "383f8922f4b6f167d384037d", +"None": "2e8f6dfec4597fb9373329e7", +"bigquery": "2e8f6dfec4597fb9373329e7", +"databricks": "2e8f6dfec4597fb9373329e7", "duckdb": "087a9417cb511ff7bcfd6ccb", -"generic": "383f8922f4b6f167d384037d", +"generic": "2e8f6dfec4597fb9373329e7", "motherduck": "901c5133ed47d4769286e860", -"mysql": "383f8922f4b6f167d384037d", +"mysql": "2e8f6dfec4597fb9373329e7", "postgres": "383f8922f4b6f167d384037d", "postgresql": "383f8922f4b6f167d384037d", "snowflake": "383f8922f4b6f167d384037d", -"tsql": "383f8922f4b6f167d384037d" +"tsql": "2e8f6dfec4597fb9373329e7" } ], [ @@ -19424,8 +19424,8 @@ "generic": "19a1a80ffc06169f32694de8", "motherduck": "901c5133ed47d4769286e860", "mysql": "19a1a80ffc06169f32694de8", -"postgres": "19a1a80ffc06169f32694de8", -"postgresql": "19a1a80ffc06169f32694de8", +"postgres": "ab1f5f46e69284778bd66d58", +"postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", "tsql": "19a1a80ffc06169f32694de8" } @@ -19435,7 +19435,7 @@ { "DuckDB": "b017fb3c48f125e51b6db123", "None": "b017fb3c48f125e51b6db123", -"bigquery": "b017fb3c48f125e51b6db123", +"bigquery": "c74425c5ce650ac69c83ca5b", "databricks": "b017fb3c48f125e51b6db123", "duckdb": "b017fb3c48f125e51b6db123", "generic": "b017fb3c48f125e51b6db123", @@ -19526,8 +19526,8 @@ "generic": "eed0645c1e86fa1a6e35ccaa", "motherduck": "901c5133ed47d4769286e860", "mysql": "eed0645c1e86fa1a6e35ccaa", -"postgres": "eed0645c1e86fa1a6e35ccaa", -"postgresql": "eed0645c1e86fa1a6e35ccaa", +"postgres": "b0477b36767a62145aa9158c", +"postgresql": "b0477b36767a62145aa9158c", "snowflake": "97a20344d811fb77c7690863", "tsql": "eed0645c1e86fa1a6e35ccaa" } @@ -19537,14 +19537,14 @@ { "DuckDB": "3a7ffeb8f81df6ccd57f3689", "None": "3a7ffeb8f81df6ccd57f3689", -"bigquery": "3a7ffeb8f81df6ccd57f3689", +"bigquery": "67d251c042daee0bb2bf7351", "databricks": "3a7ffeb8f81df6ccd57f3689", "duckdb": "3a7ffeb8f81df6ccd57f3689", "generic": "3a7ffeb8f81df6ccd57f3689", "motherduck": "901c5133ed47d4769286e860", "mysql": "3a7ffeb8f81df6ccd57f3689", -"postgres": "3a7ffeb8f81df6ccd57f3689", -"postgresql": "3a7ffeb8f81df6ccd57f3689", +"postgres": "062e51ec86ccd542eb78500d", +"postgresql": "062e51ec86ccd542eb78500d", "snowflake": "3a7ffeb8f81df6ccd57f3689", "tsql": "3a7ffeb8f81df6ccd57f3689" } @@ -19569,16 +19569,16 @@ [ "type", { -"DuckDB": "e69f24e5693aeece1e1a03b8", -"None": "6dd6559eebacf5e375e1cce3", -"bigquery": "3e76afa61378ff263b617c7e", -"databricks": "e4317ab2bb471f29ee3c44be", -"duckdb": "e69f24e5693aeece1e1a03b8", -"generic": "6dd6559eebacf5e375e1cce3", +"DuckDB": "427810237ffbfe3ee6dbe0d4", +"None": "6f2f02617b722e48d3f365c2", +"bigquery": "6f2f02617b722e48d3f365c2", +"databricks": "69ec26c1982add4704b948a6", +"duckdb": "427810237ffbfe3ee6dbe0d4", +"generic": "6f2f02617b722e48d3f365c2", "motherduck": "901c5133ed47d4769286e860", -"mysql": "6dd6559eebacf5e375e1cce3", -"postgres": "6dd6559eebacf5e375e1cce3", -"postgresql": "6dd6559eebacf5e375e1cce3", +"mysql": "6f2f02617b722e48d3f365c2", +"postgres": "6f2f02617b722e48d3f365c2", +"postgresql": "6f2f02617b722e48d3f365c2", "snowflake": "a4325ee9f4d61463eb81badb", "tsql": "6f2f02617b722e48d3f365c2" } @@ -19605,14 +19605,14 @@ { "DuckDB": "fe23594c781e79cbb0c8927a", "None": "fe23594c781e79cbb0c8927a", -"bigquery": "fe23594c781e79cbb0c8927a", +"bigquery": "8a3f094f18fb4047f0febf59", "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", "motherduck": "901c5133ed47d4769286e860", "mysql": "fe23594c781e79cbb0c8927a", -"postgres": "fe23594c781e79cbb0c8927a", -"postgresql": "fe23594c781e79cbb0c8927a", +"postgres": "8a3f094f18fb4047f0febf59", +"postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "fe23594c781e79cbb0c8927a" } @@ -19628,8 +19628,8 @@ "generic": "b8321619b794db091c9e96a1", "motherduck": "901c5133ed47d4769286e860", "mysql": "b8321619b794db091c9e96a1", -"postgres": "b8321619b794db091c9e96a1", -"postgresql": "b8321619b794db091c9e96a1", +"postgres": "bce26544873d6ec9191c9a86", +"postgresql": "bce26544873d6ec9191c9a86", "snowflake": "b8321619b794db091c9e96a1", "tsql": "b8321619b794db091c9e96a1" } @@ -19690,14 +19690,14 @@ { "DuckDB": "7cd31289a5b99b5a5e24492d", "None": "7cd31289a5b99b5a5e24492d", -"bigquery": "7cd31289a5b99b5a5e24492d", +"bigquery": "c7c38fdeaeb7858a44976f1d", "databricks": "7cd31289a5b99b5a5e24492d", "duckdb": "7cd31289a5b99b5a5e24492d", "generic": "7cd31289a5b99b5a5e24492d", "motherduck": "901c5133ed47d4769286e860", "mysql": "7cd31289a5b99b5a5e24492d", -"postgres": "7cd31289a5b99b5a5e24492d", -"postgresql": "7cd31289a5b99b5a5e24492d", +"postgres": "c5bdaf88df7ef3d9ae693106", +"postgresql": "c5bdaf88df7ef3d9ae693106", "snowflake": "7cd31289a5b99b5a5e24492d", "tsql": "7cd31289a5b99b5a5e24492d" } @@ -19775,14 +19775,14 @@ { "DuckDB": "d2c97d221599d038d9875873", "None": "d2c97d221599d038d9875873", -"bigquery": "d2c97d221599d038d9875873", +"bigquery": "55155766823b6a3a5e882fc2", "databricks": "d2c97d221599d038d9875873", "duckdb": "d2c97d221599d038d9875873", "generic": "d2c97d221599d038d9875873", "motherduck": "901c5133ed47d4769286e860", "mysql": "d2c97d221599d038d9875873", -"postgres": "d2c97d221599d038d9875873", -"postgresql": "d2c97d221599d038d9875873", +"postgres": "55155766823b6a3a5e882fc2", +"postgresql": "55155766823b6a3a5e882fc2", "snowflake": "d2c97d221599d038d9875873", "tsql": "d2c97d221599d038d9875873" } @@ -19877,14 +19877,14 @@ { "DuckDB": "f319941cc6ec088f46ecbadb", "None": "f319941cc6ec088f46ecbadb", -"bigquery": "f319941cc6ec088f46ecbadb", +"bigquery": "837468174ef3e5cb4e4b4070", "databricks": "f319941cc6ec088f46ecbadb", "duckdb": "f319941cc6ec088f46ecbadb", "generic": "f319941cc6ec088f46ecbadb", "motherduck": "901c5133ed47d4769286e860", "mysql": "f319941cc6ec088f46ecbadb", -"postgres": "f319941cc6ec088f46ecbadb", -"postgresql": "f319941cc6ec088f46ecbadb", +"postgres": "e664f46cfdd4441496e27023", +"postgresql": "e664f46cfdd4441496e27023", "snowflake": "f319941cc6ec088f46ecbadb", "tsql": "f319941cc6ec088f46ecbadb" } @@ -20014,11 +20014,11 @@ "DuckDB": "607f7c82658f39cce394b124", "None": "607f7c82658f39cce394b124", "bigquery": "607f7c82658f39cce394b124", -"databricks": "085a20ac29ebf425cac9e1d7", +"databricks": "607f7c82658f39cce394b124", "duckdb": "607f7c82658f39cce394b124", "generic": "607f7c82658f39cce394b124", "motherduck": "901c5133ed47d4769286e860", -"mysql": "085a20ac29ebf425cac9e1d7", +"mysql": "607f7c82658f39cce394b124", "postgres": "607f7c82658f39cce394b124", "postgresql": "607f7c82658f39cce394b124", "snowflake": "607f7c82658f39cce394b124", @@ -20030,7 +20030,7 @@ { "DuckDB": "b3d92948d21d50dcbeb5f80b", "None": "b3d92948d21d50dcbeb5f80b", -"bigquery": "b3d92948d21d50dcbeb5f80b", +"bigquery": "e8b4e369659058a182f86446", "databricks": "b3d92948d21d50dcbeb5f80b", "duckdb": "b3d92948d21d50dcbeb5f80b", "generic": "b3d92948d21d50dcbeb5f80b", @@ -20046,17 +20046,17 @@ "type", { "DuckDB": "c50a316d091c821866e4bfba", -"None": "c50a316d091c821866e4bfba", -"bigquery": "c50a316d091c821866e4bfba", -"databricks": "c50a316d091c821866e4bfba", +"None": "e6ffdece2977be55da73b59a", +"bigquery": "e6ffdece2977be55da73b59a", +"databricks": "e6ffdece2977be55da73b59a", "duckdb": "c50a316d091c821866e4bfba", -"generic": "c50a316d091c821866e4bfba", +"generic": "e6ffdece2977be55da73b59a", "motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"mysql": "e6ffdece2977be55da73b59a", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", -"tsql": "c50a316d091c821866e4bfba" +"tsql": "e6ffdece2977be55da73b59a" } ], [ @@ -20098,14 +20098,14 @@ { "DuckDB": "881106a4a0d2de0ff7c56373", "None": "881106a4a0d2de0ff7c56373", -"bigquery": "881106a4a0d2de0ff7c56373", +"bigquery": "893ff554934dfc6f8253597b", "databricks": "881106a4a0d2de0ff7c56373", "duckdb": "881106a4a0d2de0ff7c56373", "generic": "881106a4a0d2de0ff7c56373", "motherduck": "901c5133ed47d4769286e860", "mysql": "881106a4a0d2de0ff7c56373", -"postgres": "881106a4a0d2de0ff7c56373", -"postgresql": "881106a4a0d2de0ff7c56373", +"postgres": "cf167ec395fc843602e3b1e0", +"postgresql": "cf167ec395fc843602e3b1e0", "snowflake": "881106a4a0d2de0ff7c56373", "tsql": "881106a4a0d2de0ff7c56373" } @@ -20114,17 +20114,17 @@ "type", { "DuckDB": "f46cf4b4e131aa122b5b33d6", -"None": "f46cf4b4e131aa122b5b33d6", -"bigquery": "f46cf4b4e131aa122b5b33d6", -"databricks": "f46cf4b4e131aa122b5b33d6", +"None": "f774d5e8feadbf11edaa26b1", +"bigquery": "9bafb08d41fd1c208b5deca4", +"databricks": "f774d5e8feadbf11edaa26b1", "duckdb": "f46cf4b4e131aa122b5b33d6", -"generic": "f46cf4b4e131aa122b5b33d6", +"generic": "f774d5e8feadbf11edaa26b1", "motherduck": "901c5133ed47d4769286e860", -"mysql": "f46cf4b4e131aa122b5b33d6", +"mysql": "f774d5e8feadbf11edaa26b1", "postgres": "f46cf4b4e131aa122b5b33d6", "postgresql": "f46cf4b4e131aa122b5b33d6", "snowflake": "f46cf4b4e131aa122b5b33d6", -"tsql": "f46cf4b4e131aa122b5b33d6" +"tsql": "f774d5e8feadbf11edaa26b1" } ], [ @@ -20239,7 +20239,7 @@ "duckdb": "a01889d0707cf6b364efb5f9", "generic": "94834ae92073b4606184619b", "motherduck": "901c5133ed47d4769286e860", -"mysql": "11d9e4471b50590a198e94a1", +"mysql": "6c03a3450ea4742ee7396e3c", "postgres": "a01889d0707cf6b364efb5f9", "postgresql": "a01889d0707cf6b364efb5f9", "snowflake": "5bc09c1a33ccb4bd2e4075e5", @@ -20257,8 +20257,8 @@ "generic": "19a1a80ffc06169f32694de8", "motherduck": "901c5133ed47d4769286e860", "mysql": "19a1a80ffc06169f32694de8", -"postgres": "19a1a80ffc06169f32694de8", -"postgresql": "19a1a80ffc06169f32694de8", +"postgres": "ab1f5f46e69284778bd66d58", +"postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", "tsql": "19a1a80ffc06169f32694de8" } @@ -20267,15 +20267,15 @@ "type", { "DuckDB": "c43574432afc90bcba6944b2", -"None": "bf431bf304d57dd7b16797df", -"bigquery": "bf431bf304d57dd7b16797df", -"databricks": "bf431bf304d57dd7b16797df", +"None": "ba3896413716dbcd432e62dd", +"bigquery": "ba3896413716dbcd432e62dd", +"databricks": "ba3896413716dbcd432e62dd", "duckdb": "c43574432afc90bcba6944b2", -"generic": "bf431bf304d57dd7b16797df", +"generic": "ba3896413716dbcd432e62dd", "motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", -"postgres": "b462303a5e5a209df5a75e4b", -"postgresql": "b462303a5e5a209df5a75e4b", +"mysql": "ba3896413716dbcd432e62dd", +"postgres": "ba3896413716dbcd432e62dd", +"postgresql": "ba3896413716dbcd432e62dd", "snowflake": "26094c8570001998e247761e", "tsql": "ba3896413716dbcd432e62dd" } @@ -20290,7 +20290,7 @@ "duckdb": "66f7ca02161349d637fa7d4e", "generic": "66f7ca02161349d637fa7d4e", "motherduck": "901c5133ed47d4769286e860", -"mysql": "d90a78129f7050a540984f6f", +"mysql": "66f7ca02161349d637fa7d4e", "postgres": "66f7ca02161349d637fa7d4e", "postgresql": "66f7ca02161349d637fa7d4e", "snowflake": "66f7ca02161349d637fa7d4e", @@ -20325,8 +20325,8 @@ "generic": "e2879cf4770e7f48d2dd704a", "motherduck": "901c5133ed47d4769286e860", "mysql": "e2879cf4770e7f48d2dd704a", -"postgres": "e2879cf4770e7f48d2dd704a", -"postgresql": "e2879cf4770e7f48d2dd704a", +"postgres": "2d4aea8dec5521e258596f3d", +"postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", "tsql": "e2879cf4770e7f48d2dd704a" } @@ -20359,8 +20359,8 @@ "generic": "fe23594c781e79cbb0c8927a", "motherduck": "901c5133ed47d4769286e860", "mysql": "fe23594c781e79cbb0c8927a", -"postgres": "fe23594c781e79cbb0c8927a", -"postgresql": "fe23594c781e79cbb0c8927a", +"postgres": "b0ce99394388f977f67fd1df", +"postgresql": "b0ce99394388f977f67fd1df", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "fe23594c781e79cbb0c8927a" } @@ -20370,14 +20370,14 @@ { "DuckDB": "38b2ec94bf02be40007ce525", "None": "38b2ec94bf02be40007ce525", -"bigquery": "38b2ec94bf02be40007ce525", +"bigquery": "850deea990b52b82b36a9848", "databricks": "38b2ec94bf02be40007ce525", "duckdb": "38b2ec94bf02be40007ce525", "generic": "38b2ec94bf02be40007ce525", "motherduck": "901c5133ed47d4769286e860", "mysql": "38b2ec94bf02be40007ce525", -"postgres": "38b2ec94bf02be40007ce525", -"postgresql": "38b2ec94bf02be40007ce525", +"postgres": "850deea990b52b82b36a9848", +"postgresql": "850deea990b52b82b36a9848", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "38b2ec94bf02be40007ce525" } @@ -20385,16 +20385,16 @@ [ "type", { -"DuckDB": "267fe8533e5d50f2cd3eb176", -"None": "8e0e269b33b63f52e24399e2", -"bigquery": "6a9a0937e74ae8ee83e705e4", -"databricks": "6a9a0937e74ae8ee83e705e4", -"duckdb": "267fe8533e5d50f2cd3eb176", -"generic": "8e0e269b33b63f52e24399e2", +"DuckDB": "627ea26821da68299a37ddb8", +"None": "69ccced5b5ccc61253f2656b", +"bigquery": "69ccced5b5ccc61253f2656b", +"databricks": "69ccced5b5ccc61253f2656b", +"duckdb": "627ea26821da68299a37ddb8", +"generic": "69ccced5b5ccc61253f2656b", "motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", -"postgres": "43767924372e86845ec63bb6", -"postgresql": "43767924372e86845ec63bb6", +"mysql": "69ccced5b5ccc61253f2656b", +"postgres": "69ccced5b5ccc61253f2656b", +"postgresql": "69ccced5b5ccc61253f2656b", "snowflake": "5a7da03d64d9bf65f9984782", "tsql": "69ccced5b5ccc61253f2656b" } @@ -20404,14 +20404,14 @@ { "DuckDB": "4920de3b1203a0d66fc1b3cd", "None": "4920de3b1203a0d66fc1b3cd", -"bigquery": "4920de3b1203a0d66fc1b3cd", +"bigquery": "c74425c5ce650ac69c83ca5b", "databricks": "4920de3b1203a0d66fc1b3cd", "duckdb": "4920de3b1203a0d66fc1b3cd", "generic": "4920de3b1203a0d66fc1b3cd", "motherduck": "901c5133ed47d4769286e860", "mysql": "4920de3b1203a0d66fc1b3cd", -"postgres": "4920de3b1203a0d66fc1b3cd", -"postgresql": "4920de3b1203a0d66fc1b3cd", +"postgres": "8682e002a09d580d930440bf", +"postgresql": "8682e002a09d580d930440bf", "snowflake": "4920de3b1203a0d66fc1b3cd", "tsql": "4920de3b1203a0d66fc1b3cd" } @@ -20455,14 +20455,14 @@ { "DuckDB": "4d0f0db1a550ed1e0da1a043", "None": "4d0f0db1a550ed1e0da1a043", -"bigquery": "4d0f0db1a550ed1e0da1a043", +"bigquery": "837468174ef3e5cb4e4b4070", "databricks": "4d0f0db1a550ed1e0da1a043", "duckdb": "4d0f0db1a550ed1e0da1a043", "generic": "4d0f0db1a550ed1e0da1a043", "motherduck": "901c5133ed47d4769286e860", "mysql": "4d0f0db1a550ed1e0da1a043", -"postgres": "4d0f0db1a550ed1e0da1a043", -"postgresql": "4d0f0db1a550ed1e0da1a043", +"postgres": "e47320d1030c166a05193bc4", +"postgresql": "e47320d1030c166a05193bc4", "snowflake": "4d0f0db1a550ed1e0da1a043", "tsql": "4d0f0db1a550ed1e0da1a043" } @@ -20522,17 +20522,17 @@ "type", { "DuckDB": "a428302e4848ffe877284c84", -"None": "a428302e4848ffe877284c84", -"bigquery": "a428302e4848ffe877284c84", -"databricks": "a428302e4848ffe877284c84", +"None": "c9b810639795a7bfab91b98d", +"bigquery": "c9b810639795a7bfab91b98d", +"databricks": "c9b810639795a7bfab91b98d", "duckdb": "a428302e4848ffe877284c84", -"generic": "a428302e4848ffe877284c84", +"generic": "c9b810639795a7bfab91b98d", "motherduck": "901c5133ed47d4769286e860", -"mysql": "a428302e4848ffe877284c84", +"mysql": "c9b810639795a7bfab91b98d", "postgres": "a428302e4848ffe877284c84", "postgresql": "a428302e4848ffe877284c84", "snowflake": "652ff8b21f638eac8fdc46c5", -"tsql": "a428302e4848ffe877284c84" +"tsql": "c9b810639795a7bfab91b98d" } ], [ @@ -20545,7 +20545,7 @@ "duckdb": "8702414b6372d27d90111c0d", "generic": "a7fcdf83ba95c5481cac4059", "motherduck": "901c5133ed47d4769286e860", -"mysql": "1ebb508c9c012fda785eac26", +"mysql": "6c03a3450ea4742ee7396e3c", "postgres": "8702414b6372d27d90111c0d", "postgresql": "8702414b6372d27d90111c0d", "snowflake": "521d89d8efac986351afc368", @@ -20557,14 +20557,14 @@ { "DuckDB": "38b2ec94bf02be40007ce525", "None": "38b2ec94bf02be40007ce525", -"bigquery": "38b2ec94bf02be40007ce525", +"bigquery": "850deea990b52b82b36a9848", "databricks": "38b2ec94bf02be40007ce525", "duckdb": "38b2ec94bf02be40007ce525", "generic": "38b2ec94bf02be40007ce525", "motherduck": "901c5133ed47d4769286e860", "mysql": "38b2ec94bf02be40007ce525", -"postgres": "38b2ec94bf02be40007ce525", -"postgresql": "38b2ec94bf02be40007ce525", +"postgres": "850deea990b52b82b36a9848", +"postgresql": "850deea990b52b82b36a9848", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "38b2ec94bf02be40007ce525" } @@ -20591,7 +20591,7 @@ { "DuckDB": "62368fc15cefd5807807b209", "None": "62368fc15cefd5807807b209", -"bigquery": "e6ffdece2977be55da73b59a", +"bigquery": "c50a316d091c821866e4bfba", "databricks": "62368fc15cefd5807807b209", "duckdb": "62368fc15cefd5807807b209", "generic": "62368fc15cefd5807807b209", @@ -20625,7 +20625,7 @@ { "DuckDB": "dd061dac05d8a112fce62140", "None": "dd061dac05d8a112fce62140", -"bigquery": "dd061dac05d8a112fce62140", +"bigquery": "c74425c5ce650ac69c83ca5b", "databricks": "dd061dac05d8a112fce62140", "duckdb": "dd061dac05d8a112fce62140", "generic": "dd061dac05d8a112fce62140", @@ -20642,7 +20642,7 @@ { "DuckDB": "d7b4d15e2e3140069c23e6d4", "None": "d7b4d15e2e3140069c23e6d4", -"bigquery": "ba82290eb0d38f9638b8b0dc", +"bigquery": "01d1b160c1768fa1cd4b2e45", "databricks": "9ce61cdfd35b2a8b31faa983", "duckdb": "d7b4d15e2e3140069c23e6d4", "generic": "d7b4d15e2e3140069c23e6d4", @@ -20733,8 +20733,8 @@ "generic": "3d4143f0295abc5ed5a3d8cc", "motherduck": "901c5133ed47d4769286e860", "mysql": "3d4143f0295abc5ed5a3d8cc", -"postgres": "3d4143f0295abc5ed5a3d8cc", -"postgresql": "3d4143f0295abc5ed5a3d8cc", +"postgres": "359568909836c6e577ab7e8c", +"postgresql": "359568909836c6e577ab7e8c", "snowflake": "3d4143f0295abc5ed5a3d8cc", "tsql": "3d4143f0295abc5ed5a3d8cc" } @@ -20914,7 +20914,7 @@ { "DuckDB": "461923a0d34defd0c726d6e5", "None": "461923a0d34defd0c726d6e5", -"bigquery": "b5c4d5044b2a6e70633f0d0a", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "461923a0d34defd0c726d6e5", "duckdb": "461923a0d34defd0c726d6e5", "generic": "461923a0d34defd0c726d6e5", @@ -20954,8 +20954,8 @@ "generic": "a686bb6db17ab079cfc79224", "motherduck": "901c5133ed47d4769286e860", "mysql": "a686bb6db17ab079cfc79224", -"postgres": "a686bb6db17ab079cfc79224", -"postgresql": "a686bb6db17ab079cfc79224", +"postgres": "74a30f6aa9def0b58afc616c", +"postgresql": "74a30f6aa9def0b58afc616c", "snowflake": "1096c3c8687c8e8eb538ff74", "tsql": "a686bb6db17ab079cfc79224" } @@ -20971,8 +20971,8 @@ "generic": "e2879cf4770e7f48d2dd704a", "motherduck": "901c5133ed47d4769286e860", "mysql": "e2879cf4770e7f48d2dd704a", -"postgres": "e2879cf4770e7f48d2dd704a", -"postgresql": "e2879cf4770e7f48d2dd704a", +"postgres": "2d4aea8dec5521e258596f3d", +"postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", "tsql": "e2879cf4770e7f48d2dd704a" } @@ -20982,14 +20982,14 @@ { "DuckDB": "d2c97d221599d038d9875873", "None": "d2c97d221599d038d9875873", -"bigquery": "d2c97d221599d038d9875873", +"bigquery": "55155766823b6a3a5e882fc2", "databricks": "d2c97d221599d038d9875873", "duckdb": "d2c97d221599d038d9875873", "generic": "d2c97d221599d038d9875873", "motherduck": "901c5133ed47d4769286e860", "mysql": "d2c97d221599d038d9875873", -"postgres": "d2c97d221599d038d9875873", -"postgresql": "d2c97d221599d038d9875873", +"postgres": "55155766823b6a3a5e882fc2", +"postgresql": "55155766823b6a3a5e882fc2", "snowflake": "d2c97d221599d038d9875873", "tsql": "d2c97d221599d038d9875873" } @@ -21015,17 +21015,17 @@ "type", { "DuckDB": "a428302e4848ffe877284c84", -"None": "a428302e4848ffe877284c84", -"bigquery": "a428302e4848ffe877284c84", -"databricks": "a428302e4848ffe877284c84", +"None": "c9b810639795a7bfab91b98d", +"bigquery": "c9b810639795a7bfab91b98d", +"databricks": "c9b810639795a7bfab91b98d", "duckdb": "a428302e4848ffe877284c84", -"generic": "a428302e4848ffe877284c84", +"generic": "c9b810639795a7bfab91b98d", "motherduck": "901c5133ed47d4769286e860", -"mysql": "a428302e4848ffe877284c84", +"mysql": "c9b810639795a7bfab91b98d", "postgres": "a428302e4848ffe877284c84", "postgresql": "a428302e4848ffe877284c84", "snowflake": "652ff8b21f638eac8fdc46c5", -"tsql": "a428302e4848ffe877284c84" +"tsql": "c9b810639795a7bfab91b98d" } ], [ @@ -21056,8 +21056,8 @@ "generic": "19e127c23b4598727a2d8ceb", "motherduck": "901c5133ed47d4769286e860", "mysql": "aed0419e7f2ce7c0a8892248", -"postgres": "19e127c23b4598727a2d8ceb", -"postgresql": "19e127c23b4598727a2d8ceb", +"postgres": "111af35848fe500ec2ae54b2", +"postgresql": "111af35848fe500ec2ae54b2", "snowflake": "aed0419e7f2ce7c0a8892248", "tsql": "19e127c23b4598727a2d8ceb" } @@ -21090,8 +21090,8 @@ "generic": "bce60580ef3a1dc4ce569fff", "motherduck": "901c5133ed47d4769286e860", "mysql": "bce60580ef3a1dc4ce569fff", -"postgres": "bce60580ef3a1dc4ce569fff", -"postgresql": "bce60580ef3a1dc4ce569fff", +"postgres": "eee236ea1427bdddbaf088ac", +"postgresql": "eee236ea1427bdddbaf088ac", "snowflake": "bce60580ef3a1dc4ce569fff", "tsql": "bce60580ef3a1dc4ce569fff" } @@ -21133,14 +21133,14 @@ [ "type", { -"DuckDB": "c6654395c62d21b415299f72", +"DuckDB": "0712c33a016f7dd6a37ee011", "None": "c6654395c62d21b415299f72", "bigquery": "0712c33a016f7dd6a37ee011", "databricks": "c6654395c62d21b415299f72", -"duckdb": "c6654395c62d21b415299f72", +"duckdb": "0712c33a016f7dd6a37ee011", "generic": "c6654395c62d21b415299f72", "motherduck": "901c5133ed47d4769286e860", -"mysql": "2978a8fe925c7f93c6e9fcfb", +"mysql": "c6654395c62d21b415299f72", "postgres": "c6654395c62d21b415299f72", "postgresql": "c6654395c62d21b415299f72", "snowflake": "0712c33a016f7dd6a37ee011", @@ -21260,8 +21260,8 @@ "generic": "18036ada4276b0930d870788", "motherduck": "901c5133ed47d4769286e860", "mysql": "18036ada4276b0930d870788", -"postgres": "18036ada4276b0930d870788", -"postgresql": "18036ada4276b0930d870788", +"postgres": "899d81b12a6c3aed6cf9dd53", +"postgresql": "899d81b12a6c3aed6cf9dd53", "snowflake": "18036ada4276b0930d870788", "tsql": "18036ada4276b0930d870788" } @@ -21441,14 +21441,14 @@ { "DuckDB": "7cd31289a5b99b5a5e24492d", "None": "7cd31289a5b99b5a5e24492d", -"bigquery": "7cd31289a5b99b5a5e24492d", +"bigquery": "c7c38fdeaeb7858a44976f1d", "databricks": "7cd31289a5b99b5a5e24492d", "duckdb": "7cd31289a5b99b5a5e24492d", "generic": "7cd31289a5b99b5a5e24492d", "motherduck": "901c5133ed47d4769286e860", "mysql": "7cd31289a5b99b5a5e24492d", -"postgres": "7cd31289a5b99b5a5e24492d", -"postgresql": "7cd31289a5b99b5a5e24492d", +"postgres": "c5bdaf88df7ef3d9ae693106", +"postgresql": "c5bdaf88df7ef3d9ae693106", "snowflake": "7cd31289a5b99b5a5e24492d", "tsql": "7cd31289a5b99b5a5e24492d" } @@ -21458,7 +21458,7 @@ { "DuckDB": "cbd04e906c540e91d7c8ea88", "None": "cbd04e906c540e91d7c8ea88", -"bigquery": "b5c4d5044b2a6e70633f0d0a", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "cbd04e906c540e91d7c8ea88", "duckdb": "cbd04e906c540e91d7c8ea88", "generic": "cbd04e906c540e91d7c8ea88", @@ -21611,14 +21611,14 @@ { "DuckDB": "fe23594c781e79cbb0c8927a", "None": "fe23594c781e79cbb0c8927a", -"bigquery": "fe23594c781e79cbb0c8927a", +"bigquery": "8a3f094f18fb4047f0febf59", "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", "motherduck": "901c5133ed47d4769286e860", "mysql": "fe23594c781e79cbb0c8927a", -"postgres": "fe23594c781e79cbb0c8927a", -"postgresql": "fe23594c781e79cbb0c8927a", +"postgres": "8a3f094f18fb4047f0febf59", +"postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "fe23594c781e79cbb0c8927a" } @@ -21628,7 +21628,7 @@ { "DuckDB": "18db5afe2c1e80d1ee652b54", "None": "18db5afe2c1e80d1ee652b54", -"bigquery": "e1c36a5cdd2427ff2b089ac9", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "18db5afe2c1e80d1ee652b54", "duckdb": "18db5afe2c1e80d1ee652b54", "generic": "18db5afe2c1e80d1ee652b54", @@ -21650,7 +21650,7 @@ "duckdb": "219314eaf94cabfe1c7e559c", "generic": "4aab4bdd20039d5e16058b1b", "motherduck": "901c5133ed47d4769286e860", -"mysql": "5524945ebcb15c363d317b3c", +"mysql": "6c03a3450ea4742ee7396e3c", "postgres": "219314eaf94cabfe1c7e559c", "postgresql": "219314eaf94cabfe1c7e559c", "snowflake": "b767a0334d4c0090a22665f5", @@ -21702,8 +21702,8 @@ "generic": "669084d1ea704ae5b8ee05c2", "motherduck": "901c5133ed47d4769286e860", "mysql": "669084d1ea704ae5b8ee05c2", -"postgres": "669084d1ea704ae5b8ee05c2", -"postgresql": "669084d1ea704ae5b8ee05c2", +"postgres": "93c1b5b5960e26479baf88d0", +"postgresql": "93c1b5b5960e26479baf88d0", "snowflake": "669084d1ea704ae5b8ee05c2", "tsql": "669084d1ea704ae5b8ee05c2" } @@ -21747,7 +21747,7 @@ { "DuckDB": "8203bd5901a8ec71ff7c103e", "None": "8203bd5901a8ec71ff7c103e", -"bigquery": "2a0918be53051fe629d9ff8f", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "8203bd5901a8ec71ff7c103e", "duckdb": "8203bd5901a8ec71ff7c103e", "generic": "8203bd5901a8ec71ff7c103e", @@ -21764,14 +21764,14 @@ { "DuckDB": "aab0389e2b5470136660bf8a", "None": "aab0389e2b5470136660bf8a", -"bigquery": "c9b810639795a7bfab91b98d", +"bigquery": "a428302e4848ffe877284c84", "databricks": "aab0389e2b5470136660bf8a", "duckdb": "aab0389e2b5470136660bf8a", "generic": "aab0389e2b5470136660bf8a", "motherduck": "901c5133ed47d4769286e860", "mysql": "aab0389e2b5470136660bf8a", -"postgres": "aab0389e2b5470136660bf8a", -"postgresql": "aab0389e2b5470136660bf8a", +"postgres": "f16105cbc61d547e6c08efc8", +"postgresql": "f16105cbc61d547e6c08efc8", "snowflake": "97a20344d811fb77c7690863", "tsql": "aab0389e2b5470136660bf8a" } @@ -21781,7 +21781,7 @@ { "DuckDB": "6199046833cb1b5de6e50c1f", "None": "6199046833cb1b5de6e50c1f", -"bigquery": "6199046833cb1b5de6e50c1f", +"bigquery": "c74425c5ce650ac69c83ca5b", "databricks": "6199046833cb1b5de6e50c1f", "duckdb": "6199046833cb1b5de6e50c1f", "generic": "6199046833cb1b5de6e50c1f", @@ -21798,7 +21798,7 @@ { "DuckDB": "f1158467f7ab7b47d7e993d0", "None": "f1158467f7ab7b47d7e993d0", -"bigquery": "f1158467f7ab7b47d7e993d0", +"bigquery": "c74425c5ce650ac69c83ca5b", "databricks": "f1158467f7ab7b47d7e993d0", "duckdb": "f1158467f7ab7b47d7e993d0", "generic": "f1158467f7ab7b47d7e993d0", @@ -21814,17 +21814,17 @@ "type", { "DuckDB": "23f70cb2333740d79361d52e", -"None": "23f70cb2333740d79361d52e", -"bigquery": "816384049477221b0a4a5b89", -"databricks": "23f70cb2333740d79361d52e", +"None": "a33d60d9b009878ab623c1d8", +"bigquery": "bb92c01c64d1a2d32abb67b5", +"databricks": "a33d60d9b009878ab623c1d8", "duckdb": "23f70cb2333740d79361d52e", -"generic": "23f70cb2333740d79361d52e", +"generic": "a33d60d9b009878ab623c1d8", "motherduck": "901c5133ed47d4769286e860", -"mysql": "23f70cb2333740d79361d52e", -"postgres": "23f70cb2333740d79361d52e", -"postgresql": "23f70cb2333740d79361d52e", +"mysql": "a33d60d9b009878ab623c1d8", +"postgres": "f276e907770196606577c932", +"postgresql": "f276e907770196606577c932", "snowflake": "23f70cb2333740d79361d52e", -"tsql": "23f70cb2333740d79361d52e" +"tsql": "a33d60d9b009878ab623c1d8" } ], [ @@ -21855,8 +21855,8 @@ "generic": "669084d1ea704ae5b8ee05c2", "motherduck": "901c5133ed47d4769286e860", "mysql": "669084d1ea704ae5b8ee05c2", -"postgres": "669084d1ea704ae5b8ee05c2", -"postgresql": "669084d1ea704ae5b8ee05c2", +"postgres": "93c1b5b5960e26479baf88d0", +"postgresql": "93c1b5b5960e26479baf88d0", "snowflake": "669084d1ea704ae5b8ee05c2", "tsql": "669084d1ea704ae5b8ee05c2" } @@ -21883,7 +21883,7 @@ { "DuckDB": "623dbde002b57284f477b0b5", "None": "623dbde002b57284f477b0b5", -"bigquery": "623dbde002b57284f477b0b5", +"bigquery": "c74425c5ce650ac69c83ca5b", "databricks": "623dbde002b57284f477b0b5", "duckdb": "623dbde002b57284f477b0b5", "generic": "623dbde002b57284f477b0b5", @@ -21917,7 +21917,7 @@ { "DuckDB": "558da38ec4d9d5d6c72f60cf", "None": "558da38ec4d9d5d6c72f60cf", -"bigquery": "e6ffdece2977be55da73b59a", +"bigquery": "c50a316d091c821866e4bfba", "databricks": "558da38ec4d9d5d6c72f60cf", "duckdb": "558da38ec4d9d5d6c72f60cf", "generic": "558da38ec4d9d5d6c72f60cf", @@ -21933,17 +21933,17 @@ "type", { "DuckDB": "363159f251e264604338e690", -"None": "363159f251e264604338e690", -"bigquery": "363159f251e264604338e690", -"databricks": "363159f251e264604338e690", +"None": "9a63ad6168418af23d9b6fd1", +"bigquery": "8b8d5f75ba6dec68b4cebc6b", +"databricks": "9a63ad6168418af23d9b6fd1", "duckdb": "363159f251e264604338e690", -"generic": "363159f251e264604338e690", +"generic": "9a63ad6168418af23d9b6fd1", "motherduck": "901c5133ed47d4769286e860", -"mysql": "363159f251e264604338e690", -"postgres": "363159f251e264604338e690", -"postgresql": "363159f251e264604338e690", +"mysql": "9a63ad6168418af23d9b6fd1", +"postgres": "f276e907770196606577c932", +"postgresql": "f276e907770196606577c932", "snowflake": "363159f251e264604338e690", -"tsql": "363159f251e264604338e690" +"tsql": "9a63ad6168418af23d9b6fd1" } ], [ @@ -21952,11 +21952,11 @@ "DuckDB": "b6200c25bc3749f5724d237c", "None": "b6200c25bc3749f5724d237c", "bigquery": "b6200c25bc3749f5724d237c", -"databricks": "6b1db1e2f94caeb40dbea19b", +"databricks": "b6200c25bc3749f5724d237c", "duckdb": "b6200c25bc3749f5724d237c", "generic": "b6200c25bc3749f5724d237c", "motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"mysql": "b6200c25bc3749f5724d237c", "postgres": "b6200c25bc3749f5724d237c", "postgresql": "b6200c25bc3749f5724d237c", "snowflake": "b6200c25bc3749f5724d237c", @@ -22001,32 +22001,32 @@ "type", { "DuckDB": "129cd0245509c52e0d7ac815", -"None": "8d1119d9033bc544ced02739", -"bigquery": "ef3ab21b1ec5acc43562f6e2", -"databricks": "5b41dbff68938ce2e8a5c35d", +"None": "1071df52a9e8350ac0c2cd33", +"bigquery": "1071df52a9e8350ac0c2cd33", +"databricks": "1071df52a9e8350ac0c2cd33", "duckdb": "129cd0245509c52e0d7ac815", -"generic": "8d1119d9033bc544ced02739", +"generic": "1071df52a9e8350ac0c2cd33", "motherduck": "901c5133ed47d4769286e860", -"mysql": "b9f1a78bcda3bc968346cc10", -"postgres": "129cd0245509c52e0d7ac815", -"postgresql": "129cd0245509c52e0d7ac815", +"mysql": "1071df52a9e8350ac0c2cd33", +"postgres": "1071df52a9e8350ac0c2cd33", +"postgresql": "1071df52a9e8350ac0c2cd33", "snowflake": "24b68111659c8d395da7df6a", -"tsql": "e42dc1c3607e8e3b3c865c3f" +"tsql": "1071df52a9e8350ac0c2cd33" } ], [ "type", { -"DuckDB": "60bd7daa04bed898e52118ab", -"None": "070c0836fb3402c924b00b0e", +"DuckDB": "3d2b3b1097973d173f205074", +"None": "3aac0f59706540c7a59cbce4", "bigquery": "3aac0f59706540c7a59cbce4", -"databricks": "070c0836fb3402c924b00b0e", -"duckdb": "60bd7daa04bed898e52118ab", -"generic": "070c0836fb3402c924b00b0e", +"databricks": "3aac0f59706540c7a59cbce4", +"duckdb": "3d2b3b1097973d173f205074", +"generic": "3aac0f59706540c7a59cbce4", "motherduck": "901c5133ed47d4769286e860", "mysql": "6c03a3450ea4742ee7396e3c", -"postgres": "60bd7daa04bed898e52118ab", -"postgresql": "60bd7daa04bed898e52118ab", +"postgres": "3d2b3b1097973d173f205074", +"postgresql": "3d2b3b1097973d173f205074", "snowflake": "0a5fcaec13e26e3caacd5345", "tsql": "33f30462ac4f41af4b28bd1a" } @@ -22070,14 +22070,14 @@ { "DuckDB": "60b39a68a18438603dd5496d", "None": "60b39a68a18438603dd5496d", -"bigquery": "60b39a68a18438603dd5496d", +"bigquery": "893ff554934dfc6f8253597b", "databricks": "60b39a68a18438603dd5496d", "duckdb": "60b39a68a18438603dd5496d", "generic": "60b39a68a18438603dd5496d", "motherduck": "901c5133ed47d4769286e860", "mysql": "60b39a68a18438603dd5496d", -"postgres": "60b39a68a18438603dd5496d", -"postgresql": "60b39a68a18438603dd5496d", +"postgres": "7a007e6ff9e9465c9d7fadea", +"postgresql": "7a007e6ff9e9465c9d7fadea", "snowflake": "60b39a68a18438603dd5496d", "tsql": "60b39a68a18438603dd5496d" } @@ -22122,11 +22122,11 @@ "DuckDB": "9c2cff8be7225cf79883ca65", "None": "9c2cff8be7225cf79883ca65", "bigquery": "9c2cff8be7225cf79883ca65", -"databricks": "901ac89f8d9ec18bd3a61e4b", +"databricks": "9c2cff8be7225cf79883ca65", "duckdb": "9c2cff8be7225cf79883ca65", "generic": "9c2cff8be7225cf79883ca65", "motherduck": "901c5133ed47d4769286e860", -"mysql": "901ac89f8d9ec18bd3a61e4b", +"mysql": "9c2cff8be7225cf79883ca65", "postgres": "9c2cff8be7225cf79883ca65", "postgresql": "9c2cff8be7225cf79883ca65", "snowflake": "9c2cff8be7225cf79883ca65", @@ -22161,8 +22161,8 @@ "generic": "4b356542044f1267fcfc487a", "motherduck": "901c5133ed47d4769286e860", "mysql": "4b356542044f1267fcfc487a", -"postgres": "4b356542044f1267fcfc487a", -"postgresql": "4b356542044f1267fcfc487a", +"postgres": "a5656d70e208254d1d3e9b4f", +"postgresql": "a5656d70e208254d1d3e9b4f", "snowflake": "4b356542044f1267fcfc487a", "tsql": "4b356542044f1267fcfc487a" } @@ -22206,14 +22206,14 @@ { "DuckDB": "e46d4be44ab9e500e9190d26", "None": "e46d4be44ab9e500e9190d26", -"bigquery": "e6ffdece2977be55da73b59a", +"bigquery": "c50a316d091c821866e4bfba", "databricks": "e46d4be44ab9e500e9190d26", "duckdb": "e46d4be44ab9e500e9190d26", "generic": "e46d4be44ab9e500e9190d26", "motherduck": "901c5133ed47d4769286e860", "mysql": "e46d4be44ab9e500e9190d26", -"postgres": "e46d4be44ab9e500e9190d26", -"postgresql": "e46d4be44ab9e500e9190d26", +"postgres": "251aba3fef503ce64556c701", +"postgresql": "251aba3fef503ce64556c701", "snowflake": "e46d4be44ab9e500e9190d26", "tsql": "e46d4be44ab9e500e9190d26" } @@ -22222,17 +22222,17 @@ "type", { "DuckDB": "dcd0570bcd1ca6e7cbe9b206", -"None": "dcd0570bcd1ca6e7cbe9b206", -"bigquery": "3b4c01f94cca3bf195a3fd00", -"databricks": "526d6b30a5f27d05c0a26aa0", +"None": "85a8004226c2b77c6b697bab", +"bigquery": "85a8004226c2b77c6b697bab", +"databricks": "85a8004226c2b77c6b697bab", "duckdb": "dcd0570bcd1ca6e7cbe9b206", -"generic": "dcd0570bcd1ca6e7cbe9b206", +"generic": "85a8004226c2b77c6b697bab", "motherduck": "901c5133ed47d4769286e860", -"mysql": "a5a929e25b7d0dae21e9bb65", -"postgres": "dcd0570bcd1ca6e7cbe9b206", -"postgresql": "dcd0570bcd1ca6e7cbe9b206", +"mysql": "85a8004226c2b77c6b697bab", +"postgres": "85a8004226c2b77c6b697bab", +"postgresql": "85a8004226c2b77c6b697bab", "snowflake": "339dae99bd73c126e700c594", -"tsql": "dcd0570bcd1ca6e7cbe9b206" +"tsql": "85a8004226c2b77c6b697bab" } ], [ @@ -22313,7 +22313,7 @@ "duckdb": "630c8b82ec44c7208a6df405", "generic": "fa5c71c0d2354f76510bf3e7", "motherduck": "901c5133ed47d4769286e860", -"mysql": "58f6dfd7cabdc84b33dc2a96", +"mysql": "6c03a3450ea4742ee7396e3c", "postgres": "630c8b82ec44c7208a6df405", "postgresql": "630c8b82ec44c7208a6df405", "snowflake": "8efe0c6c8c4444b8fe68129b", @@ -22325,14 +22325,14 @@ { "DuckDB": "aab0389e2b5470136660bf8a", "None": "aab0389e2b5470136660bf8a", -"bigquery": "c9b810639795a7bfab91b98d", +"bigquery": "a428302e4848ffe877284c84", "databricks": "aab0389e2b5470136660bf8a", "duckdb": "aab0389e2b5470136660bf8a", "generic": "aab0389e2b5470136660bf8a", "motherduck": "901c5133ed47d4769286e860", "mysql": "aab0389e2b5470136660bf8a", -"postgres": "aab0389e2b5470136660bf8a", -"postgresql": "aab0389e2b5470136660bf8a", +"postgres": "f16105cbc61d547e6c08efc8", +"postgresql": "f16105cbc61d547e6c08efc8", "snowflake": "97a20344d811fb77c7690863", "tsql": "aab0389e2b5470136660bf8a" } @@ -22359,14 +22359,14 @@ { "DuckDB": "dbd808c57538b7d212ed9a7c", "None": "dbd808c57538b7d212ed9a7c", -"bigquery": "dbd808c57538b7d212ed9a7c", +"bigquery": "c74425c5ce650ac69c83ca5b", "databricks": "dbd808c57538b7d212ed9a7c", "duckdb": "dbd808c57538b7d212ed9a7c", "generic": "dbd808c57538b7d212ed9a7c", "motherduck": "901c5133ed47d4769286e860", "mysql": "dbd808c57538b7d212ed9a7c", -"postgres": "dbd808c57538b7d212ed9a7c", -"postgresql": "dbd808c57538b7d212ed9a7c", +"postgres": "932963c69958d2a639768fd3", +"postgresql": "932963c69958d2a639768fd3", "snowflake": "dbd808c57538b7d212ed9a7c", "tsql": "dbd808c57538b7d212ed9a7c" } @@ -22427,14 +22427,14 @@ { "DuckDB": "38b2ec94bf02be40007ce525", "None": "38b2ec94bf02be40007ce525", -"bigquery": "38b2ec94bf02be40007ce525", +"bigquery": "850deea990b52b82b36a9848", "databricks": "38b2ec94bf02be40007ce525", "duckdb": "38b2ec94bf02be40007ce525", "generic": "38b2ec94bf02be40007ce525", "motherduck": "901c5133ed47d4769286e860", "mysql": "38b2ec94bf02be40007ce525", -"postgres": "38b2ec94bf02be40007ce525", -"postgresql": "38b2ec94bf02be40007ce525", +"postgres": "850deea990b52b82b36a9848", +"postgresql": "850deea990b52b82b36a9848", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "38b2ec94bf02be40007ce525" } @@ -22461,14 +22461,14 @@ { "DuckDB": "38b2ec94bf02be40007ce525", "None": "38b2ec94bf02be40007ce525", -"bigquery": "38b2ec94bf02be40007ce525", +"bigquery": "850deea990b52b82b36a9848", "databricks": "38b2ec94bf02be40007ce525", "duckdb": "38b2ec94bf02be40007ce525", "generic": "38b2ec94bf02be40007ce525", "motherduck": "901c5133ed47d4769286e860", "mysql": "38b2ec94bf02be40007ce525", -"postgres": "38b2ec94bf02be40007ce525", -"postgresql": "38b2ec94bf02be40007ce525", +"postgres": "850deea990b52b82b36a9848", +"postgresql": "850deea990b52b82b36a9848", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "38b2ec94bf02be40007ce525" } @@ -22478,14 +22478,14 @@ { "DuckDB": "7cd31289a5b99b5a5e24492d", "None": "7cd31289a5b99b5a5e24492d", -"bigquery": "7cd31289a5b99b5a5e24492d", +"bigquery": "c7c38fdeaeb7858a44976f1d", "databricks": "7cd31289a5b99b5a5e24492d", "duckdb": "7cd31289a5b99b5a5e24492d", "generic": "7cd31289a5b99b5a5e24492d", "motherduck": "901c5133ed47d4769286e860", "mysql": "7cd31289a5b99b5a5e24492d", -"postgres": "7cd31289a5b99b5a5e24492d", -"postgresql": "7cd31289a5b99b5a5e24492d", +"postgres": "c5bdaf88df7ef3d9ae693106", +"postgresql": "c5bdaf88df7ef3d9ae693106", "snowflake": "7cd31289a5b99b5a5e24492d", "tsql": "7cd31289a5b99b5a5e24492d" } @@ -22529,14 +22529,14 @@ { "DuckDB": "b561884132dbe7e42b719110", "None": "b561884132dbe7e42b719110", -"bigquery": "b1b23029bedaa16e2a6f8516", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "b561884132dbe7e42b719110", "duckdb": "b561884132dbe7e42b719110", "generic": "b561884132dbe7e42b719110", "motherduck": "901c5133ed47d4769286e860", "mysql": "b561884132dbe7e42b719110", -"postgres": "b561884132dbe7e42b719110", -"postgresql": "b561884132dbe7e42b719110", +"postgres": "5aa51d84ac5e6c7a9306f348", +"postgresql": "5aa51d84ac5e6c7a9306f348", "snowflake": "b561884132dbe7e42b719110", "tsql": "b561884132dbe7e42b719110" } @@ -22552,8 +22552,8 @@ "generic": "de89632e277dbc8c97115419", "motherduck": "901c5133ed47d4769286e860", "mysql": "de89632e277dbc8c97115419", -"postgres": "de89632e277dbc8c97115419", -"postgresql": "de89632e277dbc8c97115419", +"postgres": "5abf4ec799a2ec32fb10da7e", +"postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", "tsql": "de89632e277dbc8c97115419" } @@ -22654,8 +22654,8 @@ "generic": "177c462b3a0270e894c8111c", "motherduck": "901c5133ed47d4769286e860", "mysql": "177c462b3a0270e894c8111c", -"postgres": "177c462b3a0270e894c8111c", -"postgresql": "177c462b3a0270e894c8111c", +"postgres": "2625125fd986796ed33a61b7", +"postgresql": "2625125fd986796ed33a61b7", "snowflake": "177c462b3a0270e894c8111c", "tsql": "177c462b3a0270e894c8111c" } @@ -22682,14 +22682,14 @@ { "DuckDB": "08736485547ba339930697e9", "None": "08736485547ba339930697e9", -"bigquery": "08736485547ba339930697e9", +"bigquery": "837468174ef3e5cb4e4b4070", "databricks": "08736485547ba339930697e9", "duckdb": "08736485547ba339930697e9", "generic": "08736485547ba339930697e9", "motherduck": "901c5133ed47d4769286e860", "mysql": "08736485547ba339930697e9", -"postgres": "08736485547ba339930697e9", -"postgresql": "08736485547ba339930697e9", +"postgres": "eecd452b4f5f3f4133d0bd0a", +"postgresql": "eecd452b4f5f3f4133d0bd0a", "snowflake": "08736485547ba339930697e9", "tsql": "08736485547ba339930697e9" } @@ -22722,8 +22722,8 @@ "generic": "3e611643f0beab4953c4e09b", "motherduck": "901c5133ed47d4769286e860", "mysql": "3e611643f0beab4953c4e09b", -"postgres": "3e611643f0beab4953c4e09b", -"postgresql": "3e611643f0beab4953c4e09b", +"postgres": "cae477ce59c72869f37a9a40", +"postgresql": "cae477ce59c72869f37a9a40", "snowflake": "3e611643f0beab4953c4e09b", "tsql": "3e611643f0beab4953c4e09b" } @@ -22756,8 +22756,8 @@ "generic": "669084d1ea704ae5b8ee05c2", "motherduck": "901c5133ed47d4769286e860", "mysql": "669084d1ea704ae5b8ee05c2", -"postgres": "669084d1ea704ae5b8ee05c2", -"postgresql": "669084d1ea704ae5b8ee05c2", +"postgres": "93c1b5b5960e26479baf88d0", +"postgresql": "93c1b5b5960e26479baf88d0", "snowflake": "669084d1ea704ae5b8ee05c2", "tsql": "669084d1ea704ae5b8ee05c2" } @@ -22850,18 +22850,18 @@ [ "type", { -"DuckDB": "944971a819b2498d3ecdc71d", -"None": "90882506184f4f707c46fdea", +"DuckDB": "225fc4f7b28377ba911e4a47", +"None": "a05c10798b5cb323b7dc3ed4", "bigquery": "f1cb74b07e7c8c7243c9034b", -"databricks": "37decbb811ce5abeffb05b4e", -"duckdb": "944971a819b2498d3ecdc71d", -"generic": "90882506184f4f707c46fdea", +"databricks": "a05c10798b5cb323b7dc3ed4", +"duckdb": "225fc4f7b28377ba911e4a47", +"generic": "a05c10798b5cb323b7dc3ed4", "motherduck": "901c5133ed47d4769286e860", -"mysql": "90882506184f4f707c46fdea", -"postgres": "61f2d73857de8729b66f99d2", -"postgresql": "61f2d73857de8729b66f99d2", +"mysql": "a05c10798b5cb323b7dc3ed4", +"postgres": "4e87e4788e5ac719db00d4e4", +"postgresql": "4e87e4788e5ac719db00d4e4", "snowflake": "225fc4f7b28377ba911e4a47", -"tsql": "5fb4ec49639b38cdf3ccfc38" +"tsql": "1a635a1b4dbecf67ace34158" } ], [ @@ -22869,14 +22869,14 @@ { "DuckDB": "dbf9ebbc7016f5ac5df2817e", "None": "dbf9ebbc7016f5ac5df2817e", -"bigquery": "dbf9ebbc7016f5ac5df2817e", +"bigquery": "837468174ef3e5cb4e4b4070", "databricks": "dbf9ebbc7016f5ac5df2817e", "duckdb": "dbf9ebbc7016f5ac5df2817e", "generic": "dbf9ebbc7016f5ac5df2817e", "motherduck": "901c5133ed47d4769286e860", "mysql": "dbf9ebbc7016f5ac5df2817e", -"postgres": "dbf9ebbc7016f5ac5df2817e", -"postgresql": "dbf9ebbc7016f5ac5df2817e", +"postgres": "4cccf86e62d627a7a8b57726", +"postgresql": "4cccf86e62d627a7a8b57726", "snowflake": "dbf9ebbc7016f5ac5df2817e", "tsql": "dbf9ebbc7016f5ac5df2817e" } @@ -22903,14 +22903,14 @@ { "DuckDB": "d2c97d221599d038d9875873", "None": "d2c97d221599d038d9875873", -"bigquery": "d2c97d221599d038d9875873", +"bigquery": "55155766823b6a3a5e882fc2", "databricks": "d2c97d221599d038d9875873", "duckdb": "d2c97d221599d038d9875873", "generic": "d2c97d221599d038d9875873", "motherduck": "901c5133ed47d4769286e860", "mysql": "d2c97d221599d038d9875873", -"postgres": "d2c97d221599d038d9875873", -"postgresql": "d2c97d221599d038d9875873", +"postgres": "55155766823b6a3a5e882fc2", +"postgresql": "55155766823b6a3a5e882fc2", "snowflake": "d2c97d221599d038d9875873", "tsql": "d2c97d221599d038d9875873" } @@ -22959,7 +22959,7 @@ "duckdb": "5620e893a908c6743b863bf6", "generic": "5620e893a908c6743b863bf6", "motherduck": "901c5133ed47d4769286e860", -"mysql": "4281b54de06f589b3507035a", +"mysql": "5620e893a908c6743b863bf6", "postgres": "5620e893a908c6743b863bf6", "postgresql": "5620e893a908c6743b863bf6", "snowflake": "5620e893a908c6743b863bf6", @@ -22994,8 +22994,8 @@ "generic": "d16d75bc8832411932952499", "motherduck": "901c5133ed47d4769286e860", "mysql": "d16d75bc8832411932952499", -"postgres": "d16d75bc8832411932952499", -"postgresql": "d16d75bc8832411932952499", +"postgres": "9372facf3130ca098b0fd00a", +"postgresql": "9372facf3130ca098b0fd00a", "snowflake": "d16d75bc8832411932952499", "tsql": "d16d75bc8832411932952499" } @@ -23004,17 +23004,17 @@ "type", { "DuckDB": "5abf4ec799a2ec32fb10da7e", -"None": "5abf4ec799a2ec32fb10da7e", +"None": "de89632e277dbc8c97115419", "bigquery": "5abf4ec799a2ec32fb10da7e", -"databricks": "5abf4ec799a2ec32fb10da7e", +"databricks": "de89632e277dbc8c97115419", "duckdb": "5abf4ec799a2ec32fb10da7e", -"generic": "5abf4ec799a2ec32fb10da7e", +"generic": "de89632e277dbc8c97115419", "motherduck": "901c5133ed47d4769286e860", -"mysql": "5abf4ec799a2ec32fb10da7e", +"mysql": "de89632e277dbc8c97115419", "postgres": "5abf4ec799a2ec32fb10da7e", "postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "a5656d70e208254d1d3e9b4f", -"tsql": "5abf4ec799a2ec32fb10da7e" +"tsql": "de89632e277dbc8c97115419" } ], [ @@ -23045,8 +23045,8 @@ "generic": "a686bb6db17ab079cfc79224", "motherduck": "901c5133ed47d4769286e860", "mysql": "a686bb6db17ab079cfc79224", -"postgres": "a686bb6db17ab079cfc79224", -"postgresql": "a686bb6db17ab079cfc79224", +"postgres": "74a30f6aa9def0b58afc616c", +"postgresql": "74a30f6aa9def0b58afc616c", "snowflake": "1096c3c8687c8e8eb538ff74", "tsql": "a686bb6db17ab079cfc79224" } @@ -23088,16 +23088,16 @@ [ "type", { -"DuckDB": "aff2599f42411f45ff949ed9", -"None": "be47a14e7523acb3fcbd4b5b", +"DuckDB": "e3729d015bce3152bae7cd02", +"None": "f98c7776673323299fbfe6b7", "bigquery": "f98c7776673323299fbfe6b7", -"databricks": "2ed848a19b2b41fdf76d9a87", -"duckdb": "aff2599f42411f45ff949ed9", -"generic": "be47a14e7523acb3fcbd4b5b", +"databricks": "f98c7776673323299fbfe6b7", +"duckdb": "e3729d015bce3152bae7cd02", +"generic": "f98c7776673323299fbfe6b7", "motherduck": "901c5133ed47d4769286e860", "mysql": "6c03a3450ea4742ee7396e3c", -"postgres": "aff2599f42411f45ff949ed9", -"postgresql": "aff2599f42411f45ff949ed9", +"postgres": "e3729d015bce3152bae7cd02", +"postgresql": "e3729d015bce3152bae7cd02", "snowflake": "a2a30dfc031641a60ba584fa", "tsql": "d1dc9a140dbe2eb1874c6ad7" } @@ -23106,17 +23106,17 @@ "type", { "DuckDB": "f2ed52f6bf46b4db9343551c", -"None": "f2ed52f6bf46b4db9343551c", -"bigquery": "f10bdd4d40eb495cbcecd31d", -"databricks": "f2ed52f6bf46b4db9343551c", +"None": "eeb8bd6105754e78339cd228", +"bigquery": "eeb8bd6105754e78339cd228", +"databricks": "eeb8bd6105754e78339cd228", "duckdb": "f2ed52f6bf46b4db9343551c", -"generic": "f2ed52f6bf46b4db9343551c", +"generic": "eeb8bd6105754e78339cd228", "motherduck": "901c5133ed47d4769286e860", -"mysql": "f2ed52f6bf46b4db9343551c", -"postgres": "f2ed52f6bf46b4db9343551c", -"postgresql": "f2ed52f6bf46b4db9343551c", +"mysql": "eeb8bd6105754e78339cd228", +"postgres": "eeb8bd6105754e78339cd228", +"postgresql": "eeb8bd6105754e78339cd228", "snowflake": "f2ed52f6bf46b4db9343551c", -"tsql": "f2ed52f6bf46b4db9343551c" +"tsql": "eeb8bd6105754e78339cd228" } ], [ @@ -23124,14 +23124,14 @@ { "DuckDB": "de89632e277dbc8c97115419", "None": "de89632e277dbc8c97115419", -"bigquery": "de89632e277dbc8c97115419", +"bigquery": "5abf4ec799a2ec32fb10da7e", "databricks": "de89632e277dbc8c97115419", "duckdb": "de89632e277dbc8c97115419", "generic": "de89632e277dbc8c97115419", "motherduck": "901c5133ed47d4769286e860", "mysql": "de89632e277dbc8c97115419", -"postgres": "de89632e277dbc8c97115419", -"postgresql": "de89632e277dbc8c97115419", +"postgres": "5abf4ec799a2ec32fb10da7e", +"postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", "tsql": "de89632e277dbc8c97115419" } @@ -23192,14 +23192,14 @@ { "DuckDB": "18db5afe2c1e80d1ee652b54", "None": "18db5afe2c1e80d1ee652b54", -"bigquery": "e1c36a5cdd2427ff2b089ac9", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "18db5afe2c1e80d1ee652b54", "duckdb": "18db5afe2c1e80d1ee652b54", "generic": "18db5afe2c1e80d1ee652b54", "motherduck": "901c5133ed47d4769286e860", "mysql": "18db5afe2c1e80d1ee652b54", -"postgres": "18db5afe2c1e80d1ee652b54", -"postgresql": "18db5afe2c1e80d1ee652b54", +"postgres": "47d64a5994d42a2b577166ea", +"postgresql": "47d64a5994d42a2b577166ea", "snowflake": "18db5afe2c1e80d1ee652b54", "tsql": "18db5afe2c1e80d1ee652b54" } @@ -23243,7 +23243,7 @@ { "DuckDB": "e08a957777bd278f921ca0ee", "None": "e08a957777bd278f921ca0ee", -"bigquery": "b1b23029bedaa16e2a6f8516", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "e08a957777bd278f921ca0ee", "duckdb": "e08a957777bd278f921ca0ee", "generic": "e08a957777bd278f921ca0ee", @@ -23311,7 +23311,7 @@ { "DuckDB": "62368fc15cefd5807807b209", "None": "62368fc15cefd5807807b209", -"bigquery": "e6ffdece2977be55da73b59a", +"bigquery": "c50a316d091c821866e4bfba", "databricks": "62368fc15cefd5807807b209", "duckdb": "62368fc15cefd5807807b209", "generic": "62368fc15cefd5807807b209", @@ -23328,14 +23328,14 @@ { "DuckDB": "4b356542044f1267fcfc487a", "None": "4b356542044f1267fcfc487a", -"bigquery": "4b356542044f1267fcfc487a", +"bigquery": "a5656d70e208254d1d3e9b4f", "databricks": "4b356542044f1267fcfc487a", "duckdb": "4b356542044f1267fcfc487a", "generic": "4b356542044f1267fcfc487a", "motherduck": "901c5133ed47d4769286e860", "mysql": "4b356542044f1267fcfc487a", -"postgres": "4b356542044f1267fcfc487a", -"postgresql": "4b356542044f1267fcfc487a", +"postgres": "a5656d70e208254d1d3e9b4f", +"postgresql": "a5656d70e208254d1d3e9b4f", "snowflake": "4b356542044f1267fcfc487a", "tsql": "4b356542044f1267fcfc487a" } @@ -23470,8 +23470,8 @@ "generic": "19a1a80ffc06169f32694de8", "motherduck": "901c5133ed47d4769286e860", "mysql": "19a1a80ffc06169f32694de8", -"postgres": "19a1a80ffc06169f32694de8", -"postgresql": "19a1a80ffc06169f32694de8", +"postgres": "ab1f5f46e69284778bd66d58", +"postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", "tsql": "19a1a80ffc06169f32694de8" } @@ -23498,14 +23498,14 @@ { "DuckDB": "de89632e277dbc8c97115419", "None": "de89632e277dbc8c97115419", -"bigquery": "de89632e277dbc8c97115419", +"bigquery": "5abf4ec799a2ec32fb10da7e", "databricks": "de89632e277dbc8c97115419", "duckdb": "de89632e277dbc8c97115419", "generic": "de89632e277dbc8c97115419", "motherduck": "901c5133ed47d4769286e860", "mysql": "de89632e277dbc8c97115419", -"postgres": "de89632e277dbc8c97115419", -"postgresql": "de89632e277dbc8c97115419", +"postgres": "5abf4ec799a2ec32fb10da7e", +"postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", "tsql": "de89632e277dbc8c97115419" } @@ -23538,8 +23538,8 @@ "generic": "d2c97d221599d038d9875873", "motherduck": "901c5133ed47d4769286e860", "mysql": "d2c97d221599d038d9875873", -"postgres": "d2c97d221599d038d9875873", -"postgresql": "d2c97d221599d038d9875873", +"postgres": "55155766823b6a3a5e882fc2", +"postgresql": "55155766823b6a3a5e882fc2", "snowflake": "d2c97d221599d038d9875873", "tsql": "d2c97d221599d038d9875873" } @@ -23549,14 +23549,14 @@ { "DuckDB": "fe23594c781e79cbb0c8927a", "None": "fe23594c781e79cbb0c8927a", -"bigquery": "fe23594c781e79cbb0c8927a", +"bigquery": "8a3f094f18fb4047f0febf59", "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", "motherduck": "901c5133ed47d4769286e860", "mysql": "fe23594c781e79cbb0c8927a", -"postgres": "fe23594c781e79cbb0c8927a", -"postgresql": "fe23594c781e79cbb0c8927a", +"postgres": "8a3f094f18fb4047f0febf59", +"postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "fe23594c781e79cbb0c8927a" } @@ -23566,14 +23566,14 @@ { "DuckDB": "b561884132dbe7e42b719110", "None": "b561884132dbe7e42b719110", -"bigquery": "b1b23029bedaa16e2a6f8516", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "b561884132dbe7e42b719110", "duckdb": "b561884132dbe7e42b719110", "generic": "b561884132dbe7e42b719110", "motherduck": "901c5133ed47d4769286e860", "mysql": "b561884132dbe7e42b719110", -"postgres": "b561884132dbe7e42b719110", -"postgresql": "b561884132dbe7e42b719110", +"postgres": "5aa51d84ac5e6c7a9306f348", +"postgresql": "5aa51d84ac5e6c7a9306f348", "snowflake": "b561884132dbe7e42b719110", "tsql": "b561884132dbe7e42b719110" } @@ -23589,8 +23589,8 @@ "generic": "f8219c960720043f0662a603", "motherduck": "901c5133ed47d4769286e860", "mysql": "f8219c960720043f0662a603", -"postgres": "f8219c960720043f0662a603", -"postgresql": "f8219c960720043f0662a603", +"postgres": "35a9c5c1419bff507cf65ae2", +"postgresql": "35a9c5c1419bff507cf65ae2", "snowflake": "f8219c960720043f0662a603", "tsql": "f8219c960720043f0662a603" } @@ -23606,8 +23606,8 @@ "generic": "e57ac1541fb51b362cd5b470", "motherduck": "901c5133ed47d4769286e860", "mysql": "e57ac1541fb51b362cd5b470", -"postgres": "e57ac1541fb51b362cd5b470", -"postgresql": "e57ac1541fb51b362cd5b470", +"postgres": "396b2ce32efb5365b393c230", +"postgresql": "396b2ce32efb5365b393c230", "snowflake": "4b356542044f1267fcfc487a", "tsql": "e57ac1541fb51b362cd5b470" } @@ -23742,8 +23742,8 @@ "generic": "2c6b371b030464f525066893", "motherduck": "901c5133ed47d4769286e860", "mysql": "2c6b371b030464f525066893", -"postgres": "2c6b371b030464f525066893", -"postgresql": "2c6b371b030464f525066893", +"postgres": "3feb479faeec777c2ade8afd", +"postgresql": "3feb479faeec777c2ade8afd", "snowflake": "2c6b371b030464f525066893", "tsql": "2c6b371b030464f525066893" } @@ -23770,14 +23770,14 @@ { "DuckDB": "08736485547ba339930697e9", "None": "08736485547ba339930697e9", -"bigquery": "08736485547ba339930697e9", +"bigquery": "837468174ef3e5cb4e4b4070", "databricks": "08736485547ba339930697e9", "duckdb": "08736485547ba339930697e9", "generic": "08736485547ba339930697e9", "motherduck": "901c5133ed47d4769286e860", "mysql": "08736485547ba339930697e9", -"postgres": "08736485547ba339930697e9", -"postgresql": "08736485547ba339930697e9", +"postgres": "eecd452b4f5f3f4133d0bd0a", +"postgresql": "eecd452b4f5f3f4133d0bd0a", "snowflake": "08736485547ba339930697e9", "tsql": "08736485547ba339930697e9" } @@ -23787,14 +23787,14 @@ { "DuckDB": "2fcd11f2fdbe6c4498c4833e", "None": "2fcd11f2fdbe6c4498c4833e", -"bigquery": "2fcd11f2fdbe6c4498c4833e", +"bigquery": "c7c38fdeaeb7858a44976f1d", "databricks": "2fcd11f2fdbe6c4498c4833e", "duckdb": "2fcd11f2fdbe6c4498c4833e", "generic": "2fcd11f2fdbe6c4498c4833e", "motherduck": "901c5133ed47d4769286e860", "mysql": "2fcd11f2fdbe6c4498c4833e", -"postgres": "2fcd11f2fdbe6c4498c4833e", -"postgresql": "2fcd11f2fdbe6c4498c4833e", +"postgres": "3255b7052c624f34aab6633f", +"postgresql": "3255b7052c624f34aab6633f", "snowflake": "2fcd11f2fdbe6c4498c4833e", "tsql": "2fcd11f2fdbe6c4498c4833e" } @@ -23804,14 +23804,14 @@ { "DuckDB": "fe23594c781e79cbb0c8927a", "None": "fe23594c781e79cbb0c8927a", -"bigquery": "fe23594c781e79cbb0c8927a", +"bigquery": "8a3f094f18fb4047f0febf59", "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", "motherduck": "901c5133ed47d4769286e860", "mysql": "fe23594c781e79cbb0c8927a", -"postgres": "fe23594c781e79cbb0c8927a", -"postgresql": "fe23594c781e79cbb0c8927a", +"postgres": "8a3f094f18fb4047f0febf59", +"postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "fe23594c781e79cbb0c8927a" } @@ -23838,7 +23838,7 @@ { "DuckDB": "5c28019dce48a74708c90a76", "None": "5c28019dce48a74708c90a76", -"bigquery": "f28a96f0eb38ad8874f6ec5d", +"bigquery": "5c28019dce48a74708c90a76", "databricks": "5c28019dce48a74708c90a76", "duckdb": "5c28019dce48a74708c90a76", "generic": "5c28019dce48a74708c90a76", @@ -23855,14 +23855,14 @@ { "DuckDB": "2fcd11f2fdbe6c4498c4833e", "None": "2fcd11f2fdbe6c4498c4833e", -"bigquery": "2fcd11f2fdbe6c4498c4833e", +"bigquery": "c7c38fdeaeb7858a44976f1d", "databricks": "2fcd11f2fdbe6c4498c4833e", "duckdb": "2fcd11f2fdbe6c4498c4833e", "generic": "2fcd11f2fdbe6c4498c4833e", "motherduck": "901c5133ed47d4769286e860", "mysql": "2fcd11f2fdbe6c4498c4833e", -"postgres": "2fcd11f2fdbe6c4498c4833e", -"postgresql": "2fcd11f2fdbe6c4498c4833e", +"postgres": "3255b7052c624f34aab6633f", +"postgresql": "3255b7052c624f34aab6633f", "snowflake": "2fcd11f2fdbe6c4498c4833e", "tsql": "2fcd11f2fdbe6c4498c4833e" } @@ -23888,17 +23888,17 @@ "type", { "DuckDB": "6f4372a332076048e56d5076", -"None": "6f4372a332076048e56d5076", -"bigquery": "74a30f6aa9def0b58afc616c", -"databricks": "6f4372a332076048e56d5076", +"None": "fa78cfa46a78783d069e2fc5", +"bigquery": "b0477b36767a62145aa9158c", +"databricks": "fa78cfa46a78783d069e2fc5", "duckdb": "6f4372a332076048e56d5076", -"generic": "6f4372a332076048e56d5076", +"generic": "fa78cfa46a78783d069e2fc5", "motherduck": "901c5133ed47d4769286e860", -"mysql": "6f4372a332076048e56d5076", +"mysql": "fa78cfa46a78783d069e2fc5", "postgres": "6f4372a332076048e56d5076", "postgresql": "6f4372a332076048e56d5076", "snowflake": "daeeb0f00b3ee4211b8da00f", -"tsql": "6f4372a332076048e56d5076" +"tsql": "fa78cfa46a78783d069e2fc5" } ], [ @@ -23974,14 +23974,14 @@ { "DuckDB": "aab0389e2b5470136660bf8a", "None": "aab0389e2b5470136660bf8a", -"bigquery": "c9b810639795a7bfab91b98d", +"bigquery": "a428302e4848ffe877284c84", "databricks": "aab0389e2b5470136660bf8a", "duckdb": "aab0389e2b5470136660bf8a", "generic": "aab0389e2b5470136660bf8a", "motherduck": "901c5133ed47d4769286e860", "mysql": "aab0389e2b5470136660bf8a", -"postgres": "aab0389e2b5470136660bf8a", -"postgresql": "aab0389e2b5470136660bf8a", +"postgres": "f16105cbc61d547e6c08efc8", +"postgresql": "f16105cbc61d547e6c08efc8", "snowflake": "97a20344d811fb77c7690863", "tsql": "aab0389e2b5470136660bf8a" } @@ -23991,7 +23991,7 @@ { "DuckDB": "62368fc15cefd5807807b209", "None": "62368fc15cefd5807807b209", -"bigquery": "e6ffdece2977be55da73b59a", +"bigquery": "c50a316d091c821866e4bfba", "databricks": "62368fc15cefd5807807b209", "duckdb": "62368fc15cefd5807807b209", "generic": "62368fc15cefd5807807b209", @@ -24023,17 +24023,17 @@ [ "type", { -"DuckDB": "89cc3662f602a13aef39eb80", -"None": "f2bbf59a9c02f59ca89e66b6", -"bigquery": "f2bbf59a9c02f59ca89e66b6", -"databricks": "f2bbf59a9c02f59ca89e66b6", -"duckdb": "89cc3662f602a13aef39eb80", -"generic": "f2bbf59a9c02f59ca89e66b6", +"DuckDB": "01e3dfc024c37a8d62c7f810", +"None": "3a4d8bca5124769db23a43df", +"bigquery": "3a4d8bca5124769db23a43df", +"databricks": "3a4d8bca5124769db23a43df", +"duckdb": "01e3dfc024c37a8d62c7f810", +"generic": "3a4d8bca5124769db23a43df", "motherduck": "901c5133ed47d4769286e860", "mysql": "6c03a3450ea4742ee7396e3c", -"postgres": "89cc3662f602a13aef39eb80", -"postgresql": "89cc3662f602a13aef39eb80", -"snowflake": "6f88ef12d95c26bf8f0bd9e5", +"postgres": "01e3dfc024c37a8d62c7f810", +"postgresql": "01e3dfc024c37a8d62c7f810", +"snowflake": "db03b5ccf153c75e43846a6a", "tsql": "d1dc9a140dbe2eb1874c6ad7" } ], @@ -24060,11 +24060,11 @@ "DuckDB": "ef2689646fd9e772e27d2727", "None": "ef2689646fd9e772e27d2727", "bigquery": "ef2689646fd9e772e27d2727", -"databricks": "e63b3aab09c82955e2560928", +"databricks": "ef2689646fd9e772e27d2727", "duckdb": "ef2689646fd9e772e27d2727", "generic": "ef2689646fd9e772e27d2727", "motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"mysql": "ef2689646fd9e772e27d2727", "postgres": "ef2689646fd9e772e27d2727", "postgresql": "ef2689646fd9e772e27d2727", "snowflake": "ef2689646fd9e772e27d2727", @@ -24075,17 +24075,17 @@ "type", { "DuckDB": "c43574432afc90bcba6944b2", -"None": "7040afbb3a088baf55e328b2", -"bigquery": "7040afbb3a088baf55e328b2", -"databricks": "ba611057434e43344935471b", +"None": "61c3e35de716257a33271a64", +"bigquery": "61c3e35de716257a33271a64", +"databricks": "61c3e35de716257a33271a64", "duckdb": "c43574432afc90bcba6944b2", -"generic": "7040afbb3a088baf55e328b2", +"generic": "61c3e35de716257a33271a64", "motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", -"postgres": "1b3243409566f14d81c11fff", -"postgresql": "1b3243409566f14d81c11fff", +"mysql": "61c3e35de716257a33271a64", +"postgres": "61c3e35de716257a33271a64", +"postgresql": "61c3e35de716257a33271a64", "snowflake": "5b6535982c7d4576e34ee7cb", -"tsql": "d1dc9a140dbe2eb1874c6ad7" +"tsql": "61c3e35de716257a33271a64" } ], [ @@ -24132,7 +24132,7 @@ "duckdb": "bc9875824f2c0eb90fcb5e51", "generic": "0289c868616e51dc348bba36", "motherduck": "901c5133ed47d4769286e860", -"mysql": "fe2c1943dc029130eda3f066", +"mysql": "0289c868616e51dc348bba36", "postgres": "fe2c1943dc029130eda3f066", "postgresql": "fe2c1943dc029130eda3f066", "snowflake": "ffc35af553194c8ccffc9058", @@ -24229,14 +24229,14 @@ { "DuckDB": "df19c722959a4259621a1e09", "None": "df19c722959a4259621a1e09", -"bigquery": "df19c722959a4259621a1e09", +"bigquery": "c7c38fdeaeb7858a44976f1d", "databricks": "df19c722959a4259621a1e09", "duckdb": "df19c722959a4259621a1e09", "generic": "df19c722959a4259621a1e09", "motherduck": "901c5133ed47d4769286e860", "mysql": "df19c722959a4259621a1e09", -"postgres": "df19c722959a4259621a1e09", -"postgresql": "df19c722959a4259621a1e09", +"postgres": "1d7dd2736322bc33dae77252", +"postgresql": "1d7dd2736322bc33dae77252", "snowflake": "df19c722959a4259621a1e09", "tsql": "df19c722959a4259621a1e09" } @@ -24330,17 +24330,17 @@ "type", { "DuckDB": "c50a316d091c821866e4bfba", -"None": "c50a316d091c821866e4bfba", -"bigquery": "c50a316d091c821866e4bfba", -"databricks": "c50a316d091c821866e4bfba", +"None": "e6ffdece2977be55da73b59a", +"bigquery": "e6ffdece2977be55da73b59a", +"databricks": "e6ffdece2977be55da73b59a", "duckdb": "c50a316d091c821866e4bfba", -"generic": "c50a316d091c821866e4bfba", +"generic": "e6ffdece2977be55da73b59a", "motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"mysql": "e6ffdece2977be55da73b59a", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", -"tsql": "c50a316d091c821866e4bfba" +"tsql": "e6ffdece2977be55da73b59a" } ], [ @@ -24371,8 +24371,8 @@ "generic": "3673b4fbbd8a7fed7120ec2b", "motherduck": "901c5133ed47d4769286e860", "mysql": "3673b4fbbd8a7fed7120ec2b", -"postgres": "3673b4fbbd8a7fed7120ec2b", -"postgresql": "3673b4fbbd8a7fed7120ec2b", +"postgres": "9aff7fcc7d550e1a8d105471", +"postgresql": "9aff7fcc7d550e1a8d105471", "snowflake": "3673b4fbbd8a7fed7120ec2b", "tsql": "3673b4fbbd8a7fed7120ec2b" } @@ -24381,15 +24381,15 @@ "type", { "DuckDB": "d2e8f9f7288b371199b1652e", -"None": "9bca83e64bfacf0a80ca9b34", +"None": "fe1b4290ba0c2395d949643d", "bigquery": "fe1b4290ba0c2395d949643d", "databricks": "fe1b4290ba0c2395d949643d", "duckdb": "d2e8f9f7288b371199b1652e", -"generic": "9bca83e64bfacf0a80ca9b34", +"generic": "fe1b4290ba0c2395d949643d", "motherduck": "901c5133ed47d4769286e860", "mysql": "fe1b4290ba0c2395d949643d", -"postgres": "d2e8f9f7288b371199b1652e", -"postgresql": "d2e8f9f7288b371199b1652e", +"postgres": "e0367d4e3fcf904cd5657220", +"postgresql": "e0367d4e3fcf904cd5657220", "snowflake": "c326a07d4a8adffde91e55cd", "tsql": "fe1b4290ba0c2395d949643d" } @@ -24433,14 +24433,14 @@ { "DuckDB": "b69a57be16780e0a5e4acadb", "None": "b69a57be16780e0a5e4acadb", -"bigquery": "b69a57be16780e0a5e4acadb", +"bigquery": "67d251c042daee0bb2bf7351", "databricks": "b69a57be16780e0a5e4acadb", "duckdb": "b69a57be16780e0a5e4acadb", "generic": "b69a57be16780e0a5e4acadb", "motherduck": "901c5133ed47d4769286e860", "mysql": "b69a57be16780e0a5e4acadb", -"postgres": "b69a57be16780e0a5e4acadb", -"postgresql": "b69a57be16780e0a5e4acadb", +"postgres": "a7d1c1767c3d4e900c7d3cb4", +"postgresql": "a7d1c1767c3d4e900c7d3cb4", "snowflake": "b69a57be16780e0a5e4acadb", "tsql": "b69a57be16780e0a5e4acadb" } @@ -24500,17 +24500,17 @@ "type", { "DuckDB": "fa45b7e4a9ef3b5a9c5781af", -"None": "fa45b7e4a9ef3b5a9c5781af", -"bigquery": "fa45b7e4a9ef3b5a9c5781af", -"databricks": "fa45b7e4a9ef3b5a9c5781af", +"None": "ebe2513b6cb4001e42cc464b", +"bigquery": "0c43dc837a50933667588ef1", +"databricks": "ebe2513b6cb4001e42cc464b", "duckdb": "fa45b7e4a9ef3b5a9c5781af", -"generic": "fa45b7e4a9ef3b5a9c5781af", +"generic": "ebe2513b6cb4001e42cc464b", "motherduck": "901c5133ed47d4769286e860", -"mysql": "fa45b7e4a9ef3b5a9c5781af", +"mysql": "ebe2513b6cb4001e42cc464b", "postgres": "fa45b7e4a9ef3b5a9c5781af", "postgresql": "fa45b7e4a9ef3b5a9c5781af", "snowflake": "fa45b7e4a9ef3b5a9c5781af", -"tsql": "fa45b7e4a9ef3b5a9c5781af" +"tsql": "ebe2513b6cb4001e42cc464b" } ], [ @@ -24524,8 +24524,8 @@ "generic": "4b356542044f1267fcfc487a", "motherduck": "901c5133ed47d4769286e860", "mysql": "4b356542044f1267fcfc487a", -"postgres": "4b356542044f1267fcfc487a", -"postgresql": "4b356542044f1267fcfc487a", +"postgres": "a5656d70e208254d1d3e9b4f", +"postgresql": "a5656d70e208254d1d3e9b4f", "snowflake": "4b356542044f1267fcfc487a", "tsql": "4b356542044f1267fcfc487a" } @@ -24534,34 +24534,34 @@ "type", { "DuckDB": "1ebac487198b6c90b027faab", -"None": "1ebac487198b6c90b027faab", -"bigquery": "a428302e4848ffe877284c84", -"databricks": "1ebac487198b6c90b027faab", +"None": "79278352c17c9de0f5a28af5", +"bigquery": "c9b810639795a7bfab91b98d", +"databricks": "79278352c17c9de0f5a28af5", "duckdb": "1ebac487198b6c90b027faab", -"generic": "1ebac487198b6c90b027faab", +"generic": "79278352c17c9de0f5a28af5", "motherduck": "901c5133ed47d4769286e860", -"mysql": "1ebac487198b6c90b027faab", -"postgres": "1ebac487198b6c90b027faab", -"postgresql": "1ebac487198b6c90b027faab", -"snowflake": "652ff8b21f638eac8fdc46c5", -"tsql": "c692d974ee3f362ecb38f27c" +"mysql": "79278352c17c9de0f5a28af5", +"postgres": "79278352c17c9de0f5a28af5", +"postgresql": "79278352c17c9de0f5a28af5", +"snowflake": "97a20344d811fb77c7690863", +"tsql": "79278352c17c9de0f5a28af5" } ], [ "type", { "DuckDB": "1fe8a2d5a41383bd8e502808", -"None": "1fe8a2d5a41383bd8e502808", +"None": "70f3d045595fbd43219efb36", "bigquery": "70f3d045595fbd43219efb36", "databricks": "70f3d045595fbd43219efb36", "duckdb": "1fe8a2d5a41383bd8e502808", -"generic": "1fe8a2d5a41383bd8e502808", +"generic": "70f3d045595fbd43219efb36", "motherduck": "901c5133ed47d4769286e860", "mysql": "70f3d045595fbd43219efb36", -"postgres": "1fe8a2d5a41383bd8e502808", -"postgresql": "1fe8a2d5a41383bd8e502808", +"postgres": "3c17ab67cd8c034fc4911ef2", +"postgresql": "3c17ab67cd8c034fc4911ef2", "snowflake": "1fe8a2d5a41383bd8e502808", -"tsql": "1fe8a2d5a41383bd8e502808" +"tsql": "70f3d045595fbd43219efb36" } ], [ @@ -24587,11 +24587,11 @@ "DuckDB": "035bb58fd2c7dca585a5899c", "None": "035bb58fd2c7dca585a5899c", "bigquery": "035bb58fd2c7dca585a5899c", -"databricks": "958e791772d9007024c889d3", +"databricks": "035bb58fd2c7dca585a5899c", "duckdb": "035bb58fd2c7dca585a5899c", "generic": "035bb58fd2c7dca585a5899c", "motherduck": "901c5133ed47d4769286e860", -"mysql": "770b1d275ab7799c8484f304", +"mysql": "035bb58fd2c7dca585a5899c", "postgres": "035bb58fd2c7dca585a5899c", "postgresql": "035bb58fd2c7dca585a5899c", "snowflake": "035bb58fd2c7dca585a5899c", @@ -24602,34 +24602,34 @@ "type", { "DuckDB": "7515c0db413239044adef879", -"None": "7515c0db413239044adef879", -"bigquery": "f276e907770196606577c932", -"databricks": "7515c0db413239044adef879", +"None": "d4b89e61a66aeb076db92954", +"bigquery": "bb92c01c64d1a2d32abb67b5", +"databricks": "d4b89e61a66aeb076db92954", "duckdb": "7515c0db413239044adef879", -"generic": "7515c0db413239044adef879", +"generic": "d4b89e61a66aeb076db92954", "motherduck": "901c5133ed47d4769286e860", -"mysql": "7515c0db413239044adef879", +"mysql": "d4b89e61a66aeb076db92954", "postgres": "7515c0db413239044adef879", "postgresql": "7515c0db413239044adef879", "snowflake": "67b5874a2c569226c490211f", -"tsql": "7515c0db413239044adef879" +"tsql": "d4b89e61a66aeb076db92954" } ], [ "type", { "DuckDB": "a3b6b25265be330ec52a3322", -"None": "a3b6b25265be330ec52a3322", -"bigquery": "204206d1d1eecf007966cc5b", -"databricks": "e3bc1b8bad22d8104417b00f", +"None": "62368fc15cefd5807807b209", +"bigquery": "c50a316d091c821866e4bfba", +"databricks": "62368fc15cefd5807807b209", "duckdb": "a3b6b25265be330ec52a3322", -"generic": "a3b6b25265be330ec52a3322", +"generic": "62368fc15cefd5807807b209", "motherduck": "901c5133ed47d4769286e860", -"mysql": "919b7ec6a7b49d8527b784cd", -"postgres": "a3b6b25265be330ec52a3322", -"postgresql": "a3b6b25265be330ec52a3322", +"mysql": "62368fc15cefd5807807b209", +"postgres": "62368fc15cefd5807807b209", +"postgresql": "62368fc15cefd5807807b209", "snowflake": "919b7ec6a7b49d8527b784cd", -"tsql": "a3b6b25265be330ec52a3322" +"tsql": "62368fc15cefd5807807b209" } ], [ @@ -24686,16 +24686,16 @@ [ "type", { -"DuckDB": "4ed568ea1adb2c8a7141be20", -"None": "b981394a3f4d961a44e73868", -"bigquery": "f65845aac707b13992e53f87", -"databricks": "b981394a3f4d961a44e73868", -"duckdb": "4ed568ea1adb2c8a7141be20", -"generic": "b981394a3f4d961a44e73868", +"DuckDB": "efd6c6e5ae853495e89afda4", +"None": "8e94482f81cd02e976359898", +"bigquery": "8e94482f81cd02e976359898", +"databricks": "8e94482f81cd02e976359898", +"duckdb": "efd6c6e5ae853495e89afda4", +"generic": "8e94482f81cd02e976359898", "motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", -"postgres": "4ed568ea1adb2c8a7141be20", -"postgresql": "4ed568ea1adb2c8a7141be20", +"mysql": "8e94482f81cd02e976359898", +"postgres": "efd6c6e5ae853495e89afda4", +"postgresql": "efd6c6e5ae853495e89afda4", "snowflake": "311749caf0bd53fee7b23fa1", "tsql": "8e94482f81cd02e976359898" } @@ -24745,8 +24745,8 @@ "generic": "3673b4fbbd8a7fed7120ec2b", "motherduck": "901c5133ed47d4769286e860", "mysql": "3673b4fbbd8a7fed7120ec2b", -"postgres": "3673b4fbbd8a7fed7120ec2b", -"postgresql": "3673b4fbbd8a7fed7120ec2b", +"postgres": "9aff7fcc7d550e1a8d105471", +"postgresql": "9aff7fcc7d550e1a8d105471", "snowflake": "3673b4fbbd8a7fed7120ec2b", "tsql": "3673b4fbbd8a7fed7120ec2b" } @@ -24762,8 +24762,8 @@ "generic": "7770c7aba92f1bba835ef3f8", "motherduck": "901c5133ed47d4769286e860", "mysql": "7770c7aba92f1bba835ef3f8", -"postgres": "7770c7aba92f1bba835ef3f8", -"postgresql": "7770c7aba92f1bba835ef3f8", +"postgres": "55de7125c9ef4832b853c094", +"postgresql": "55de7125c9ef4832b853c094", "snowflake": "7770c7aba92f1bba835ef3f8", "tsql": "7770c7aba92f1bba835ef3f8" } @@ -24772,17 +24772,17 @@ "type", { "DuckDB": "70fb4728c3adcf6dbe4975d5", -"None": "70fb4728c3adcf6dbe4975d5", -"bigquery": "70fb4728c3adcf6dbe4975d5", -"databricks": "70fb4728c3adcf6dbe4975d5", +"None": "fb4b6f5101f973d16ea8d1f3", +"bigquery": "fb4b6f5101f973d16ea8d1f3", +"databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", -"generic": "70fb4728c3adcf6dbe4975d5", +"generic": "fb4b6f5101f973d16ea8d1f3", "motherduck": "901c5133ed47d4769286e860", -"mysql": "70fb4728c3adcf6dbe4975d5", +"mysql": "fb4b6f5101f973d16ea8d1f3", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", -"tsql": "70fb4728c3adcf6dbe4975d5" +"tsql": "fb4b6f5101f973d16ea8d1f3" } ], [ @@ -24805,18 +24805,18 @@ [ "type", { -"DuckDB": "436bf2624179c536a0157841", -"None": "3c6e55e3040bec9d73dd1bae", +"DuckDB": "4cce9f29790ed5c86ebce81d", +"None": "00e3060d99b1d939ec9f3616", "bigquery": "f1e1c64039c39ec114b5ede6", -"databricks": "21bc62fef8fe5716ed99f7be", -"duckdb": "436bf2624179c536a0157841", -"generic": "3c6e55e3040bec9d73dd1bae", +"databricks": "b1961f9482bbd04d61ba1c52", +"duckdb": "4cce9f29790ed5c86ebce81d", +"generic": "00e3060d99b1d939ec9f3616", "motherduck": "901c5133ed47d4769286e860", -"mysql": "3c6e55e3040bec9d73dd1bae", -"postgres": "8141c88dbee0d916c5447a97", -"postgresql": "8141c88dbee0d916c5447a97", +"mysql": "00e3060d99b1d939ec9f3616", +"postgres": "6e5449b80c5f377b124fba88", +"postgresql": "6e5449b80c5f377b124fba88", "snowflake": "cfa49b8de6f3939e2701f38d", -"tsql": "28f1e600e17067938d8a74f4" +"tsql": "7755cfc984436641c774f231" } ], [ @@ -24824,7 +24824,7 @@ { "DuckDB": "431ce1fed4f6ce1e6c2a6479", "None": "6282d36888b8e0aadda09779", -"bigquery": "9da8d0485dcb32b3b63f0ed3", +"bigquery": "02e0b2ed25f32a8d09350a23", "databricks": "6282d36888b8e0aadda09779", "duckdb": "431ce1fed4f6ce1e6c2a6479", "generic": "6282d36888b8e0aadda09779", @@ -24880,7 +24880,7 @@ "duckdb": "0712c33a016f7dd6a37ee011", "generic": "0712c33a016f7dd6a37ee011", "motherduck": "901c5133ed47d4769286e860", -"mysql": "0f3f17e0fe13c04b84ded556", +"mysql": "0712c33a016f7dd6a37ee011", "postgres": "0712c33a016f7dd6a37ee011", "postgresql": "0712c33a016f7dd6a37ee011", "snowflake": "0712c33a016f7dd6a37ee011", @@ -24892,14 +24892,14 @@ { "DuckDB": "fe23594c781e79cbb0c8927a", "None": "fe23594c781e79cbb0c8927a", -"bigquery": "fe23594c781e79cbb0c8927a", +"bigquery": "8a3f094f18fb4047f0febf59", "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", "motherduck": "901c5133ed47d4769286e860", "mysql": "fe23594c781e79cbb0c8927a", -"postgres": "fe23594c781e79cbb0c8927a", -"postgresql": "fe23594c781e79cbb0c8927a", +"postgres": "8a3f094f18fb4047f0febf59", +"postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", "tsql": "fe23594c781e79cbb0c8927a" } @@ -24907,18 +24907,18 @@ [ "type", { -"DuckDB": "7fa8f60977ab1aa305af9d4b", -"None": "8c6ca3f77bf44837655a903e", -"bigquery": "89138ea06a52edf9b7991dd8", -"databricks": "3aa50c38af078e9f02f5a1c8", -"duckdb": "7fa8f60977ab1aa305af9d4b", -"generic": "8c6ca3f77bf44837655a903e", +"DuckDB": "909ab87c62786e3181c86ba6", +"None": "22bed6e6de77311e5f24d414", +"bigquery": "22bed6e6de77311e5f24d414", +"databricks": "22bed6e6de77311e5f24d414", +"duckdb": "909ab87c62786e3181c86ba6", +"generic": "22bed6e6de77311e5f24d414", "motherduck": "901c5133ed47d4769286e860", -"mysql": "8c6ca3f77bf44837655a903e", -"postgres": "8c6ca3f77bf44837655a903e", -"postgresql": "8c6ca3f77bf44837655a903e", +"mysql": "22bed6e6de77311e5f24d414", +"postgres": "22bed6e6de77311e5f24d414", +"postgresql": "22bed6e6de77311e5f24d414", "snowflake": "05188197e056965f895710b0", -"tsql": "8c6ca3f77bf44837655a903e" +"tsql": "22bed6e6de77311e5f24d414" } ], [ @@ -24926,14 +24926,14 @@ { "DuckDB": "ee47e1424cb01deb0cf51760", "None": "ee47e1424cb01deb0cf51760", -"bigquery": "ee47e1424cb01deb0cf51760", +"bigquery": "893ff554934dfc6f8253597b", "databricks": "ee47e1424cb01deb0cf51760", "duckdb": "ee47e1424cb01deb0cf51760", "generic": "ee47e1424cb01deb0cf51760", "motherduck": "901c5133ed47d4769286e860", "mysql": "ee47e1424cb01deb0cf51760", -"postgres": "ee47e1424cb01deb0cf51760", -"postgresql": "ee47e1424cb01deb0cf51760", +"postgres": "3c1521654088dd54d0dc9ead", +"postgresql": "3c1521654088dd54d0dc9ead", "snowflake": "ee47e1424cb01deb0cf51760", "tsql": "ee47e1424cb01deb0cf51760" } @@ -24949,8 +24949,8 @@ "generic": "49fdedaa5356fcccaa6f2f38", "motherduck": "901c5133ed47d4769286e860", "mysql": "49fdedaa5356fcccaa6f2f38", -"postgres": "49fdedaa5356fcccaa6f2f38", -"postgresql": "49fdedaa5356fcccaa6f2f38", +"postgres": "cf14fd6412673db81925b3a0", +"postgresql": "cf14fd6412673db81925b3a0", "snowflake": "49fdedaa5356fcccaa6f2f38", "tsql": "49fdedaa5356fcccaa6f2f38" } @@ -24959,17 +24959,17 @@ "type", { "DuckDB": "70fb4728c3adcf6dbe4975d5", -"None": "70fb4728c3adcf6dbe4975d5", -"bigquery": "70fb4728c3adcf6dbe4975d5", -"databricks": "70fb4728c3adcf6dbe4975d5", +"None": "fb4b6f5101f973d16ea8d1f3", +"bigquery": "fb4b6f5101f973d16ea8d1f3", +"databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", -"generic": "70fb4728c3adcf6dbe4975d5", +"generic": "fb4b6f5101f973d16ea8d1f3", "motherduck": "901c5133ed47d4769286e860", -"mysql": "70fb4728c3adcf6dbe4975d5", +"mysql": "fb4b6f5101f973d16ea8d1f3", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", -"tsql": "70fb4728c3adcf6dbe4975d5" +"tsql": "fb4b6f5101f973d16ea8d1f3" } ], [ @@ -24983,8 +24983,8 @@ "generic": "b8321619b794db091c9e96a1", "motherduck": "901c5133ed47d4769286e860", "mysql": "b8321619b794db091c9e96a1", -"postgres": "b8321619b794db091c9e96a1", -"postgresql": "b8321619b794db091c9e96a1", +"postgres": "bce26544873d6ec9191c9a86", +"postgresql": "bce26544873d6ec9191c9a86", "snowflake": "b8321619b794db091c9e96a1", "tsql": "b8321619b794db091c9e96a1" } @@ -25079,7 +25079,7 @@ { "DuckDB": "0f799a95a4f5ef502897f02c", "None": "0f799a95a4f5ef502897f02c", -"bigquery": "b1b23029bedaa16e2a6f8516", +"bigquery": "1e75e5227bea0ae294ac7af1", "databricks": "0f799a95a4f5ef502897f02c", "duckdb": "0f799a95a4f5ef502897f02c", "generic": "0f799a95a4f5ef502897f02c", @@ -25113,7 +25113,7 @@ { "DuckDB": "bda0c90e573c6a01b55627e2", "None": "bda0c90e573c6a01b55627e2", -"bigquery": "bda0c90e573c6a01b55627e2", +"bigquery": "0017a88bb60ec3403c786ed8", "databricks": "bda0c90e573c6a01b55627e2", "duckdb": "bda0c90e573c6a01b55627e2", "generic": "bda0c90e573c6a01b55627e2", @@ -25136,8 +25136,8 @@ "generic": "2705f19942a4194a72576911", "motherduck": "901c5133ed47d4769286e860", "mysql": "2705f19942a4194a72576911", -"postgres": "2705f19942a4194a72576911", -"postgresql": "2705f19942a4194a72576911", +"postgres": "5226d7e2a62c7650a87d6e84", +"postgresql": "5226d7e2a62c7650a87d6e84", "snowflake": "2705f19942a4194a72576911", "tsql": "2705f19942a4194a72576911" } @@ -25147,7 +25147,7 @@ { "DuckDB": "558da38ec4d9d5d6c72f60cf", "None": "558da38ec4d9d5d6c72f60cf", -"bigquery": "e6ffdece2977be55da73b59a", +"bigquery": "c50a316d091c821866e4bfba", "databricks": "558da38ec4d9d5d6c72f60cf", "duckdb": "558da38ec4d9d5d6c72f60cf", "generic": "558da38ec4d9d5d6c72f60cf", @@ -25163,17 +25163,17 @@ "type", { "DuckDB": "314ed506f24e92300a425780", -"None": "314ed506f24e92300a425780", -"bigquery": "314ed506f24e92300a425780", -"databricks": "314ed506f24e92300a425780", +"None": "9a66317fb8d040bf991502c0", +"bigquery": "9bafb08d41fd1c208b5deca4", +"databricks": "9a66317fb8d040bf991502c0", "duckdb": "314ed506f24e92300a425780", -"generic": "314ed506f24e92300a425780", +"generic": "9a66317fb8d040bf991502c0", "motherduck": "901c5133ed47d4769286e860", -"mysql": "314ed506f24e92300a425780", +"mysql": "9a66317fb8d040bf991502c0", "postgres": "314ed506f24e92300a425780", "postgresql": "314ed506f24e92300a425780", "snowflake": "314ed506f24e92300a425780", -"tsql": "314ed506f24e92300a425780" +"tsql": "9a66317fb8d040bf991502c0" } ], [ @@ -25197,17 +25197,17 @@ "type", { "DuckDB": "363159f251e264604338e690", -"None": "363159f251e264604338e690", -"bigquery": "363159f251e264604338e690", -"databricks": "363159f251e264604338e690", +"None": "9a63ad6168418af23d9b6fd1", +"bigquery": "8b8d5f75ba6dec68b4cebc6b", +"databricks": "9a63ad6168418af23d9b6fd1", "duckdb": "363159f251e264604338e690", -"generic": "363159f251e264604338e690", +"generic": "9a63ad6168418af23d9b6fd1", "motherduck": "901c5133ed47d4769286e860", -"mysql": "363159f251e264604338e690", -"postgres": "363159f251e264604338e690", -"postgresql": "363159f251e264604338e690", +"mysql": "9a63ad6168418af23d9b6fd1", +"postgres": "f276e907770196606577c932", +"postgresql": "f276e907770196606577c932", "snowflake": "363159f251e264604338e690", -"tsql": "363159f251e264604338e690" +"tsql": "9a63ad6168418af23d9b6fd1" } ], [ @@ -25289,8 +25289,8 @@ "generic": "7f7363467010768b3de8fe05", "motherduck": "901c5133ed47d4769286e860", "mysql": "7f7363467010768b3de8fe05", -"postgres": "7f7363467010768b3de8fe05", -"postgresql": "7f7363467010768b3de8fe05", +"postgres": "039da801fa79cb9eabce307d", +"postgresql": "039da801fa79cb9eabce307d", "snowflake": "7f7363467010768b3de8fe05", "tsql": "7f7363467010768b3de8fe05" } @@ -25340,8 +25340,8 @@ "generic": "c91ea1fc2032c6f196132d1e", "motherduck": "901c5133ed47d4769286e860", "mysql": "c91ea1fc2032c6f196132d1e", -"postgres": "c91ea1fc2032c6f196132d1e", -"postgresql": "c91ea1fc2032c6f196132d1e", +"postgres": "652ff8b21f638eac8fdc46c5", +"postgresql": "652ff8b21f638eac8fdc46c5", "snowflake": "c91ea1fc2032c6f196132d1e", "tsql": "c91ea1fc2032c6f196132d1e" } @@ -25352,11 +25352,11 @@ "DuckDB": "87496ad15619ba3d233601ad", "None": "87496ad15619ba3d233601ad", "bigquery": "87496ad15619ba3d233601ad", -"databricks": "5878645b159a4a60df8c2681", +"databricks": "87496ad15619ba3d233601ad", "duckdb": "87496ad15619ba3d233601ad", "generic": "87496ad15619ba3d233601ad", "motherduck": "901c5133ed47d4769286e860", -"mysql": "675e09f896465856e2673811", +"mysql": "87496ad15619ba3d233601ad", "postgres": "87496ad15619ba3d233601ad", "postgresql": "87496ad15619ba3d233601ad", "snowflake": "87496ad15619ba3d233601ad", @@ -25367,17 +25367,17 @@ "type", { "DuckDB": "23f70cb2333740d79361d52e", -"None": "23f70cb2333740d79361d52e", -"bigquery": "816384049477221b0a4a5b89", -"databricks": "23f70cb2333740d79361d52e", +"None": "a33d60d9b009878ab623c1d8", +"bigquery": "bb92c01c64d1a2d32abb67b5", +"databricks": "a33d60d9b009878ab623c1d8", "duckdb": "23f70cb2333740d79361d52e", -"generic": "23f70cb2333740d79361d52e", +"generic": "a33d60d9b009878ab623c1d8", "motherduck": "901c5133ed47d4769286e860", -"mysql": "23f70cb2333740d79361d52e", -"postgres": "23f70cb2333740d79361d52e", -"postgresql": "23f70cb2333740d79361d52e", +"mysql": "a33d60d9b009878ab623c1d8", +"postgres": "f276e907770196606577c932", +"postgresql": "f276e907770196606577c932", "snowflake": "23f70cb2333740d79361d52e", -"tsql": "23f70cb2333740d79361d52e" +"tsql": "a33d60d9b009878ab623c1d8" } ], [ @@ -25425,8 +25425,8 @@ "generic": "669084d1ea704ae5b8ee05c2", "motherduck": "901c5133ed47d4769286e860", "mysql": "669084d1ea704ae5b8ee05c2", -"postgres": "669084d1ea704ae5b8ee05c2", -"postgresql": "669084d1ea704ae5b8ee05c2", +"postgres": "93c1b5b5960e26479baf88d0", +"postgresql": "93c1b5b5960e26479baf88d0", "snowflake": "669084d1ea704ae5b8ee05c2", "tsql": "669084d1ea704ae5b8ee05c2" } @@ -25436,14 +25436,14 @@ { "DuckDB": "08736485547ba339930697e9", "None": "08736485547ba339930697e9", -"bigquery": "08736485547ba339930697e9", +"bigquery": "837468174ef3e5cb4e4b4070", "databricks": "08736485547ba339930697e9", "duckdb": "08736485547ba339930697e9", "generic": "08736485547ba339930697e9", "motherduck": "901c5133ed47d4769286e860", "mysql": "08736485547ba339930697e9", -"postgres": "08736485547ba339930697e9", -"postgresql": "08736485547ba339930697e9", +"postgres": "eecd452b4f5f3f4133d0bd0a", +"postgresql": "eecd452b4f5f3f4133d0bd0a", "snowflake": "08736485547ba339930697e9", "tsql": "08736485547ba339930697e9" } diff --git a/tests/integration/src/sqlbuild/adapter/type_system/_test_types.py b/tests/integration/src/sqlbuild/adapter/type_system/_test_types.py index 858a2dee8e..89beb07f70 100644 --- a/tests/integration/src/sqlbuild/adapter/type_system/_test_types.py +++ b/tests/integration/src/sqlbuild/adapter/type_system/_test_types.py @@ -33,7 +33,7 @@ class TypeNormalizationTestCase: @dataclass(frozen=True) class UnknownDialectTypeTestCase: - """A dialect Polyglot does not know, and the error Python's wheel raised for it.""" + """A dialect SQLBuild has no type rules for, and the error normalization raises for it.""" description: str dialect: str diff --git a/tests/integration/src/sqlbuild/adapter/type_system/helpers.py b/tests/integration/src/sqlbuild/adapter/type_system/helpers.py index fba51cf622..2d019de5d1 100644 --- a/tests/integration/src/sqlbuild/adapter/type_system/helpers.py +++ b/tests/integration/src/sqlbuild/adapter/type_system/helpers.py @@ -168,7 +168,7 @@ def is_normalized(outcome: TypeOutcome) -> bool: def logged_parse_error(outcome: TypeOutcome) -> bool: - """Return whether a Polyglot parse failure was logged for this type.""" + """Return whether a type parse failure was logged for this type.""" return bool(outcome.logged_errors) diff --git a/tests/integration/src/sqlbuild/adapter/type_system/test_native_type_normalization.py b/tests/integration/src/sqlbuild/adapter/type_system/test_native_type_normalization.py index 79cb00a903..81c037456e 100644 --- a/tests/integration/src/sqlbuild/adapter/type_system/test_native_type_normalization.py +++ b/tests/integration/src/sqlbuild/adapter/type_system/test_native_type_normalization.py @@ -244,7 +244,7 @@ def test_given_type_when_normalizing_then_python_shape_is_returned( "test_case", [ UnknownDialectTypeTestCase( - description="a dialect name Polyglot does not know", + description="a dialect name SQLBuild does not know", dialect="motherduck", expected_error="ValueError: Unknown dialect: motherduck", ), From d2e1fb8a48566feecc100f2e90c4ba7797930a17 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 08:05:59 +0000 Subject: [PATCH 027/106] fix(ui): stop a burst of control API requests on every live refresh The person trigger listing now carries each trigger's newest tick, so the jobs and triggers pages no longer read ticks once per trigger. Pages coalesce change notices into one reload, and the HTTP client shares an in-flight listing between pages that refresh together. --- clients/typescript/src/schema.d.ts | 7 ++- .../api/_helpers/resources/person_reads.rs | 6 +-- .../src/declarations/_helpers/reads.rs | 14 ++++++ .../declarations/main/list_person_triggers.rs | 21 ++++++-- .../src/declarations/models.rs | 8 ++++ .../tests/api/test_reads.rs | 48 +++++++++++++++++-- ui/src/lib/api/http-client.test.ts | 42 +++++++++++++++- ui/src/lib/api/http-client.ts | 33 +++++++++++-- ui/src/lib/state/resource.svelte.ts | 37 +++++++++++++- 9 files changed, 196 insertions(+), 20 deletions(-) diff --git a/clients/typescript/src/schema.d.ts b/clients/typescript/src/schema.d.ts index 117fb8c855..fa63012262 100644 --- a/clients/typescript/src/schema.d.ts +++ b/clients/typescript/src/schema.d.ts @@ -1476,6 +1476,10 @@ export interface components { /** Format: uuid */ principal_id: string; }; + /** @description A trigger as people list it, with its newest tick so a list needs no read per trigger. */ + PersonTriggerRecord: components["schemas"]["TriggerRecord"] & { + latest_tick?: null | components["schemas"]["TickRecord"]; + }; /** @description Observed physical evidence is not a live warehouse-existence guarantee. */ PhysicalEvidence: { observation: components["schemas"]["CatalogObservation"]; @@ -3350,12 +3354,13 @@ export interface operations { }; requestBody?: never; responses: { + /** @description Published triggers with their newest tick */ 200: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["TriggerRecord"][]; + "application/json": components["schemas"]["PersonTriggerRecord"][]; }; }; /** @description Session rejected */ diff --git a/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_reads.rs b/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_reads.rs index 79fbb670ad..e4c055796c 100644 --- a/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_reads.rs +++ b/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_reads.rs @@ -10,7 +10,7 @@ use crate::api::models::ReadQuery; use crate::credentials::models::{AuditContext, AuthenticatedPerson, DefaultRole}; use crate::declarations::main::list_person_jobs::list_person_jobs; use crate::declarations::main::list_person_triggers::list_person_triggers; -use crate::declarations::models::{JobRecord, TriggerRecord}; +use crate::declarations::models::{JobRecord, PersonTriggerRecord}; use crate::runs::main::list_person_run_nodes::list_person_run_nodes; use crate::runs::models::RunNodeRecord; use crate::scheduler::main::list_person_ticks::list_person_ticks; @@ -87,14 +87,14 @@ pub(crate) async fn jobs( #[utoipa::path(get, path = "/api/control/locations/{location_id}/targets/{target}/triggers", operation_id = "listPersonTriggers", params(("location_id" = Uuid, Path), ("target" = String, Path)), - responses((status = 200, body = Vec), (status = 401, description = "Session rejected"), + responses((status = 200, body = Vec, description = "Published triggers with their newest tick"), (status = 401, description = "Session rejected"), (status = 403, body = crate::api::models::ApiErrorBody)), security(("session" = []), ("cliSession" = [])))] pub(crate) async fn triggers( (State(store), Extension(context)): (State, Extension), Extension(default_role): Extension, Path((location_id, target)): Path<(Uuid, String)>, Extension(AuthenticatedPerson(principal, _)): Extension, -) -> Result>, ApiError> { +) -> Result>, ApiError> { Ok(Json( list_person_triggers( &store, diff --git a/crates/sqlbuild-orchestration/src/declarations/_helpers/reads.rs b/crates/sqlbuild-orchestration/src/declarations/_helpers/reads.rs index 00f3bf5150..ac716e189b 100644 --- a/crates/sqlbuild-orchestration/src/declarations/_helpers/reads.rs +++ b/crates/sqlbuild-orchestration/src/declarations/_helpers/reads.rs @@ -3,6 +3,7 @@ use uuid::Uuid; use crate::declarations::models::{JobRecord, StoredJob, TriggerRecord}; +use crate::scheduler::models::TickRecord; use crate::store::errors::ControlError; use crate::store::main::query_records::query_records; use crate::store::models::{ControlTransaction, Parameter}; @@ -42,3 +43,16 @@ pub(crate) async fn jobs_in( .collect(); Ok((transaction, jobs)) } + +/// The newest tick of every published trigger in the target, each found through the trigger-time index. +pub(crate) async fn latest_ticks_in( + transaction: ControlTransaction, + (location_id, target): (Uuid, &str), +) -> Result<(ControlTransaction, Vec), ControlError> { + query_records(transaction, + "SELECT k.tick_id, k.trigger_id, k.scheduled_for, k.evaluated_at, k.epoch, k.outcome, k.run_id, k.covered_by_run_id \ + FROM sqb_triggers t JOIN sqb_ticks k ON k.tick_id=(SELECT x.tick_id FROM sqb_ticks x WHERE x.trigger_id=t.trigger_id \ + ORDER BY x.scheduled_for DESC LIMIT 1) \ + WHERE t.location_id=$1 AND t.target=$2 AND t.removed_in_snapshot_id IS NULL", + vec![Parameter::Id(location_id), Parameter::Text(target.into())]).await +} diff --git a/crates/sqlbuild-orchestration/src/declarations/main/list_person_triggers.rs b/crates/sqlbuild-orchestration/src/declarations/main/list_person_triggers.rs index 472327f34c..3e178777ac 100644 --- a/crates/sqlbuild-orchestration/src/declarations/main/list_person_triggers.rs +++ b/crates/sqlbuild-orchestration/src/declarations/main/list_person_triggers.rs @@ -1,11 +1,13 @@ //! A person lists triggers only with view on every current node of the target. +use std::collections::BTreeMap; + use uuid::Uuid; use crate::authorization::main::authorize_listing::authorize_listing; use crate::credentials::models::{AuditContext, DefaultRole}; -use crate::declarations::_helpers::reads::triggers_in; -use crate::declarations::models::TriggerRecord; +use crate::declarations::_helpers::reads::{latest_ticks_in, triggers_in}; +use crate::declarations::models::PersonTriggerRecord; use crate::store::errors::ControlError; use crate::store::main::{begin::begin, commit::commit}; use crate::store::models::{ControlStore, TransactionMode}; @@ -15,7 +17,7 @@ pub async fn list_person_triggers( (context, credentials): (&AuditContext, Uuid), (location_id, target): (Uuid, &str), default_role: DefaultRole, -) -> Result, ControlError> { +) -> Result, ControlError> { let (transaction, _) = authorize_listing( begin(store, TransactionMode::Write).await?, (context, credentials), @@ -24,6 +26,17 @@ pub async fn list_person_triggers( ) .await?; let (transaction, triggers) = triggers_in(transaction, (location_id, target)).await?; + let (transaction, ticks) = latest_ticks_in(transaction, (location_id, target)).await?; commit(transaction).await?; - Ok(triggers) + let mut latest: BTreeMap = ticks + .into_iter() + .map(|tick| (tick.trigger_id, tick)) + .collect(); + Ok(triggers + .into_iter() + .map(|trigger| PersonTriggerRecord { + latest_tick: latest.remove(&trigger.trigger_id), + trigger, + }) + .collect()) } diff --git a/crates/sqlbuild-orchestration/src/declarations/models.rs b/crates/sqlbuild-orchestration/src/declarations/models.rs index 726975e856..8a981a57b4 100644 --- a/crates/sqlbuild-orchestration/src/declarations/models.rs +++ b/crates/sqlbuild-orchestration/src/declarations/models.rs @@ -119,6 +119,14 @@ pub struct TriggerRecord { pub next_due_at: Option>, } +/// A trigger as people list it, with its newest tick so a list needs no read per trigger. +#[derive(Clone, Debug, PartialEq, Serialize, utoipa::ToSchema)] +pub struct PersonTriggerRecord { + #[serde(flatten)] + pub trigger: TriggerRecord, + pub latest_tick: Option, +} + /// Operator changes are immutable facts; clearing an override restores the published code default. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] #[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] diff --git a/crates/sqlbuild-orchestration/tests/api/test_reads.rs b/crates/sqlbuild-orchestration/tests/api/test_reads.rs index e3adc23031..2dabd755c2 100644 --- a/crates/sqlbuild-orchestration/tests/api/test_reads.rs +++ b/crates/sqlbuild-orchestration/tests/api/test_reads.rs @@ -1,12 +1,16 @@ //! Real HTTP reads keep both location and target scope, including guessed nested resource IDs. -use crate::scheduler::tests::contracts::ticks::helpers::calendar_text; +use crate::authorization::main::bootstrap_user::bootstrap_user; +use crate::declarations::main::list_person_triggers::list_person_triggers; +use crate::scheduler::tests::contracts::ticks::helpers::{calendar_text, calendar_time}; +use crate::store::main::{begin::begin, commit::commit, query::query}; +use crate::store::models::{Parameter, TransactionMode}; use std::collections::BTreeMap; use std::time::Duration; use crate::credentials::main::tokens::issue_token::issue_token; use crate::credentials::main::tokens::revoke_token::revoke_token; -use crate::credentials::models::{AuditContext, CredentialScope}; +use crate::credentials::models::{AuditContext, CredentialScope, DefaultRole}; use crate::declarations::main::change_trigger::change_trigger; use crate::declarations::main::list_triggers::list_triggers; use crate::declarations::models::{TriggerChange, TriggerChangeRequest}; @@ -43,13 +47,13 @@ fn given_published_locations_and_scheduled_work_when_read_through_http_then_reso description: "SQLite scoped HTTP reads", backend: Backend::Sqlite, expected_runs: 3, - expected_ticks: 1, + expected_ticks: 2, }, ApiReadTestCase { description: "PostgreSQL scoped HTTP reads", backend: Backend::Postgres, expected_runs: 3, - expected_ticks: 1, + expected_ticks: 2, }, ]; for test_case in test_cases { @@ -316,6 +320,19 @@ fn given_published_locations_and_scheduled_work_when_read_through_http_then_reso let events = read_json(&client, (&event_path, &token.bearer)).await; assert_eq!(events.as_array().expect("events").len(), 1); assert_eq!(events[0]["request_json"], json!(change)); + let (transaction, _) = query( + begin(store, TransactionMode::Write).await.expect("seed older tick"), + "INSERT INTO sqb_ticks (tick_id,trigger_id,scheduled_for,evaluated_at,epoch,outcome) \ + VALUES ($1,$2,$3,$3,1,'covered')", + vec![ + Parameter::Id(Uuid::now_v7()), + Parameter::Id(trigger_id), + Parameter::Time(calendar_time(1)), + ], + ) + .await + .expect("older tick"); + commit(transaction).await.expect("commit older tick"); let ticks = read_json(&client, (&tick_path, &token.bearer)).await; assert_eq!( ticks.as_array().expect("ticks").len(), @@ -323,6 +340,29 @@ fn given_published_locations_and_scheduled_work_when_read_through_http_then_reso ); assert_eq!(ticks[0]["outcome"], "requested"); assert_eq!(ticks[0]["scheduled_for"], calendar_text(2)); + assert_eq!(ticks[1]["outcome"], "covered"); + let administrator = bootstrap_user(store, &audit, "orders-admin") + .await + .expect("administrator"); + for (target, expected) in [ + ("development", ticks[0].clone()), + ("validation", json!(null)), + ] { + let listed = list_person_triggers( + store, + (&audit, administrator), + (location_id, target), + DefaultRole::None, + ) + .await + .expect("person triggers"); + assert_eq!(listed.len(), 1, "{target}"); + assert_eq!( + json!(listed[0].latest_tick), + expected, + "newest tick only, never another target's" + ); + } for path in [&run_path, &event_path, &tick_path, &invocation_path] { assert_eq!( read_json(&client, (&format!("{path}?limit=1"), &token.bearer)) diff --git a/ui/src/lib/api/http-client.test.ts b/ui/src/lib/api/http-client.test.ts index 8d05b8dc33..9ae9cd852e 100644 --- a/ui/src/lib/api/http-client.test.ts +++ b/ui/src/lib/api/http-client.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { HttpChangeFeed, feedAllowed } from './http-client'; +import { HttpChangeFeed, createHttpClient, feedAllowed } from './http-client'; import type { ChangedSignal, ConnectionState } from './signals'; /** A stand-in `EventSource` the test opens, fails and feeds by hand. */ @@ -237,3 +237,43 @@ describe('feedAllowed', () => { await expect(feedAllowed('', ORDERS, () => {})).rejects.toThrow('network down'); }); }); + +describe('listing requests', () => { + const tick = { tick_id: 'k-2', trigger_id: 't-orders', scheduled_for: '2026-11-07T06:00:00Z', evaluated_at: '2026-11-07T06:00:02Z', epoch: 1, outcome: 'requested', run_id: null, covered_by_run_id: null }; + const declaration = { description: 'Daily orders', job_key: 'orders', schedule: { kind: 'cron', expression: '0 6 * * *', timezone: 'UTC' } }; + const triggers = [ + { trigger_id: 't-orders', name: 'orders_daily', job_key: 'orders', definition_json: declaration, status: 'running', last_tick_for: null, next_due_at: null, latest_tick: tick }, + { trigger_id: 't-stock', name: 'stock_daily', job_key: 'stock', definition_json: declaration, status: 'running', last_tick_for: null, next_due_at: null, latest_tick: null } + ]; + + function server() { + const calls: string[] = []; + vi.stubGlobal('document', { cookie: '' }); + vi.stubGlobal( + 'fetch', + vi.fn(async (request: Request) => { + const path = new URL(request.url).pathname.replace('/api/control/locations/0191f0aa-orders/targets/development', ''); + calls.push(`${request.method} ${path}`); + const body = request.method === 'POST' ? { applied: true } : path === '/triggers' ? triggers : []; + return new Response(JSON.stringify(body), { status: 200, headers: { 'content-type': 'application/json' } }); + }) + ); + return { calls, client: createHttpClient({ baseUrl: 'http://ui.test', onUnauthorized: () => {} }) }; + } + + it('lists triggers with their newest tick in one request, never one per trigger', async () => { + const { calls, client } = server(); + const listed = await client.listTriggers(ORDERS); + expect(listed.map((t) => t.lastTick?.id ?? null)).toEqual(['k-2', null]); + expect(calls.sort()).toEqual(['GET /runs', 'GET /triggers']); + }); + + it('shares listings between pages that refresh together, and reads again after a write', async () => { + const { calls, client } = server(); + await Promise.all([client.listJobs(ORDERS), client.listTriggers(ORDERS)]); + expect(calls.sort()).toEqual(['GET /jobs', 'GET /runs', 'GET /triggers']); + calls.length = 0; + await client.setTriggerStates(ORDERS, ['t-orders'], 'paused'); + expect([calls[0], ...calls.slice(1).sort()]).toEqual(['POST /api/control/triggers', 'GET /runs', 'GET /triggers']); + }); +}); diff --git a/ui/src/lib/api/http-client.ts b/ui/src/lib/api/http-client.ts index 70a1df67b9..8698a32f8a 100644 --- a/ui/src/lib/api/http-client.ts +++ b/ui/src/lib/api/http-client.ts @@ -207,6 +207,7 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt async cancelRun(scope: Scope, runId: string): Promise { const body = { run_id: runId, event_id: crypto.randomUUID(), reason: 'Cancelled from the web UI' }; const { error, response } = await http.POST('/api/control/runs/cancel', { body, params: { header: { 'x-sqb-csrf': csrfToken(document.cookie) } } }); + inFlight.clear(); if (response.status === 401) onUnauthorized(); if (!response.ok) throw new ApiError(errorText(error, response), response.status); return requireRun(scope, runId); @@ -215,6 +216,7 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt const source = await requireRun(scope, runId); const body = { run_id: crypto.randomUUID(), source_run_id: runId, from_failure: mode === 'from_failure', priority: source.priority, reason: 'Rerun from the web UI' }; const { error, response } = await http.POST('/api/control/runs/rerun', { body, params: { header: { 'x-sqb-csrf': csrfToken(document.cookie) } } }); + inFlight.clear(); if (response.status === 401) onUnauthorized(); if (!response.ok) throw new ApiError(errorText(error, response), response.status); return requireRun(scope, body.run_id); @@ -233,6 +235,7 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt const { locationId, target } = parseLocationKey(scope.locationId); const body = { run_id: crypto.randomUUID(), location_id: locationId, target, job_key: key, priority: 0, reason: 'Started from the web UI' }; const { error, response } = await http.POST('/api/control/runs', { body, params: { header: { 'x-sqb-csrf': csrfToken(document.cookie) } } }); + inFlight.clear(); if (response.status === 401) onUnauthorized(); if (!response.ok) throw new ApiError(errorText(error, response), response.status); return requireRun(scope, body.run_id); @@ -297,7 +300,22 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt } }; - async function listedJobs(scope: Scope) { + /** + * One request per listing at a time: pages that refresh together on a change notice share it + * instead of each asking the server again. A write clears it, so reads after a write see it. + */ + const inFlight = new Map>(); + function shared(key: string, load: () => Promise): Promise { + const pending = inFlight.get(key) as Promise | undefined; + if (pending) return pending; + const started: Promise = load().finally(() => inFlight.get(key) === started && inFlight.delete(key)); + inFlight.set(key, started); + return started; + } + + const listedJobs = (scope: Scope) => shared(`jobs ${scope.locationId}`, () => fetchJobs(scope)); + + async function fetchJobs(scope: Scope) { const { locationId, target } = parseLocationKey(scope.locationId); const { data, error, response } = await http.GET('/api/control/locations/{location_id}/targets/{target}/jobs', { params: { path: { location_id: locationId, target } } }); if (response.status === 401) onUnauthorized(); @@ -305,7 +323,10 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt return data; } - async function listedTriggers(scope: Scope) { + const listedTriggers = (scope: Scope) => shared(`triggers ${scope.locationId}`, () => fetchTriggers(scope)); + + /** Published triggers, each with its newest tick. */ + async function fetchTriggers(scope: Scope) { const { locationId, target } = parseLocationKey(scope.locationId); const { data, error, response } = await http.GET('/api/control/locations/{location_id}/targets/{target}/triggers', { params: { path: { location_id: locationId, target } } }); if (response.status === 401) onUnauthorized(); @@ -341,8 +362,7 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt /** Triggers with their latest tick and the newest listed run each started. */ async function triggersOf(scope: Scope): Promise { const [wires, runs] = await Promise.all([listedTriggers(scope), recentRuns(scope)]); - const latest = await Promise.all(wires.map(async (t) => (await ticksOf(scope, t.trigger_id, 1)).map(toTick)[0] ?? null)); - return wires.map((t, i) => toTrigger(t, runs, latest[i])); + return wires.map((t) => toTrigger(t, runs, t.latest_tick ? toTick(t.latest_tick) : null)); } async function requireTrigger(scope: Scope, id: string): Promise { @@ -355,12 +375,15 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt async function changeTrigger(triggerId: string, change: Wire['TriggerChange'], reason?: string | null) { const body = { trigger_id: triggerId, event_id: crypto.randomUUID(), actor: '', reason: reason?.trim() || 'Changed from the web UI', change }; const { error, response } = await http.POST('/api/control/triggers', { body, params: { header: { 'x-sqb-csrf': csrfToken(document.cookie) } } }); + inFlight.clear(); if (response.status === 401) onUnauthorized(); if (!response.ok) throw new ApiError(errorText(error, response), response.status); } + const listedRuns = (scope: Scope) => shared(`runs ${scope.locationId}`, () => fetchRuns(scope)); + /** The newest runs of the location, up to the server's cap. */ - async function listedRuns(scope: Scope) { + async function fetchRuns(scope: Scope) { const { locationId, target } = parseLocationKey(scope.locationId); const { data, error, response } = await http.GET('/api/control/locations/{location_id}/targets/{target}/runs', { params: { path: { location_id: locationId, target }, query: { limit: RUN_WINDOW } } diff --git a/ui/src/lib/state/resource.svelte.ts b/ui/src/lib/state/resource.svelte.ts index 79361e5e0f..db4da3e202 100644 --- a/ui/src/lib/state/resource.svelte.ts +++ b/ui/src/lib/state/resource.svelte.ts @@ -2,6 +2,9 @@ import type { ApiClient, Scope } from '$lib/api'; import { channelMatches } from '$lib/api'; import { app } from './app.svelte'; +/** How long a burst of change signals may last before one reload starts. */ +const SIGNAL_PAUSE_MS = 300; + export type Resource = { readonly data: T | undefined; readonly error: string | null; @@ -13,6 +16,10 @@ export type Resource = { * Loads data for the selected location and target, reloading when the scope * changes and when a "changed" signal arrives on one of `topics`. * + * Change signals arrive in bursts while a run progresses, so they are coalesced: + * one reload starts after a short pause, and signals during a reload queue at + * most one more. + * * Must be called during component initialisation. Reactive values read * synchronously inside `load` (such as route params) are also dependencies. */ @@ -24,25 +31,46 @@ export function createResource( let error = $state(null); let loading = $state(true); let latest = 0; + let settled = 0; + let reloadAgain = false; + let pause: ReturnType | null = null; function run(scope: Scope) { const request = ++latest; loading = true; + const done = () => { + settled = request; + if (reloadAgain && request === latest) { + reloadAgain = false; + refresh(); + } + }; load(app.client, scope).then( (value) => { if (request !== latest) return; data = value; error = null; loading = false; + done(); }, (reason: unknown) => { if (request !== latest) return; error = reason instanceof Error ? reason.message : String(reason); loading = false; + done(); } ); } + function signalled() { + if (pause) return; + pause = setTimeout(() => { + pause = null; + if (settled === latest) refresh(); + else reloadAgain = true; + }, SIGNAL_PAUSE_MS); + } + function refresh() { if (app.scope) run(app.scope); } @@ -54,9 +82,14 @@ export function createResource( $effect(() => { if (!topics.length) return; - return app.client.changes.subscribe((signal) => { - if (channelMatches(signal.channel, topics)) refresh(); + const unsubscribe = app.client.changes.subscribe((signal) => { + if (channelMatches(signal.channel, topics)) signalled(); }); + return () => { + unsubscribe(); + if (pause) clearTimeout(pause); + pause = null; + }; }); return { From 9c09e5475aee6732cb850eb5a22cda9abe382107 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 08:21:15 +0000 Subject: [PATCH 028/106] fix(types): read only shipped dialects and ClickHouse nested parameters MotherDuck now reads and spells types as DuckDB. MySQL and the other SQL analysis dialects no adapter ships raise Unknown dialect instead of reading as generic SQL. Under ClickHouse the type reader keeps nested type arguments, enum members with values, and named tuple fields. --- .../tests/test_evaluate_model_contracts.rs | 6 +- .../semantic_checks/tests/test_metadata.rs | 4 +- .../src/type_system/_helpers/parsed.rs | 1 - .../src/type_system/_helpers/spelling.rs | 32 +- .../src/type_system/_helpers/type_reader.rs | 95 +- .../src/type_system/constants.rs | 27 - .../src/type_system/tests/alias_groups.json | 29 - .../type_system/tests/test_normalize_type.rs | 42 +- ...and_comments_new_and_unknown_dialects.json | 8 +- ...ects_added_to_the_native_parser_build.json | 8 +- ...rized_nested_padded_and_invalid_types.json | 6000 ++++++++--------- .../test_native_type_normalization.py | 10 +- .../contracts/test_native_model_contracts.py | 10 +- .../test_native_metadata_checks.py | 2 +- .../test_native_semantic_checks.py | 2 +- 15 files changed, 3155 insertions(+), 3121 deletions(-) diff --git a/crates/sqlbuild-analysis/src/contracts/tests/test_evaluate_model_contracts.rs b/crates/sqlbuild-analysis/src/contracts/tests/test_evaluate_model_contracts.rs index d1a7294798..4573a0010a 100644 --- a/crates/sqlbuild-analysis/src/contracts/tests/test_evaluate_model_contracts.rs +++ b/crates/sqlbuild-analysis/src/contracts/tests/test_evaluate_model_contracts.rs @@ -282,11 +282,11 @@ fn given_models_sharing_types_when_evaluating_together_then_each_matches_its_own } #[test] -fn given_a_dialect_polyglot_does_not_know_when_comparing_types_then_python_error_is_returned() { +fn given_a_dialect_no_adapter_ships_when_comparing_types_then_python_error_is_returned() { let test_cases = [UnknownDialectTestCase { description: "an enforced contract compared under an unknown dialect", - dialect: "motherduck", - expected_error: "Unknown dialect: motherduck", + dialect: "mysql", + expected_error: "Unknown dialect: mysql", }]; for test_case in test_cases { let request = ContractRequest { diff --git a/crates/sqlbuild-analysis/src/semantic_checks/tests/test_metadata.rs b/crates/sqlbuild-analysis/src/semantic_checks/tests/test_metadata.rs index 55c2ed010d..7f5d8d5bb3 100644 --- a/crates/sqlbuild-analysis/src/semantic_checks/tests/test_metadata.rs +++ b/crates/sqlbuild-analysis/src/semantic_checks/tests/test_metadata.rs @@ -17,8 +17,8 @@ fn given_project_metadata_when_checking_then_reports_python_errors() { expected_outcome: Ok(expected_checked_outcome()), }, MetadataTestCase { - description: "a dialect the all-dialects parser build added", - dialect: "mysql", + description: "a shipped dialect without a native type parser", + dialect: "tsql", mart_shape: MART_SHAPE, expected_outcome: Ok(expected_checked_outcome()), }, diff --git a/crates/sqlbuild-analysis/src/type_system/_helpers/parsed.rs b/crates/sqlbuild-analysis/src/type_system/_helpers/parsed.rs index e09d002fee..3d2cada562 100644 --- a/crates/sqlbuild-analysis/src/type_system/_helpers/parsed.rs +++ b/crates/sqlbuild-analysis/src/type_system/_helpers/parsed.rs @@ -34,7 +34,6 @@ pub(crate) fn parse_type(type_sql: &str, dialect: SpellingDialect) -> Result TypeParser::Postgres, SpellingDialect::BigQuery => TypeParser::BigQuery, SpellingDialect::Generic - | SpellingDialect::MySql | SpellingDialect::Databricks | SpellingDialect::Tsql | SpellingDialect::ClickHouse => return read_type(type_sql, dialect), diff --git a/crates/sqlbuild-analysis/src/type_system/_helpers/spelling.rs b/crates/sqlbuild-analysis/src/type_system/_helpers/spelling.rs index 7465e89f2f..87235d9225 100644 --- a/crates/sqlbuild-analysis/src/type_system/_helpers/spelling.rs +++ b/crates/sqlbuild-analysis/src/type_system/_helpers/spelling.rs @@ -2,8 +2,6 @@ use serde_json::Value; -use crate::type_system::constants::GENERIC_TYPE_DIALECTS; - /// A dialect whose type spellings SQLBuild knows. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum SpellingDialect { @@ -16,7 +14,6 @@ pub(crate) enum SpellingDialect { Tsql, Sqlite, ClickHouse, - MySql, } impl SpellingDialect { @@ -26,14 +23,12 @@ impl SpellingDialect { "" | "generic" => Self::Generic, "snowflake" => Self::Snowflake, "bigquery" => Self::BigQuery, - "duckdb" => Self::DuckDb, + "duckdb" | "motherduck" => Self::DuckDb, "databricks" => Self::Databricks, "postgres" | "postgresql" => Self::Postgres, "tsql" | "mssql" | "sqlserver" => Self::Tsql, "sqlite" => Self::Sqlite, "clickhouse" => Self::ClickHouse, - "mysql" => Self::MySql, - other if GENERIC_TYPE_DIALECTS.contains(&other) => Self::Generic, _ => return None, }) } @@ -102,7 +97,7 @@ impl Speller<'_> { /// A boolean or numeric type; false for any other tag. fn number_type(&mut self, tree: &Value, tag: &str, map_key: bool) -> bool { use SpellingDialect::{ - BigQuery, ClickHouse, Databricks, MySql, Postgres, Snowflake, Sqlite, Tsql, + BigQuery, ClickHouse, Databricks, Postgres, Snowflake, Sqlite, Tsql, }; let dialect: SpellingDialect = self.dialect; let number = |key: &str| tree.get(key).and_then(Value::as_u64); @@ -166,7 +161,7 @@ impl Speller<'_> { } "float" => { let name: &str = if flag("real_spelling") - && !matches!(dialect, Databricks | Snowflake | BigQuery | MySql) + && !matches!(dialect, Databricks | Snowflake | BigQuery) { "REAL" } else { @@ -205,7 +200,7 @@ impl Speller<'_> { /// A text, binary, JSON or UUID type; false for any other tag. fn text_type(&mut self, tree: &Value, tag: &str, map_key: bool) -> bool { use SpellingDialect::{ - BigQuery, ClickHouse, Databricks, DuckDb, MySql, Postgres, Snowflake, Sqlite, Tsql, + BigQuery, ClickHouse, Databricks, DuckDb, Postgres, Snowflake, Sqlite, Tsql, }; let dialect: SpellingDialect = self.dialect; let number = |key: &str| tree.get(key).and_then(Value::as_u64); @@ -222,7 +217,6 @@ impl Speller<'_> { }, "var_char" => match dialect { DuckDb | Sqlite => self.sized("TEXT", length), - MySql if length.is_none() => self.write("TEXT"), Databricks if length.is_none() => self.write("STRING"), _ => self.sized("VARCHAR", length), }, @@ -238,7 +232,7 @@ impl Speller<'_> { _ => self.sized("TEXT", length), }, "string" => match dialect { - Postgres | MySql if length.is_none() => self.write("TEXT"), + Postgres if length.is_none() => self.write("TEXT"), Postgres => self.sized("VARCHAR", length), Tsql if length.is_none() => self.write("VARCHAR(MAX)"), Tsql => self.sized("VARCHAR", length), @@ -279,7 +273,6 @@ impl Speller<'_> { }, "uuid" => match dialect { Tsql => self.write("UNIQUEIDENTIFIER"), - MySql => self.write("CHAR(36)"), BigQuery | Databricks => self.write("STRING"), _ => self.write("UUID"), }, @@ -290,7 +283,7 @@ impl Speller<'_> { /// A date, time, timestamp or interval type; false for any other tag. fn time_type(&mut self, tree: &Value, tag: &str) -> bool { - use SpellingDialect::{BigQuery, ClickHouse, Databricks, DuckDb, MySql, Postgres, Tsql}; + use SpellingDialect::{BigQuery, ClickHouse, Databricks, DuckDb, Postgres, Tsql}; let dialect: SpellingDialect = self.dialect; let number = |key: &str| tree.get(key).and_then(Value::as_u64); let flag = |key: &str| tree.get(key).and_then(Value::as_bool).unwrap_or(false); @@ -318,7 +311,6 @@ impl Speller<'_> { BigQuery if timezone => self.write("TIMESTAMP"), BigQuery => self.write("DATETIME"), DuckDb if timezone => self.write("TIMESTAMPTZ"), - MySql => self.sized("TIMESTAMP", precision), _ if timezone => self.sized("TIMESTAMPTZ", precision), _ => self.sized("TIMESTAMP", precision), } @@ -339,7 +331,7 @@ impl Speller<'_> { /// A nested, custom or other type. fn other_type(&mut self, tree: &Value, tag: &str) { - use SpellingDialect::{ClickHouse, DuckDb, Generic, MySql, Postgres, Snowflake, Tsql}; + use SpellingDialect::{ClickHouse, DuckDb, Generic, Postgres, Snowflake, Tsql}; let dialect: SpellingDialect = self.dialect; let number = |key: &str| tree.get(key).and_then(Value::as_u64); match tag { @@ -363,7 +355,6 @@ impl Speller<'_> { self.write(")"); } Tsql => self.write("NVARCHAR(MAX)"), - MySql => self.write("JSON"), _ => { self.write("ARRAY<"); self.data_type(element, false); @@ -424,7 +415,7 @@ impl Speller<'_> { let name: &str = tree.get("name").and_then(Value::as_str).unwrap_or(""); self.write(&custom_name(name, dialect)); } - _ if matches!(dialect, Generic | MySql) => self.write(&tag.to_ascii_uppercase()), + _ if dialect == Generic => self.write(&tag.to_ascii_uppercase()), _ => self.write(&tag.to_ascii_uppercase().replace('_', "")), } } @@ -492,13 +483,6 @@ fn custom_name(name: &str, dialect: SpellingDialect) -> String { format!("{}{arguments}", base_upper.replace('_', "")) } (SpellingDialect::Tsql, "VARIANT") => "SQL_VARIANT".to_owned(), - (SpellingDialect::MySql, "GEOGRAPHY") => { - let srid: &str = arguments.trim_matches(['(', ')']); - format!( - "GEOMETRY SRID {}", - if srid.is_empty() { "4326" } else { srid } - ) - } ( SpellingDialect::BigQuery | SpellingDialect::ClickHouse | SpellingDialect::Snowflake, _, diff --git a/crates/sqlbuild-analysis/src/type_system/_helpers/type_reader.rs b/crates/sqlbuild-analysis/src/type_system/_helpers/type_reader.rs index b04611dc70..d98af083c5 100644 --- a/crates/sqlbuild-analysis/src/type_system/_helpers/type_reader.rs +++ b/crates/sqlbuild-analysis/src/type_system/_helpers/type_reader.rs @@ -26,6 +26,8 @@ pub(crate) fn read_type(type_sql: &str, dialect: SpellingDialect) -> Result String { match self { - Self::Word(text) | Self::Quoted(text) | Self::Number(text) => format!("'{text}'"), + Self::Word(text) | Self::Quoted(text) | Self::Literal(text) | Self::Number(text) => { + format!("'{text}'") + } Self::Punct(character) => format!("'{character}'"), } } + + /// The token as a type name writes it. + fn source(&self) -> String { + match self { + Self::Word(text) | Self::Number(text) => text.clone(), + Self::Quoted(text) => format!("\"{text}\""), + Self::Literal(text) => format!("'{}'", text.replace('\'', "''")), + Self::Punct(character) => character.to_string(), + } + } } fn tokens(type_sql: &str, dialect: SpellingDialect) -> Result, String> { @@ -78,7 +92,13 @@ fn tokens(type_sql: &str, dialect: SpellingDialect) -> Result, String } tokens.push(Token::Quoted(characters[start..index].iter().collect())); index += 1; - } else if "()<>,[]:;".contains(character) { + } else if character == '\'' && dialect == SpellingDialect::ClickHouse { + let (literal, end) = literal(&characters, index)?; + tokens.push(Token::Literal(literal)); + index = end; + } else if "()<>,[]:;".contains(character) + || ("=-".contains(character) && dialect == SpellingDialect::ClickHouse) + { tokens.push(Token::Punct(character)); index += 1; } else { @@ -88,6 +108,30 @@ fn tokens(type_sql: &str, dialect: SpellingDialect) -> Result, String Ok(tokens) } +/// The single-quoted string opening at `start`, with `''` and backslash escapes, and the index after it. +fn literal(characters: &[char], start: usize) -> Result<(String, usize), String> { + let mut text: String = String::new(); + let mut index: usize = start + 1; + loop { + match characters.get(index) { + None => return Err("unterminated string".to_owned()), + Some('\\') => { + text.extend(characters.get(index + 1)); + index += 2; + } + Some('\'') if characters.get(index + 1) == Some(&'\'') => { + text.push('\''); + index += 2; + } + Some('\'') => return Ok((text, index + 1)), + Some(&character) => { + text.push(character); + index += 1; + } + } + } +} + /// The closing quote of a quoted name `open` starts in `dialect`. fn quote_close(open: char, dialect: SpellingDialect) -> Option { let backtick: bool = matches!( @@ -354,10 +398,13 @@ impl Reader<'_> { } let mut parameters: Vec = Vec::new(); loop { - match self.next() { - Some(Token::Number(number)) => parameters.push(number), - Some(Token::Word(word)) => parameters.push(word), - _ => return Err(format!("expected a parameter of {name}")), + match self.dialect { + SpellingDialect::ClickHouse => parameters.push(self.clickhouse_parameter(&name)?), + _ => match self.next() { + Some(Token::Number(number)) => parameters.push(number), + Some(Token::Word(word)) => parameters.push(word), + _ => return Err(format!("expected a parameter of {name}")), + }, } if !self.eat_punct(',') { break; @@ -367,6 +414,42 @@ impl Reader<'_> { Ok(json!({"data_type": "custom", "name": format!("{name}({})", parameters.join(", "))})) } + /// One ClickHouse type parameter as written: a nested type, as in `Nullable(DateTime64(3))`, + /// a named tuple field, or an enum member and its value. + fn clickhouse_parameter(&mut self, name: &str) -> Result { + let mut text: String = String::new(); + let mut depth: usize = 0; + let mut spaced: bool = false; + loop { + let token: Token = match self.peek() { + None => return Err(format!("expected ')' after the parameters of {name}")), + Some(Token::Punct(',' | ')')) if depth == 0 => break, + Some(token) => token.clone(), + }; + self.position += 1; + let word: bool = !matches!(token, Token::Punct(_)); + match token { + Token::Punct('(') => depth += 1, + Token::Punct(')') => depth -= 1, + _ => {} + } + if token == Token::Punct('=') { + text.push_str(" = "); + spaced = false; + continue; + } + if word && spaced { + text.push(' '); + } + text.push_str(&token.source()); + spaced = word; + } + if text.is_empty() { + return Err(format!("expected a parameter of {name}")); + } + Ok(text) + } + /// The optional length of an integer or character type; empty parentheses state none. fn sized_length(&mut self) -> Result, String> { if self.peek() == Some(&Token::Punct('(')) diff --git a/crates/sqlbuild-analysis/src/type_system/constants.rs b/crates/sqlbuild-analysis/src/type_system/constants.rs index b432058a51..8fd1b089fb 100644 --- a/crates/sqlbuild-analysis/src/type_system/constants.rs +++ b/crates/sqlbuild-analysis/src/type_system/constants.rs @@ -9,33 +9,6 @@ pub(crate) const FLOAT_TYPE_NAMES: &[&str] = &["DOUBLE", "FLOAT", "FLOAT64", "RE pub(crate) const INTEGER_TYPE_NAMES: &[&str] = &[ "BIGINT", "INT", "INT64", "INTEGER", "LONG", "SMALLINT", "TINYINT", ]; -/// The other dialects SQL analysis accepts, which have no type rules of their own. -pub(crate) const GENERIC_TYPE_DIALECTS: &[&str] = &[ - "athena", - "cockroachdb", - "datafusion", - "doris", - "dremio", - "drill", - "druid", - "dune", - "exasol", - "fabric", - "hive", - "materialize", - "oracle", - "presto", - "redshift", - "risingwave", - "singlestore", - "solr", - "spark", - "starrocks", - "tableau", - "teradata", - "tidb", - "trino", -]; /// The deepest type nesting SQLBuild reads: SQLQuack's syntax tree depth. pub(crate) const MAX_TYPE_NESTING: usize = 512; /// The type nesting read on the caller's stack; deeper types get their own. diff --git a/crates/sqlbuild-analysis/src/type_system/tests/alias_groups.json b/crates/sqlbuild-analysis/src/type_system/tests/alias_groups.json index b0308fc473..ba75108724 100644 --- a/crates/sqlbuild-analysis/src/type_system/tests/alias_groups.json +++ b/crates/sqlbuild-analysis/src/type_system/tests/alias_groups.json @@ -174,35 +174,6 @@ ["TIMESTAMPTZ(10)", "TIMESTAMP_TZ(10)"], ["TIMESTAMP_LTZ", "TIMESTAMP_LTZ(10)"] ], - "mysql": [ - ["ARRAY< DEC( 38 , 0 ) >", "ARRAY< FLOAT64(10,2) >", "ARRAY< TIMESTAMP(10) >", "ARRAY< TIMESTAMP_LTZ>", "ARRAY< TIMESTAMP_NTZ(0)>", "ARRAY< VARIANT[]>", "ARRAY>", "ARRAY", "ARRAY", "ARRAY", "ARRAY", "ARRAY", "ARRAY", "BIGINT[]", "bignumeric(10,2) []", "DECIMAL( 38 , 0 ) []", "DECIMAL(10,2)[]", "DOUBLE PRECISION(10)[]", "FLOAT64(10,2) []", "INT[]", "JSON", "JSON []", "MONEY(10)[]", "SMALLINT(10) []", "TIME(0) []", "TIMESTAMPNTZ(0) []", "TIMESTAMP_NTZ []"], - ["BIGINT", "INT64"], - ["BOOL", "BOOLEAN", "BOOLEAN(10)"], - ["CHAR", "CHARACTER"], - ["CHAR(10)", "CHARACTER(10)"], - ["CHARACTER VARYING(10)", "string(10)", "TEXT(10)", "VARCHAR(10)"], - ["DATETIME", "DATETIME(0)", "DATETIME(10)"], - ["DECIMAL( 38 , 0 )", "NUMERIC( 38 , 0 )"], - ["DECIMAL(10, 2)", "NUMERIC(10,2)"], - ["DOUBLE", "DOUBLE PRECISION", "FLOAT64"], - ["DOUBLE PRECISION(10)", "DOUBLE(10)"], - ["FLOAT", "REAL"], - ["FLOAT(10)", "REAL(10)"], - ["FLOAT64( 38 , 0 )", "FLOAT64(10)", "FLOAT64(10, 2)", "FLOAT64(MAX)"], - ["INT", "integer"], - ["INT(10)", "integer(10)"], - ["INT64( 38 , 0 )", "INT64(10)", "INT64(10,2)", "INT64(MAX)"], - ["LONG", "LONG(10)"], - ["TIMESTAMP", "TIMESTAMP WITH TIME ZONE", "TIMESTAMP WITHOUT TIME ZONE", "TIMESTAMPTZ", "TIMESTAMP_TZ"], - ["TIMESTAMP(0)", "timestamptz(0)", "TIMESTAMP_TZ(0)"], - ["TIMESTAMPNTZ", "TIMESTAMPNTZ(0)", "TIMESTAMPNTZ(10)", "TIMESTAMP_NTZ", "TIMESTAMP_NTZ(0)", "TIMESTAMP_NTZ(10)"], - ["TIMESTAMP_LTZ", "TIMESTAMP_LTZ(0)", "TIMESTAMP_LTZ(10)"], - ["character varying", "STRING", "TEXT", "VARCHAR"], - ["date", "DATE(10)"], - ["decimal", "NUMERIC"], - ["decimal(10)", "NUMERIC(10)"], - ["timestamp(10)", "TIMESTAMPTZ(10)", "TIMESTAMP_TZ(10)"] - ], "postgres": [ ["ARRAY< FLOAT64(10,2) >", "FLOAT64(10,2) []"], ["ARRAY< TIMESTAMP_NTZ(0)>", "TIMESTAMPNTZ(0) []"], diff --git a/crates/sqlbuild-analysis/src/type_system/tests/test_normalize_type.rs b/crates/sqlbuild-analysis/src/type_system/tests/test_normalize_type.rs index e33e79fba0..278b6c7657 100644 --- a/crates/sqlbuild-analysis/src/type_system/tests/test_normalize_type.rs +++ b/crates/sqlbuild-analysis/src/type_system/tests/test_normalize_type.rs @@ -87,16 +87,44 @@ fn given_type_strings_when_normalizing_then_python_normalization_is_returned() { ), }, NormalizeTypeTestCase { - description: "another SQL analysis dialect reads types as generic SQL", - type_sql: "INT", - dialect: "mysql", - expected_normalization: Ok("INT | integer | - | - | - | -"), + description: "MotherDuck reads and spells types as DuckDB", + type_sql: "STRUCT(a INT, b VARCHAR(10))[]", + dialect: "motherduck", + expected_normalization: Ok("STRUCT(AINT,BTEXT(10))[] | other | - | - | - | -"), + }, + NormalizeTypeTestCase { + description: "a ClickHouse nested type argument, as cursor types arrive upper-cased", + type_sql: "NULLABLE(DATETIME64(3))", + dialect: "clickhouse", + expected_normalization: Ok("NULLABLE(DATETIME64(3)) | other | - | - | - | -"), + }, + NormalizeTypeTestCase { + description: "a ClickHouse enum member and its value", + type_sql: "ENUM8('a' = 1, 'it''s' = -2)", + dialect: "clickhouse", + expected_normalization: Ok("ENUM8('A'=1,'IT''S'=-2) | other | - | - | - | -"), + }, + NormalizeTypeTestCase { + description: "a ClickHouse named tuple field", + type_sql: "TUPLE(a INT64, b LOWCARDINALITY(NULLABLE(STRING)))", + dialect: "clickhouse", + expected_normalization: Ok( + "TUPLE(AINT64,BLOWCARDINALITY(NULLABLE(STRING))) | other | - | - | - | -", + ), }, NormalizeTypeTestCase { - description: "a dialect name SQLBuild does not know raises Python's error", + description: "an enum member outside ClickHouse falls back to the text", + type_sql: "ENUM8('a' = 1)", + dialect: "tsql", + expected_normalization: Ok( + "ENUM8('A'=1) | other | - | - | - | unexpected character '''", + ), + }, + NormalizeTypeTestCase { + description: "a dialect no SQLBuild adapter ships raises Python's error", type_sql: "INT", - dialect: "motherduck", - expected_normalization: Err("Unknown dialect: motherduck"), + dialect: "mysql", + expected_normalization: Err("Unknown dialect: mysql"), }, NormalizeTypeTestCase { description: "non-ASCII text upper-cases with Python's Unicode mapping", diff --git a/tests/goldens/python_oracle/metadata_formerly_deferred_non_ascii_names_and_comments_new_and_unknown_dialects.json b/tests/goldens/python_oracle/metadata_formerly_deferred_non_ascii_names_and_comments_new_and_unknown_dialects.json index c1e0c0cba7..ca4fc48fe6 100644 --- a/tests/goldens/python_oracle/metadata_formerly_deferred_non_ascii_names_and_comments_new_and_unknown_dialects.json +++ b/tests/goldens/python_oracle/metadata_formerly_deferred_non_ascii_names_and_comments_new_and_unknown_dialects.json @@ -3,8 +3,8 @@ "metadata", { "None": "257eadf2980dd54d687bb376", +"clickhouse": "e9bb705b46de595b24a50b7d", "duckdb": "d1d66a9dd0325471cc034afd", -"mysql": "257eadf2980dd54d687bb376", "nonsense": "6f6079f31e12e028922d40f2", "tsql": "257eadf2980dd54d687bb376" } @@ -13,8 +13,8 @@ "metadata", { "None": "c9f66e2829627aeb5e4728db", +"clickhouse": "c9f66e2829627aeb5e4728db", "duckdb": "0ed0773a2a4a6fca23c1dcac", -"mysql": "c9f66e2829627aeb5e4728db", "nonsense": "6f6079f31e12e028922d40f2", "tsql": "c9f66e2829627aeb5e4728db" } @@ -23,8 +23,8 @@ "metadata", { "None": "a86fc9ef8fd7f46e7110dff4", +"clickhouse": "a86fc9ef8fd7f46e7110dff4", "duckdb": "30c78fd5afb3d05479d6c4ca", -"mysql": "a86fc9ef8fd7f46e7110dff4", "nonsense": "6f6079f31e12e028922d40f2", "tsql": "a86fc9ef8fd7f46e7110dff4" } @@ -33,8 +33,8 @@ "metadata", { "None": "9225fd40168382c4100e98c3", +"clickhouse": "9225fd40168382c4100e98c3", "duckdb": "55da090c1795767af58252eb", -"mysql": "9225fd40168382c4100e98c3", "nonsense": "6f6079f31e12e028922d40f2", "tsql": "9225fd40168382c4100e98c3" } diff --git a/tests/goldens/python_oracle/semantic_formerly_deferred_dialects_added_to_the_native_parser_build.json b/tests/goldens/python_oracle/semantic_formerly_deferred_dialects_added_to_the_native_parser_build.json index 64b357cbe9..74d44ab9a3 100644 --- a/tests/goldens/python_oracle/semantic_formerly_deferred_dialects_added_to_the_native_parser_build.json +++ b/tests/goldens/python_oracle/semantic_formerly_deferred_dialects_added_to_the_native_parser_build.json @@ -2,22 +2,18 @@ [ "completion", { +"clickhouse": "25140233ccf8e7760c91ffd6", "databricks": "25140233ccf8e7760c91ffd6", -"mysql": "25140233ccf8e7760c91ffd6", -"oracle": "25140233ccf8e7760c91ffd6", "sqlite": "25140233ccf8e7760c91ffd6", -"trino": "25140233ccf8e7760c91ffd6", "tsql": "25140233ccf8e7760c91ffd6" } ], [ "completion", { +"clickhouse": "edd3b1ec05f9481067078045", "databricks": "edd3b1ec05f9481067078045", -"mysql": "edd3b1ec05f9481067078045", -"oracle": "edd3b1ec05f9481067078045", "sqlite": "edd3b1ec05f9481067078045", -"trino": "edd3b1ec05f9481067078045", "tsql": "edd3b1ec05f9481067078045" } ] diff --git a/tests/goldens/python_oracle/types_generated_seeded_scalar_parameterized_nested_padded_and_invalid_types.json b/tests/goldens/python_oracle/types_generated_seeded_scalar_parameterized_nested_padded_and_invalid_types.json index 10981f8afe..f2b91b5ff9 100644 --- a/tests/goldens/python_oracle/types_generated_seeded_scalar_parameterized_nested_padded_and_invalid_types.json +++ b/tests/goldens/python_oracle/types_generated_seeded_scalar_parameterized_nested_padded_and_invalid_types.json @@ -8,8 +8,8 @@ "databricks": "7e163d1c3f6840b5164def94", "duckdb": "7e163d1c3f6840b5164def94", "generic": "7e163d1c3f6840b5164def94", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "7e163d1c3f6840b5164def94", +"motherduck": "7e163d1c3f6840b5164def94", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7e163d1c3f6840b5164def94", "postgresql": "7e163d1c3f6840b5164def94", "snowflake": "7e163d1c3f6840b5164def94", @@ -25,8 +25,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1eb7fb4e8cf75773c5573562", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1eb7fb4e8cf75773c5573562", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -42,8 +42,8 @@ "databricks": "19a1a80ffc06169f32694de8", "duckdb": "19a1a80ffc06169f32694de8", "generic": "19a1a80ffc06169f32694de8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "19a1a80ffc06169f32694de8", +"motherduck": "19a1a80ffc06169f32694de8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ab1f5f46e69284778bd66d58", "postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", @@ -59,8 +59,8 @@ "databricks": "e6badc927d1adf4563b4f3a5", "duckdb": "e6badc927d1adf4563b4f3a5", "generic": "e6badc927d1adf4563b4f3a5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e6badc927d1adf4563b4f3a5", +"motherduck": "e6badc927d1adf4563b4f3a5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e6badc927d1adf4563b4f3a5", "postgresql": "e6badc927d1adf4563b4f3a5", "snowflake": "e6badc927d1adf4563b4f3a5", @@ -76,8 +76,8 @@ "databricks": "bbb2563edda71d0b69ef0855", "duckdb": "8a3f094f18fb4047f0febf59", "generic": "bbb2563edda71d0b69ef0855", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "51d717b08676179ee1ccefe4", +"motherduck": "8a3f094f18fb4047f0febf59", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "bbb2563edda71d0b69ef0855", "postgresql": "bbb2563edda71d0b69ef0855", "snowflake": "bbb2563edda71d0b69ef0855", @@ -93,8 +93,8 @@ "databricks": "688179272da645629077e980", "duckdb": "688179272da645629077e980", "generic": "688179272da645629077e980", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "688179272da645629077e980", +"motherduck": "688179272da645629077e980", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "688179272da645629077e980", "postgresql": "688179272da645629077e980", "snowflake": "b210f8eb26c37b422f786c6b", @@ -110,8 +110,8 @@ "databricks": "0703bf71a54942bdaa28406e", "duckdb": "0703bf71a54942bdaa28406e", "generic": "0703bf71a54942bdaa28406e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0703bf71a54942bdaa28406e", +"motherduck": "0703bf71a54942bdaa28406e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0703bf71a54942bdaa28406e", "postgresql": "0703bf71a54942bdaa28406e", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -127,8 +127,8 @@ "databricks": "81a45386abae5f6be0a027d0", "duckdb": "5c8b98f516cedefc237fa721", "generic": "81a45386abae5f6be0a027d0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "81a45386abae5f6be0a027d0", +"motherduck": "5c8b98f516cedefc237fa721", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5c8b98f516cedefc237fa721", "postgresql": "5c8b98f516cedefc237fa721", "snowflake": "5c8b98f516cedefc237fa721", @@ -144,8 +144,8 @@ "databricks": "9ae2060b9503b8d23efaa736", "duckdb": "6f9bc04f2e8c71a032255bac", "generic": "9ae2060b9503b8d23efaa736", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9ae2060b9503b8d23efaa736", +"motherduck": "6f9bc04f2e8c71a032255bac", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9ae2060b9503b8d23efaa736", "postgresql": "9ae2060b9503b8d23efaa736", "snowflake": "6f9bc04f2e8c71a032255bac", @@ -161,8 +161,8 @@ "databricks": "ee1b4ffc65518c536e3632ee", "duckdb": "ee1b4ffc65518c536e3632ee", "generic": "ee1b4ffc65518c536e3632ee", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ee1b4ffc65518c536e3632ee", +"motherduck": "ee1b4ffc65518c536e3632ee", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ee1b4ffc65518c536e3632ee", "postgresql": "ee1b4ffc65518c536e3632ee", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -178,8 +178,8 @@ "databricks": "ebc821a95b9286e22c2b26d0", "duckdb": "ebc821a95b9286e22c2b26d0", "generic": "ebc821a95b9286e22c2b26d0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ebc821a95b9286e22c2b26d0", +"motherduck": "ebc821a95b9286e22c2b26d0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ebc821a95b9286e22c2b26d0", "postgresql": "ebc821a95b9286e22c2b26d0", "snowflake": "ebc821a95b9286e22c2b26d0", @@ -195,8 +195,8 @@ "databricks": "0f799a95a4f5ef502897f02c", "duckdb": "0f799a95a4f5ef502897f02c", "generic": "0f799a95a4f5ef502897f02c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0f799a95a4f5ef502897f02c", +"motherduck": "0f799a95a4f5ef502897f02c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0f799a95a4f5ef502897f02c", "postgresql": "0f799a95a4f5ef502897f02c", "snowflake": "0f799a95a4f5ef502897f02c", @@ -212,8 +212,8 @@ "databricks": "2bfa352725f9d8a91250f75e", "duckdb": "2bfa352725f9d8a91250f75e", "generic": "2bfa352725f9d8a91250f75e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2bfa352725f9d8a91250f75e", +"motherduck": "2bfa352725f9d8a91250f75e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2bfa352725f9d8a91250f75e", "postgresql": "2bfa352725f9d8a91250f75e", "snowflake": "2bfa352725f9d8a91250f75e", @@ -229,8 +229,8 @@ "databricks": "1e75e5227bea0ae294ac7af1", "duckdb": "19bdd92ea0cc5fb028657510", "generic": "19bdd92ea0cc5fb028657510", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "19bdd92ea0cc5fb028657510", +"motherduck": "19bdd92ea0cc5fb028657510", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "19bdd92ea0cc5fb028657510", "postgresql": "19bdd92ea0cc5fb028657510", "snowflake": "8ff614b7306c0edbe36ce00b", @@ -246,8 +246,8 @@ "databricks": "18db5afe2c1e80d1ee652b54", "duckdb": "18db5afe2c1e80d1ee652b54", "generic": "18db5afe2c1e80d1ee652b54", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "18db5afe2c1e80d1ee652b54", +"motherduck": "18db5afe2c1e80d1ee652b54", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "47d64a5994d42a2b577166ea", "postgresql": "47d64a5994d42a2b577166ea", "snowflake": "18db5afe2c1e80d1ee652b54", @@ -263,8 +263,8 @@ "databricks": "73151c1398b30e5c5d18f4ce", "duckdb": "73151c1398b30e5c5d18f4ce", "generic": "73151c1398b30e5c5d18f4ce", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "73151c1398b30e5c5d18f4ce", +"motherduck": "73151c1398b30e5c5d18f4ce", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f68b7dbd74ec7c3b9f3066ef", "postgresql": "f68b7dbd74ec7c3b9f3066ef", "snowflake": "73151c1398b30e5c5d18f4ce", @@ -280,8 +280,8 @@ "databricks": "da0625706f12b764a6912278", "duckdb": "3147a9e06daa6de4a4df5abc", "generic": "da0625706f12b764a6912278", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "da0625706f12b764a6912278", +"motherduck": "3147a9e06daa6de4a4df5abc", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3147a9e06daa6de4a4df5abc", "postgresql": "3147a9e06daa6de4a4df5abc", "snowflake": "67b5874a2c569226c490211f", @@ -297,8 +297,8 @@ "databricks": "0703bf71a54942bdaa28406e", "duckdb": "0703bf71a54942bdaa28406e", "generic": "0703bf71a54942bdaa28406e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0703bf71a54942bdaa28406e", +"motherduck": "0703bf71a54942bdaa28406e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0703bf71a54942bdaa28406e", "postgresql": "0703bf71a54942bdaa28406e", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -314,8 +314,8 @@ "databricks": "78b2cf275c640af54feeca31", "duckdb": "78b2cf275c640af54feeca31", "generic": "78b2cf275c640af54feeca31", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "78b2cf275c640af54feeca31", +"motherduck": "78b2cf275c640af54feeca31", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "78b2cf275c640af54feeca31", "postgresql": "78b2cf275c640af54feeca31", "snowflake": "8ff614b7306c0edbe36ce00b", @@ -331,8 +331,8 @@ "databricks": "f15c56475abfc73488abe63b", "duckdb": "f15c56475abfc73488abe63b", "generic": "f15c56475abfc73488abe63b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f15c56475abfc73488abe63b", +"motherduck": "f15c56475abfc73488abe63b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f15c56475abfc73488abe63b", "postgresql": "f15c56475abfc73488abe63b", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -348,8 +348,8 @@ "databricks": "717cf79ef777823eb3576deb", "duckdb": "717cf79ef777823eb3576deb", "generic": "717cf79ef777823eb3576deb", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "717cf79ef777823eb3576deb", +"motherduck": "717cf79ef777823eb3576deb", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "717cf79ef777823eb3576deb", "postgresql": "717cf79ef777823eb3576deb", "snowflake": "717cf79ef777823eb3576deb", @@ -365,8 +365,8 @@ "databricks": "ea191a784359be345cd9ded6", "duckdb": "a6c6f4aa4257d422cbf3f875", "generic": "a6c6f4aa4257d422cbf3f875", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a6c6f4aa4257d422cbf3f875", +"motherduck": "a6c6f4aa4257d422cbf3f875", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a6c6f4aa4257d422cbf3f875", "postgresql": "a6c6f4aa4257d422cbf3f875", "snowflake": "a6c6f4aa4257d422cbf3f875", @@ -382,8 +382,8 @@ "databricks": "1e75e5227bea0ae294ac7af1", "duckdb": "19bdd92ea0cc5fb028657510", "generic": "78b2cf275c640af54feeca31", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "19bdd92ea0cc5fb028657510", +"motherduck": "19bdd92ea0cc5fb028657510", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "78b2cf275c640af54feeca31", "postgresql": "78b2cf275c640af54feeca31", "snowflake": "8ff614b7306c0edbe36ce00b", @@ -399,8 +399,8 @@ "databricks": "0f799a95a4f5ef502897f02c", "duckdb": "0f799a95a4f5ef502897f02c", "generic": "0f799a95a4f5ef502897f02c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0f799a95a4f5ef502897f02c", +"motherduck": "0f799a95a4f5ef502897f02c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0f799a95a4f5ef502897f02c", "postgresql": "0f799a95a4f5ef502897f02c", "snowflake": "0f799a95a4f5ef502897f02c", @@ -416,8 +416,8 @@ "databricks": "e9b32e3b5a5505b32f33e169", "duckdb": "e9b32e3b5a5505b32f33e169", "generic": "e9b32e3b5a5505b32f33e169", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e9b32e3b5a5505b32f33e169", +"motherduck": "e9b32e3b5a5505b32f33e169", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3fd123ac5ae610dffb11b495", "postgresql": "3fd123ac5ae610dffb11b495", "snowflake": "e9b32e3b5a5505b32f33e169", @@ -433,8 +433,8 @@ "databricks": "3f71ab5f3c1c040da96f2cec", "duckdb": "3f71ab5f3c1c040da96f2cec", "generic": "3f71ab5f3c1c040da96f2cec", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3f71ab5f3c1c040da96f2cec", +"motherduck": "3f71ab5f3c1c040da96f2cec", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3f71ab5f3c1c040da96f2cec", "postgresql": "3f71ab5f3c1c040da96f2cec", "snowflake": "3f71ab5f3c1c040da96f2cec", @@ -450,8 +450,8 @@ "databricks": "e4f0244beaa01667e80482b6", "duckdb": "a766fea06fc070b352976b0a", "generic": "a766fea06fc070b352976b0a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a766fea06fc070b352976b0a", +"motherduck": "a766fea06fc070b352976b0a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a766fea06fc070b352976b0a", "postgresql": "a766fea06fc070b352976b0a", "snowflake": "a766fea06fc070b352976b0a", @@ -467,8 +467,8 @@ "databricks": "4fb572024031cb7631777be1", "duckdb": "4114b18e7e26d16613331fa7", "generic": "4114b18e7e26d16613331fa7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4114b18e7e26d16613331fa7", +"motherduck": "4114b18e7e26d16613331fa7", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "4114b18e7e26d16613331fa7", "postgresql": "4114b18e7e26d16613331fa7", "snowflake": "4114b18e7e26d16613331fa7", @@ -484,8 +484,8 @@ "databricks": "98a80237e4e0a12f35bbf17e", "duckdb": "98a80237e4e0a12f35bbf17e", "generic": "98a80237e4e0a12f35bbf17e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "98a80237e4e0a12f35bbf17e", +"motherduck": "98a80237e4e0a12f35bbf17e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "98a80237e4e0a12f35bbf17e", "postgresql": "98a80237e4e0a12f35bbf17e", "snowflake": "98a80237e4e0a12f35bbf17e", @@ -501,8 +501,8 @@ "databricks": "d61c83ef8bfdf043fa672cb5", "duckdb": "d61c83ef8bfdf043fa672cb5", "generic": "d61c83ef8bfdf043fa672cb5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d61c83ef8bfdf043fa672cb5", +"motherduck": "d61c83ef8bfdf043fa672cb5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d61c83ef8bfdf043fa672cb5", "postgresql": "d61c83ef8bfdf043fa672cb5", "snowflake": "97a20344d811fb77c7690863", @@ -518,8 +518,8 @@ "databricks": "f7fca17b74cc50ae3de8c826", "duckdb": "f7fca17b74cc50ae3de8c826", "generic": "f7fca17b74cc50ae3de8c826", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f7fca17b74cc50ae3de8c826", +"motherduck": "f7fca17b74cc50ae3de8c826", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f7fca17b74cc50ae3de8c826", "postgresql": "f7fca17b74cc50ae3de8c826", "snowflake": "55155766823b6a3a5e882fc2", @@ -535,8 +535,8 @@ "databricks": "9a79cb58680bebe49009a72d", "duckdb": "9a79cb58680bebe49009a72d", "generic": "9a79cb58680bebe49009a72d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9a79cb58680bebe49009a72d", +"motherduck": "9a79cb58680bebe49009a72d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9a79cb58680bebe49009a72d", "postgresql": "9a79cb58680bebe49009a72d", "snowflake": "9a79cb58680bebe49009a72d", @@ -552,8 +552,8 @@ "databricks": "9bafb08d41fd1c208b5deca4", "duckdb": "9bafb08d41fd1c208b5deca4", "generic": "9bafb08d41fd1c208b5deca4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9bafb08d41fd1c208b5deca4", +"motherduck": "9bafb08d41fd1c208b5deca4", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9bafb08d41fd1c208b5deca4", "postgresql": "9bafb08d41fd1c208b5deca4", "snowflake": "9bafb08d41fd1c208b5deca4", @@ -569,8 +569,8 @@ "databricks": "de89632e277dbc8c97115419", "duckdb": "de89632e277dbc8c97115419", "generic": "de89632e277dbc8c97115419", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "de89632e277dbc8c97115419", +"motherduck": "de89632e277dbc8c97115419", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5abf4ec799a2ec32fb10da7e", "postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", @@ -586,8 +586,8 @@ "databricks": "2e60720c3255d112fcd0938a", "duckdb": "2e60720c3255d112fcd0938a", "generic": "2e60720c3255d112fcd0938a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2e60720c3255d112fcd0938a", +"motherduck": "2e60720c3255d112fcd0938a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2e60720c3255d112fcd0938a", "postgresql": "2e60720c3255d112fcd0938a", "snowflake": "2e60720c3255d112fcd0938a", @@ -603,8 +603,8 @@ "databricks": "669084d1ea704ae5b8ee05c2", "duckdb": "669084d1ea704ae5b8ee05c2", "generic": "669084d1ea704ae5b8ee05c2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "669084d1ea704ae5b8ee05c2", +"motherduck": "669084d1ea704ae5b8ee05c2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "93c1b5b5960e26479baf88d0", "postgresql": "93c1b5b5960e26479baf88d0", "snowflake": "669084d1ea704ae5b8ee05c2", @@ -620,8 +620,8 @@ "databricks": "c70e0a464fcbb5e5a1f7a8b7", "duckdb": "c9cc0c1ce817773e19f141b5", "generic": "c70e0a464fcbb5e5a1f7a8b7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c70e0a464fcbb5e5a1f7a8b7", +"motherduck": "c9cc0c1ce817773e19f141b5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c70e0a464fcbb5e5a1f7a8b7", "postgresql": "c70e0a464fcbb5e5a1f7a8b7", "snowflake": "89091c4ddfa26794a7c15ab6", @@ -637,8 +637,8 @@ "databricks": "319a9fbb02ed503eb114164b", "duckdb": "e060b5a5a8e9774b580d58d3", "generic": "e060b5a5a8e9774b580d58d3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e060b5a5a8e9774b580d58d3", +"motherduck": "e060b5a5a8e9774b580d58d3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e060b5a5a8e9774b580d58d3", "postgresql": "e060b5a5a8e9774b580d58d3", "snowflake": "e060b5a5a8e9774b580d58d3", @@ -654,8 +654,8 @@ "databricks": "3dbbe73ee4310c73030e8110", "duckdb": "3dbbe73ee4310c73030e8110", "generic": "3dbbe73ee4310c73030e8110", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3dbbe73ee4310c73030e8110", +"motherduck": "3dbbe73ee4310c73030e8110", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3dbbe73ee4310c73030e8110", "postgresql": "3dbbe73ee4310c73030e8110", "snowflake": "f43dac38f34a1c0618a3f812", @@ -671,8 +671,8 @@ "databricks": "8111d803c9182d0189c728e9", "duckdb": "8111d803c9182d0189c728e9", "generic": "8111d803c9182d0189c728e9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8111d803c9182d0189c728e9", +"motherduck": "8111d803c9182d0189c728e9", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8111d803c9182d0189c728e9", "postgresql": "8111d803c9182d0189c728e9", "snowflake": "8111d803c9182d0189c728e9", @@ -688,8 +688,8 @@ "databricks": "f7fca17b74cc50ae3de8c826", "duckdb": "f7fca17b74cc50ae3de8c826", "generic": "f7fca17b74cc50ae3de8c826", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f7fca17b74cc50ae3de8c826", +"motherduck": "f7fca17b74cc50ae3de8c826", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f7fca17b74cc50ae3de8c826", "postgresql": "f7fca17b74cc50ae3de8c826", "snowflake": "55155766823b6a3a5e882fc2", @@ -705,8 +705,8 @@ "databricks": "d79c3ac70093cb4cb531e71c", "duckdb": "d79c3ac70093cb4cb531e71c", "generic": "d79c3ac70093cb4cb531e71c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d79c3ac70093cb4cb531e71c", +"motherduck": "d79c3ac70093cb4cb531e71c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d79c3ac70093cb4cb531e71c", "postgresql": "d79c3ac70093cb4cb531e71c", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -722,8 +722,8 @@ "databricks": "4b356542044f1267fcfc487a", "duckdb": "4b356542044f1267fcfc487a", "generic": "4b356542044f1267fcfc487a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4b356542044f1267fcfc487a", +"motherduck": "4b356542044f1267fcfc487a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a5656d70e208254d1d3e9b4f", "postgresql": "a5656d70e208254d1d3e9b4f", "snowflake": "4b356542044f1267fcfc487a", @@ -739,8 +739,8 @@ "databricks": "f0ec29663977eb66dca07a0e", "duckdb": "f0ec29663977eb66dca07a0e", "generic": "f0ec29663977eb66dca07a0e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f0ec29663977eb66dca07a0e", +"motherduck": "f0ec29663977eb66dca07a0e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f0ec29663977eb66dca07a0e", "postgresql": "f0ec29663977eb66dca07a0e", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -756,8 +756,8 @@ "databricks": "08736485547ba339930697e9", "duckdb": "08736485547ba339930697e9", "generic": "08736485547ba339930697e9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "08736485547ba339930697e9", +"motherduck": "08736485547ba339930697e9", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "eecd452b4f5f3f4133d0bd0a", "postgresql": "eecd452b4f5f3f4133d0bd0a", "snowflake": "08736485547ba339930697e9", @@ -773,8 +773,8 @@ "databricks": "8a3f094f18fb4047f0febf59", "duckdb": "8a3f094f18fb4047f0febf59", "generic": "8a3f094f18fb4047f0febf59", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5c28019dce48a74708c90a76", +"motherduck": "8a3f094f18fb4047f0febf59", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8a3f094f18fb4047f0febf59", "postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "850deea990b52b82b36a9848", @@ -790,8 +790,8 @@ "databricks": "64fc778308b798a730b1d5c9", "duckdb": "64fc778308b798a730b1d5c9", "generic": "64fc778308b798a730b1d5c9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "64fc778308b798a730b1d5c9", +"motherduck": "64fc778308b798a730b1d5c9", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c8ca7a940ad228dd836cad6f", "postgresql": "c8ca7a940ad228dd836cad6f", "snowflake": "64fc778308b798a730b1d5c9", @@ -807,8 +807,8 @@ "databricks": "b46dfa976d5ba04f95bea32b", "duckdb": "b46dfa976d5ba04f95bea32b", "generic": "b46dfa976d5ba04f95bea32b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b46dfa976d5ba04f95bea32b", +"motherduck": "b46dfa976d5ba04f95bea32b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "34dcdfe3e3afe6140e1353e0", "postgresql": "34dcdfe3e3afe6140e1353e0", "snowflake": "b46dfa976d5ba04f95bea32b", @@ -824,8 +824,8 @@ "databricks": "20c9f38cf0da7cc6079a3a47", "duckdb": "20c9f38cf0da7cc6079a3a47", "generic": "20c9f38cf0da7cc6079a3a47", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "20c9f38cf0da7cc6079a3a47", +"motherduck": "20c9f38cf0da7cc6079a3a47", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "20c9f38cf0da7cc6079a3a47", "postgresql": "20c9f38cf0da7cc6079a3a47", "snowflake": "20c9f38cf0da7cc6079a3a47", @@ -841,8 +841,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1eb7fb4e8cf75773c5573562", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1eb7fb4e8cf75773c5573562", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -858,8 +858,8 @@ "databricks": "c50a316d091c821866e4bfba", "duckdb": "c50a316d091c821866e4bfba", "generic": "c50a316d091c821866e4bfba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -875,8 +875,8 @@ "databricks": "38b2ec94bf02be40007ce525", "duckdb": "38b2ec94bf02be40007ce525", "generic": "38b2ec94bf02be40007ce525", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "38b2ec94bf02be40007ce525", +"motherduck": "38b2ec94bf02be40007ce525", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "850deea990b52b82b36a9848", "postgresql": "850deea990b52b82b36a9848", "snowflake": "38b2ec94bf02be40007ce525", @@ -892,8 +892,8 @@ "databricks": "19a1a80ffc06169f32694de8", "duckdb": "19a1a80ffc06169f32694de8", "generic": "19a1a80ffc06169f32694de8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "19a1a80ffc06169f32694de8", +"motherduck": "19a1a80ffc06169f32694de8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "19a1a80ffc06169f32694de8", "postgresql": "19a1a80ffc06169f32694de8", "snowflake": "19a1a80ffc06169f32694de8", @@ -909,8 +909,8 @@ "databricks": "6ef74921b43664a98de3f7e4", "duckdb": "09a5861e271d932cd3a9a264", "generic": "6ef74921b43664a98de3f7e4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "09a5861e271d932cd3a9a264", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "cc685e113aac64bd76fbc3cb", "postgresql": "cc685e113aac64bd76fbc3cb", "snowflake": "6fe4c4c52324360189048430", @@ -926,8 +926,8 @@ "databricks": "46ec3e19e05a6cb29854ccac", "duckdb": "35e216b6921bf843994f2eea", "generic": "46ec3e19e05a6cb29854ccac", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "46ec3e19e05a6cb29854ccac", +"motherduck": "35e216b6921bf843994f2eea", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "46ec3e19e05a6cb29854ccac", "postgresql": "46ec3e19e05a6cb29854ccac", "snowflake": "46ec3e19e05a6cb29854ccac", @@ -943,8 +943,8 @@ "databricks": "de89632e277dbc8c97115419", "duckdb": "5abf4ec799a2ec32fb10da7e", "generic": "de89632e277dbc8c97115419", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "de89632e277dbc8c97115419", +"motherduck": "5abf4ec799a2ec32fb10da7e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5abf4ec799a2ec32fb10da7e", "postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", @@ -960,8 +960,8 @@ "databricks": "1950e32420d1d40d41ffae64", "duckdb": "1950e32420d1d40d41ffae64", "generic": "1950e32420d1d40d41ffae64", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1950e32420d1d40d41ffae64", +"motherduck": "1950e32420d1d40d41ffae64", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1950e32420d1d40d41ffae64", "postgresql": "1950e32420d1d40d41ffae64", "snowflake": "1950e32420d1d40d41ffae64", @@ -977,8 +977,8 @@ "databricks": "3f71ab5f3c1c040da96f2cec", "duckdb": "3f71ab5f3c1c040da96f2cec", "generic": "3f71ab5f3c1c040da96f2cec", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3f71ab5f3c1c040da96f2cec", +"motherduck": "3f71ab5f3c1c040da96f2cec", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3f71ab5f3c1c040da96f2cec", "postgresql": "3f71ab5f3c1c040da96f2cec", "snowflake": "3f71ab5f3c1c040da96f2cec", @@ -994,8 +994,8 @@ "databricks": "9e4a50a8aab648a9737f7d0c", "duckdb": "9e4a50a8aab648a9737f7d0c", "generic": "9e4a50a8aab648a9737f7d0c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9e4a50a8aab648a9737f7d0c", +"motherduck": "9e4a50a8aab648a9737f7d0c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "36d29258901a4d8c296baa68", "postgresql": "36d29258901a4d8c296baa68", "snowflake": "9e4a50a8aab648a9737f7d0c", @@ -1011,8 +1011,8 @@ "databricks": "461923a0d34defd0c726d6e5", "duckdb": "461923a0d34defd0c726d6e5", "generic": "461923a0d34defd0c726d6e5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "461923a0d34defd0c726d6e5", +"motherduck": "461923a0d34defd0c726d6e5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "461923a0d34defd0c726d6e5", "postgresql": "461923a0d34defd0c726d6e5", "snowflake": "461923a0d34defd0c726d6e5", @@ -1028,8 +1028,8 @@ "databricks": "2bfa352725f9d8a91250f75e", "duckdb": "2bfa352725f9d8a91250f75e", "generic": "2bfa352725f9d8a91250f75e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2bfa352725f9d8a91250f75e", +"motherduck": "2bfa352725f9d8a91250f75e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2bfa352725f9d8a91250f75e", "postgresql": "2bfa352725f9d8a91250f75e", "snowflake": "2bfa352725f9d8a91250f75e", @@ -1045,8 +1045,8 @@ "databricks": "8441d00030df06ad6a5534d3", "duckdb": "8441d00030df06ad6a5534d3", "generic": "8441d00030df06ad6a5534d3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8441d00030df06ad6a5534d3", +"motherduck": "8441d00030df06ad6a5534d3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8441d00030df06ad6a5534d3", "postgresql": "8441d00030df06ad6a5534d3", "snowflake": "97a20344d811fb77c7690863", @@ -1062,8 +1062,8 @@ "databricks": "e46d4be44ab9e500e9190d26", "duckdb": "e46d4be44ab9e500e9190d26", "generic": "e46d4be44ab9e500e9190d26", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e46d4be44ab9e500e9190d26", +"motherduck": "e46d4be44ab9e500e9190d26", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "251aba3fef503ce64556c701", "postgresql": "251aba3fef503ce64556c701", "snowflake": "e46d4be44ab9e500e9190d26", @@ -1079,8 +1079,8 @@ "databricks": "a7120d1b1abcb844c87a968a", "duckdb": "a7120d1b1abcb844c87a968a", "generic": "a7120d1b1abcb844c87a968a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a7120d1b1abcb844c87a968a", +"motherduck": "a7120d1b1abcb844c87a968a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a7120d1b1abcb844c87a968a", "postgresql": "a7120d1b1abcb844c87a968a", "snowflake": "a7120d1b1abcb844c87a968a", @@ -1096,8 +1096,8 @@ "databricks": "cc2a8dbf7a9806988a7b5067", "duckdb": "cc2a8dbf7a9806988a7b5067", "generic": "cc2a8dbf7a9806988a7b5067", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "cc2a8dbf7a9806988a7b5067", +"motherduck": "cc2a8dbf7a9806988a7b5067", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "cc2a8dbf7a9806988a7b5067", "postgresql": "cc2a8dbf7a9806988a7b5067", "snowflake": "cc2a8dbf7a9806988a7b5067", @@ -1113,8 +1113,8 @@ "databricks": "5d4d5145235f17764810d03f", "duckdb": "5d4d5145235f17764810d03f", "generic": "5d4d5145235f17764810d03f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5d4d5145235f17764810d03f", +"motherduck": "5d4d5145235f17764810d03f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5d4d5145235f17764810d03f", "postgresql": "5d4d5145235f17764810d03f", "snowflake": "5d4d5145235f17764810d03f", @@ -1130,8 +1130,8 @@ "databricks": "2427f5522575cdc3d6a74e4c", "duckdb": "2427f5522575cdc3d6a74e4c", "generic": "2427f5522575cdc3d6a74e4c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2427f5522575cdc3d6a74e4c", +"motherduck": "2427f5522575cdc3d6a74e4c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2427f5522575cdc3d6a74e4c", "postgresql": "2427f5522575cdc3d6a74e4c", "snowflake": "2427f5522575cdc3d6a74e4c", @@ -1147,8 +1147,8 @@ "databricks": "2bfa352725f9d8a91250f75e", "duckdb": "2bfa352725f9d8a91250f75e", "generic": "2bfa352725f9d8a91250f75e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2bfa352725f9d8a91250f75e", +"motherduck": "2bfa352725f9d8a91250f75e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2bfa352725f9d8a91250f75e", "postgresql": "2bfa352725f9d8a91250f75e", "snowflake": "2bfa352725f9d8a91250f75e", @@ -1164,8 +1164,8 @@ "databricks": "12b8a08a67c6c5ca887183e3", "duckdb": "97d3bfe2b726a642c0011696", "generic": "5a48ec343f2513c62588cb55", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5a48ec343f2513c62588cb55", +"motherduck": "97d3bfe2b726a642c0011696", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5a48ec343f2513c62588cb55", "postgresql": "5a48ec343f2513c62588cb55", "snowflake": "582c76388cd6468a1e793692", @@ -1181,8 +1181,8 @@ "databricks": "8441d00030df06ad6a5534d3", "duckdb": "8441d00030df06ad6a5534d3", "generic": "8441d00030df06ad6a5534d3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8441d00030df06ad6a5534d3", +"motherduck": "8441d00030df06ad6a5534d3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8441d00030df06ad6a5534d3", "postgresql": "8441d00030df06ad6a5534d3", "snowflake": "97a20344d811fb77c7690863", @@ -1198,8 +1198,8 @@ "databricks": "b5c4d5044b2a6e70633f0d0a", "duckdb": "b5c4d5044b2a6e70633f0d0a", "generic": "b5c4d5044b2a6e70633f0d0a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b5c4d5044b2a6e70633f0d0a", +"motherduck": "b5c4d5044b2a6e70633f0d0a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b5c4d5044b2a6e70633f0d0a", "postgresql": "b5c4d5044b2a6e70633f0d0a", "snowflake": "8ff614b7306c0edbe36ce00b", @@ -1215,8 +1215,8 @@ "databricks": "ac984ee30077ed3bb3c7c9d5", "duckdb": "7a8df655ee308ea4f929b9fd", "generic": "7a8df655ee308ea4f929b9fd", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "7a8df655ee308ea4f929b9fd", +"motherduck": "7a8df655ee308ea4f929b9fd", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7a8df655ee308ea4f929b9fd", "postgresql": "7a8df655ee308ea4f929b9fd", "snowflake": "7a8df655ee308ea4f929b9fd", @@ -1232,8 +1232,8 @@ "databricks": "804fec6876d0b43935790142", "duckdb": "804fec6876d0b43935790142", "generic": "804fec6876d0b43935790142", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "804fec6876d0b43935790142", +"motherduck": "804fec6876d0b43935790142", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "191fc59c754706512337edc1", "postgresql": "191fc59c754706512337edc1", "snowflake": "804fec6876d0b43935790142", @@ -1249,8 +1249,8 @@ "databricks": "8a4c61c12f9eaa564d1c19a1", "duckdb": "8a4c61c12f9eaa564d1c19a1", "generic": "8a4c61c12f9eaa564d1c19a1", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8a4c61c12f9eaa564d1c19a1", +"motherduck": "8a4c61c12f9eaa564d1c19a1", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8a4c61c12f9eaa564d1c19a1", "postgresql": "8a4c61c12f9eaa564d1c19a1", "snowflake": "8a4c61c12f9eaa564d1c19a1", @@ -1266,8 +1266,8 @@ "databricks": "fcb2bd160300a30539bd5398", "duckdb": "fcb2bd160300a30539bd5398", "generic": "fcb2bd160300a30539bd5398", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fcb2bd160300a30539bd5398", +"motherduck": "fcb2bd160300a30539bd5398", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "fcb2bd160300a30539bd5398", "postgresql": "fcb2bd160300a30539bd5398", "snowflake": "fcb2bd160300a30539bd5398", @@ -1283,8 +1283,8 @@ "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fe23594c781e79cbb0c8927a", +"motherduck": "fe23594c781e79cbb0c8927a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8a3f094f18fb4047f0febf59", "postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", @@ -1300,8 +1300,8 @@ "databricks": "d3bcf10e00645b12a802c7d0", "duckdb": "c0eec31620dfc2ac0028173d", "generic": "d3bcf10e00645b12a802c7d0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "c0eec31620dfc2ac0028173d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c0eec31620dfc2ac0028173d", "postgresql": "c0eec31620dfc2ac0028173d", "snowflake": "d379c0ee4dad89e570b7329e", @@ -1317,8 +1317,8 @@ "databricks": "06aec7cca4b30489405665e2", "duckdb": "06aec7cca4b30489405665e2", "generic": "06aec7cca4b30489405665e2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "06aec7cca4b30489405665e2", +"motherduck": "06aec7cca4b30489405665e2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "06aec7cca4b30489405665e2", "postgresql": "06aec7cca4b30489405665e2", "snowflake": "06aec7cca4b30489405665e2", @@ -1334,8 +1334,8 @@ "databricks": "76b7de42fb46036e5c9279bf", "duckdb": "76b7de42fb46036e5c9279bf", "generic": "76b7de42fb46036e5c9279bf", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "76b7de42fb46036e5c9279bf", +"motherduck": "76b7de42fb46036e5c9279bf", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "76b7de42fb46036e5c9279bf", "postgresql": "76b7de42fb46036e5c9279bf", "snowflake": "76b7de42fb46036e5c9279bf", @@ -1351,8 +1351,8 @@ "databricks": "93c1b5b5960e26479baf88d0", "duckdb": "93c1b5b5960e26479baf88d0", "generic": "93c1b5b5960e26479baf88d0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "93c1b5b5960e26479baf88d0", +"motherduck": "93c1b5b5960e26479baf88d0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "93c1b5b5960e26479baf88d0", "postgresql": "93c1b5b5960e26479baf88d0", "snowflake": "93c1b5b5960e26479baf88d0", @@ -1368,8 +1368,8 @@ "databricks": "9db9d9c0f19fb71af06e1044", "duckdb": "3b3de43279bf3f0706595e4a", "generic": "9db9d9c0f19fb71af06e1044", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "3b3de43279bf3f0706595e4a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3b3de43279bf3f0706595e4a", "postgresql": "3b3de43279bf3f0706595e4a", "snowflake": "0b6d401f07331fac8353bbd3", @@ -1385,8 +1385,8 @@ "databricks": "6965b3d5426a0af9750f9a03", "duckdb": "6965b3d5426a0af9750f9a03", "generic": "6965b3d5426a0af9750f9a03", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6965b3d5426a0af9750f9a03", +"motherduck": "6965b3d5426a0af9750f9a03", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "6965b3d5426a0af9750f9a03", "postgresql": "6965b3d5426a0af9750f9a03", "snowflake": "6965b3d5426a0af9750f9a03", @@ -1402,8 +1402,8 @@ "databricks": "7aa59eed1bba74a9dee48e59", "duckdb": "7aa59eed1bba74a9dee48e59", "generic": "7aa59eed1bba74a9dee48e59", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "7aa59eed1bba74a9dee48e59", +"motherduck": "7aa59eed1bba74a9dee48e59", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7aa59eed1bba74a9dee48e59", "postgresql": "7aa59eed1bba74a9dee48e59", "snowflake": "7aa59eed1bba74a9dee48e59", @@ -1419,8 +1419,8 @@ "databricks": "66e7166890ccedeaad6c241a", "duckdb": "66e7166890ccedeaad6c241a", "generic": "66e7166890ccedeaad6c241a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "66e7166890ccedeaad6c241a", +"motherduck": "66e7166890ccedeaad6c241a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "66e7166890ccedeaad6c241a", "postgresql": "66e7166890ccedeaad6c241a", "snowflake": "66e7166890ccedeaad6c241a", @@ -1436,8 +1436,8 @@ "databricks": "9aff7fcc7d550e1a8d105471", "duckdb": "9aff7fcc7d550e1a8d105471", "generic": "9aff7fcc7d550e1a8d105471", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9aff7fcc7d550e1a8d105471", +"motherduck": "9aff7fcc7d550e1a8d105471", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9aff7fcc7d550e1a8d105471", "postgresql": "9aff7fcc7d550e1a8d105471", "snowflake": "9aff7fcc7d550e1a8d105471", @@ -1453,8 +1453,8 @@ "databricks": "b561884132dbe7e42b719110", "duckdb": "b561884132dbe7e42b719110", "generic": "b561884132dbe7e42b719110", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b561884132dbe7e42b719110", +"motherduck": "b561884132dbe7e42b719110", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b561884132dbe7e42b719110", "postgresql": "b561884132dbe7e42b719110", "snowflake": "b561884132dbe7e42b719110", @@ -1470,8 +1470,8 @@ "databricks": "fb6eefdc9bed918e3ac82197", "duckdb": "fb6eefdc9bed918e3ac82197", "generic": "fb6eefdc9bed918e3ac82197", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fb6eefdc9bed918e3ac82197", +"motherduck": "fb6eefdc9bed918e3ac82197", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "4d3368f4985b5822bc4239af", "postgresql": "4d3368f4985b5822bc4239af", "snowflake": "fb6eefdc9bed918e3ac82197", @@ -1487,8 +1487,8 @@ "databricks": "47e0816fecdd9e015d7aeb79", "duckdb": "aa7afbd57560be67e5221a3b", "generic": "47e0816fecdd9e015d7aeb79", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "aa7afbd57560be67e5221a3b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "aa7afbd57560be67e5221a3b", "postgresql": "aa7afbd57560be67e5221a3b", "snowflake": "e6f36041b1d9fb8fe3a18cbb", @@ -1504,8 +1504,8 @@ "databricks": "0a22fc4bfa30bc1953d41de6", "duckdb": "ee07062f9d57cfdfb20f327b", "generic": "ee07062f9d57cfdfb20f327b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ee07062f9d57cfdfb20f327b", +"motherduck": "ee07062f9d57cfdfb20f327b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0a9a262e0d175ab0eb0865e2", "postgresql": "0a9a262e0d175ab0eb0865e2", "snowflake": "0f4d95882eb1debabb5a887b", @@ -1521,8 +1521,8 @@ "databricks": "0c43dc837a50933667588ef1", "duckdb": "0c43dc837a50933667588ef1", "generic": "0c43dc837a50933667588ef1", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0c43dc837a50933667588ef1", +"motherduck": "0c43dc837a50933667588ef1", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0c43dc837a50933667588ef1", "postgresql": "0c43dc837a50933667588ef1", "snowflake": "0c43dc837a50933667588ef1", @@ -1538,8 +1538,8 @@ "databricks": "d2c97d221599d038d9875873", "duckdb": "d2c97d221599d038d9875873", "generic": "d2c97d221599d038d9875873", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d2c97d221599d038d9875873", +"motherduck": "d2c97d221599d038d9875873", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "55155766823b6a3a5e882fc2", "postgresql": "55155766823b6a3a5e882fc2", "snowflake": "d2c97d221599d038d9875873", @@ -1555,8 +1555,8 @@ "databricks": "e2879cf4770e7f48d2dd704a", "duckdb": "e2879cf4770e7f48d2dd704a", "generic": "e2879cf4770e7f48d2dd704a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e2879cf4770e7f48d2dd704a", +"motherduck": "e2879cf4770e7f48d2dd704a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2d4aea8dec5521e258596f3d", "postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", @@ -1572,8 +1572,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1eb7fb4e8cf75773c5573562", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1eb7fb4e8cf75773c5573562", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -1589,8 +1589,8 @@ "databricks": "aab0389e2b5470136660bf8a", "duckdb": "aab0389e2b5470136660bf8a", "generic": "aab0389e2b5470136660bf8a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "aab0389e2b5470136660bf8a", +"motherduck": "aab0389e2b5470136660bf8a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f16105cbc61d547e6c08efc8", "postgresql": "f16105cbc61d547e6c08efc8", "snowflake": "97a20344d811fb77c7690863", @@ -1606,8 +1606,8 @@ "databricks": "3f71ab5f3c1c040da96f2cec", "duckdb": "3f71ab5f3c1c040da96f2cec", "generic": "3f71ab5f3c1c040da96f2cec", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3f71ab5f3c1c040da96f2cec", +"motherduck": "3f71ab5f3c1c040da96f2cec", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3f71ab5f3c1c040da96f2cec", "postgresql": "3f71ab5f3c1c040da96f2cec", "snowflake": "3f71ab5f3c1c040da96f2cec", @@ -1623,8 +1623,8 @@ "databricks": "0c4610f182f18bf41d306e01", "duckdb": "0c4610f182f18bf41d306e01", "generic": "0c4610f182f18bf41d306e01", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0c4610f182f18bf41d306e01", +"motherduck": "0c4610f182f18bf41d306e01", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0c4610f182f18bf41d306e01", "postgresql": "0c4610f182f18bf41d306e01", "snowflake": "0c4610f182f18bf41d306e01", @@ -1640,8 +1640,8 @@ "databricks": "ab5ad6180652b3b00d1f5b01", "duckdb": "ab5ad6180652b3b00d1f5b01", "generic": "ab5ad6180652b3b00d1f5b01", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ab5ad6180652b3b00d1f5b01", +"motherduck": "ab5ad6180652b3b00d1f5b01", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ab5ad6180652b3b00d1f5b01", "postgresql": "ab5ad6180652b3b00d1f5b01", "snowflake": "f43dac38f34a1c0618a3f812", @@ -1657,8 +1657,8 @@ "databricks": "e3bc1b8bad22d8104417b00f", "duckdb": "1d8b945fd793ecdcacb90c21", "generic": "1d8b945fd793ecdcacb90c21", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1d8b945fd793ecdcacb90c21", +"motherduck": "1d8b945fd793ecdcacb90c21", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3efe8bb57c98fcbe74096b3c", "postgresql": "3efe8bb57c98fcbe74096b3c", "snowflake": "1d8b945fd793ecdcacb90c21", @@ -1674,8 +1674,8 @@ "databricks": "5d25d5e13f81700741695ed4", "duckdb": "5d25d5e13f81700741695ed4", "generic": "5d25d5e13f81700741695ed4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5d25d5e13f81700741695ed4", +"motherduck": "5d25d5e13f81700741695ed4", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "62131fb5b065f5de2e74a0f1", "postgresql": "62131fb5b065f5de2e74a0f1", "snowflake": "5d25d5e13f81700741695ed4", @@ -1691,8 +1691,8 @@ "databricks": "adbb06204fd66d8071c3dbc6", "duckdb": "29739921db4d47abc1ac18c7", "generic": "29739921db4d47abc1ac18c7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "29739921db4d47abc1ac18c7", +"motherduck": "29739921db4d47abc1ac18c7", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "29739921db4d47abc1ac18c7", "postgresql": "29739921db4d47abc1ac18c7", "snowflake": "216a7f0ead009a087eea4e1c", @@ -1708,8 +1708,8 @@ "databricks": "e2879cf4770e7f48d2dd704a", "duckdb": "e2879cf4770e7f48d2dd704a", "generic": "e2879cf4770e7f48d2dd704a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e2879cf4770e7f48d2dd704a", +"motherduck": "e2879cf4770e7f48d2dd704a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2d4aea8dec5521e258596f3d", "postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", @@ -1725,8 +1725,8 @@ "databricks": "ee47e1424cb01deb0cf51760", "duckdb": "ee47e1424cb01deb0cf51760", "generic": "ee47e1424cb01deb0cf51760", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ee47e1424cb01deb0cf51760", +"motherduck": "ee47e1424cb01deb0cf51760", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3c1521654088dd54d0dc9ead", "postgresql": "3c1521654088dd54d0dc9ead", "snowflake": "ee47e1424cb01deb0cf51760", @@ -1742,8 +1742,8 @@ "databricks": "b4c316e81db1cb6a277b5d27", "duckdb": "1655128a72b1db9af6a89df3", "generic": "1655128a72b1db9af6a89df3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1655128a72b1db9af6a89df3", +"motherduck": "1655128a72b1db9af6a89df3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1655128a72b1db9af6a89df3", "postgresql": "1655128a72b1db9af6a89df3", "snowflake": "1655128a72b1db9af6a89df3", @@ -1759,8 +1759,8 @@ "databricks": "3f998a9ab68d8b2514840489", "duckdb": "f2ed52f6bf46b4db9343551c", "generic": "3f998a9ab68d8b2514840489", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3f998a9ab68d8b2514840489", +"motherduck": "f2ed52f6bf46b4db9343551c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7ce41cc5375b618de22b6a4e", "postgresql": "7ce41cc5375b618de22b6a4e", "snowflake": "f2ed52f6bf46b4db9343551c", @@ -1776,8 +1776,8 @@ "databricks": "b46dfa976d5ba04f95bea32b", "duckdb": "b46dfa976d5ba04f95bea32b", "generic": "b46dfa976d5ba04f95bea32b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b46dfa976d5ba04f95bea32b", +"motherduck": "b46dfa976d5ba04f95bea32b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "34dcdfe3e3afe6140e1353e0", "postgresql": "34dcdfe3e3afe6140e1353e0", "snowflake": "b46dfa976d5ba04f95bea32b", @@ -1793,8 +1793,8 @@ "databricks": "c50a316d091c821866e4bfba", "duckdb": "c50a316d091c821866e4bfba", "generic": "c50a316d091c821866e4bfba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -1810,8 +1810,8 @@ "databricks": "3e611643f0beab4953c4e09b", "duckdb": "3e611643f0beab4953c4e09b", "generic": "3e611643f0beab4953c4e09b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3e611643f0beab4953c4e09b", +"motherduck": "3e611643f0beab4953c4e09b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "cae477ce59c72869f37a9a40", "postgresql": "cae477ce59c72869f37a9a40", "snowflake": "3e611643f0beab4953c4e09b", @@ -1827,8 +1827,8 @@ "databricks": "e2879cf4770e7f48d2dd704a", "duckdb": "e2879cf4770e7f48d2dd704a", "generic": "e2879cf4770e7f48d2dd704a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e2879cf4770e7f48d2dd704a", +"motherduck": "e2879cf4770e7f48d2dd704a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2d4aea8dec5521e258596f3d", "postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", @@ -1844,8 +1844,8 @@ "databricks": "62368fc15cefd5807807b209", "duckdb": "62368fc15cefd5807807b209", "generic": "62368fc15cefd5807807b209", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "62368fc15cefd5807807b209", +"motherduck": "62368fc15cefd5807807b209", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "62368fc15cefd5807807b209", "postgresql": "62368fc15cefd5807807b209", "snowflake": "62368fc15cefd5807807b209", @@ -1861,8 +1861,8 @@ "databricks": "17a6d1702d6997aa4e447f46", "duckdb": "755657fbcced1de4551fb370", "generic": "17a6d1702d6997aa4e447f46", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "17a6d1702d6997aa4e447f46", +"motherduck": "755657fbcced1de4551fb370", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "17a6d1702d6997aa4e447f46", "postgresql": "17a6d1702d6997aa4e447f46", "snowflake": "17a6d1702d6997aa4e447f46", @@ -1878,8 +1878,8 @@ "databricks": "8d66732c326e570c1efcd995", "duckdb": "8d66732c326e570c1efcd995", "generic": "8d66732c326e570c1efcd995", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8d66732c326e570c1efcd995", +"motherduck": "8d66732c326e570c1efcd995", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8d66732c326e570c1efcd995", "postgresql": "8d66732c326e570c1efcd995", "snowflake": "8d66732c326e570c1efcd995", @@ -1895,8 +1895,8 @@ "databricks": "caf173733defe796376d4efe", "duckdb": "caf173733defe796376d4efe", "generic": "caf173733defe796376d4efe", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "caf173733defe796376d4efe", +"motherduck": "caf173733defe796376d4efe", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "caf173733defe796376d4efe", "postgresql": "caf173733defe796376d4efe", "snowflake": "caf173733defe796376d4efe", @@ -1912,8 +1912,8 @@ "databricks": "de89632e277dbc8c97115419", "duckdb": "de89632e277dbc8c97115419", "generic": "de89632e277dbc8c97115419", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "de89632e277dbc8c97115419", +"motherduck": "de89632e277dbc8c97115419", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5abf4ec799a2ec32fb10da7e", "postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", @@ -1929,8 +1929,8 @@ "databricks": "14cd6e78d75f61b2bd1bbd71", "duckdb": "14cd6e78d75f61b2bd1bbd71", "generic": "14cd6e78d75f61b2bd1bbd71", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "14cd6e78d75f61b2bd1bbd71", +"motherduck": "14cd6e78d75f61b2bd1bbd71", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "14cd6e78d75f61b2bd1bbd71", "postgresql": "14cd6e78d75f61b2bd1bbd71", "snowflake": "14cd6e78d75f61b2bd1bbd71", @@ -1946,8 +1946,8 @@ "databricks": "79278352c17c9de0f5a28af5", "duckdb": "79278352c17c9de0f5a28af5", "generic": "79278352c17c9de0f5a28af5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "79278352c17c9de0f5a28af5", +"motherduck": "79278352c17c9de0f5a28af5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "79278352c17c9de0f5a28af5", "postgresql": "79278352c17c9de0f5a28af5", "snowflake": "97a20344d811fb77c7690863", @@ -1963,8 +1963,8 @@ "databricks": "cf5e019d9de8b46b75fdfdb6", "duckdb": "cf5e019d9de8b46b75fdfdb6", "generic": "cf5e019d9de8b46b75fdfdb6", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "cf5e019d9de8b46b75fdfdb6", +"motherduck": "cf5e019d9de8b46b75fdfdb6", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "cf5e019d9de8b46b75fdfdb6", "postgresql": "cf5e019d9de8b46b75fdfdb6", "snowflake": "cf5e019d9de8b46b75fdfdb6", @@ -1980,8 +1980,8 @@ "databricks": "4f8af4650ce9c18ed0cb0bca", "duckdb": "4f8af4650ce9c18ed0cb0bca", "generic": "4f8af4650ce9c18ed0cb0bca", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4f8af4650ce9c18ed0cb0bca", +"motherduck": "4f8af4650ce9c18ed0cb0bca", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "4f8af4650ce9c18ed0cb0bca", "postgresql": "4f8af4650ce9c18ed0cb0bca", "snowflake": "4f8af4650ce9c18ed0cb0bca", @@ -1997,8 +1997,8 @@ "databricks": "cbd04e906c540e91d7c8ea88", "duckdb": "cbd04e906c540e91d7c8ea88", "generic": "cbd04e906c540e91d7c8ea88", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "cbd04e906c540e91d7c8ea88", +"motherduck": "cbd04e906c540e91d7c8ea88", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "cbd04e906c540e91d7c8ea88", "postgresql": "cbd04e906c540e91d7c8ea88", "snowflake": "cbd04e906c540e91d7c8ea88", @@ -2014,8 +2014,8 @@ "databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", "generic": "fb4b6f5101f973d16ea8d1f3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fb4b6f5101f973d16ea8d1f3", +"motherduck": "70fb4728c3adcf6dbe4975d5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", @@ -2031,8 +2031,8 @@ "databricks": "8b8d5f75ba6dec68b4cebc6b", "duckdb": "8b8d5f75ba6dec68b4cebc6b", "generic": "8b8d5f75ba6dec68b4cebc6b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8b8d5f75ba6dec68b4cebc6b", +"motherduck": "8b8d5f75ba6dec68b4cebc6b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7914189ff18b0e70ddf7d042", "postgresql": "7914189ff18b0e70ddf7d042", "snowflake": "8b8d5f75ba6dec68b4cebc6b", @@ -2048,8 +2048,8 @@ "databricks": "aae2b1fe6f2d9807ecde412b", "duckdb": "aae2b1fe6f2d9807ecde412b", "generic": "aae2b1fe6f2d9807ecde412b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "aae2b1fe6f2d9807ecde412b", +"motherduck": "aae2b1fe6f2d9807ecde412b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "aae2b1fe6f2d9807ecde412b", "postgresql": "aae2b1fe6f2d9807ecde412b", "snowflake": "aae2b1fe6f2d9807ecde412b", @@ -2065,8 +2065,8 @@ "databricks": "38b2ec94bf02be40007ce525", "duckdb": "8a3f094f18fb4047f0febf59", "generic": "38b2ec94bf02be40007ce525", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "38b2ec94bf02be40007ce525", +"motherduck": "8a3f094f18fb4047f0febf59", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "850deea990b52b82b36a9848", "postgresql": "850deea990b52b82b36a9848", "snowflake": "38b2ec94bf02be40007ce525", @@ -2082,8 +2082,8 @@ "databricks": "036dd6864cf4939023240245", "duckdb": "f30e987a1e4e17305f6863ad", "generic": "036dd6864cf4939023240245", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "f30e987a1e4e17305f6863ad", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f30e987a1e4e17305f6863ad", "postgresql": "f30e987a1e4e17305f6863ad", "snowflake": "ff9e7821f3892ba4ddf70a84", @@ -2099,8 +2099,8 @@ "databricks": "bce60580ef3a1dc4ce569fff", "duckdb": "bce60580ef3a1dc4ce569fff", "generic": "bce60580ef3a1dc4ce569fff", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "bce60580ef3a1dc4ce569fff", +"motherduck": "bce60580ef3a1dc4ce569fff", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "eee236ea1427bdddbaf088ac", "postgresql": "eee236ea1427bdddbaf088ac", "snowflake": "bce60580ef3a1dc4ce569fff", @@ -2116,8 +2116,8 @@ "databricks": "e3bc1b8bad22d8104417b00f", "duckdb": "19e127c23b4598727a2d8ceb", "generic": "19e127c23b4598727a2d8ceb", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "aed0419e7f2ce7c0a8892248", +"motherduck": "19e127c23b4598727a2d8ceb", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "111af35848fe500ec2ae54b2", "postgresql": "111af35848fe500ec2ae54b2", "snowflake": "aed0419e7f2ce7c0a8892248", @@ -2133,8 +2133,8 @@ "databricks": "558da38ec4d9d5d6c72f60cf", "duckdb": "558da38ec4d9d5d6c72f60cf", "generic": "558da38ec4d9d5d6c72f60cf", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "558da38ec4d9d5d6c72f60cf", +"motherduck": "558da38ec4d9d5d6c72f60cf", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "558da38ec4d9d5d6c72f60cf", "postgresql": "558da38ec4d9d5d6c72f60cf", "snowflake": "558da38ec4d9d5d6c72f60cf", @@ -2150,8 +2150,8 @@ "databricks": "f43dac38f34a1c0618a3f812", "duckdb": "f43dac38f34a1c0618a3f812", "generic": "f43dac38f34a1c0618a3f812", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f43dac38f34a1c0618a3f812", +"motherduck": "f43dac38f34a1c0618a3f812", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f43dac38f34a1c0618a3f812", "postgresql": "f43dac38f34a1c0618a3f812", "snowflake": "f43dac38f34a1c0618a3f812", @@ -2167,8 +2167,8 @@ "databricks": "3dbbe73ee4310c73030e8110", "duckdb": "3dbbe73ee4310c73030e8110", "generic": "3dbbe73ee4310c73030e8110", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3dbbe73ee4310c73030e8110", +"motherduck": "3dbbe73ee4310c73030e8110", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3dbbe73ee4310c73030e8110", "postgresql": "3dbbe73ee4310c73030e8110", "snowflake": "f43dac38f34a1c0618a3f812", @@ -2184,8 +2184,8 @@ "databricks": "c6106f30f23f392e17eadef4", "duckdb": "c6106f30f23f392e17eadef4", "generic": "c6106f30f23f392e17eadef4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c6106f30f23f392e17eadef4", +"motherduck": "c6106f30f23f392e17eadef4", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f43dac38f34a1c0618a3f812", "postgresql": "f43dac38f34a1c0618a3f812", "snowflake": "c6106f30f23f392e17eadef4", @@ -2201,8 +2201,8 @@ "databricks": "cfb4a49a57197b0df1b2e905", "duckdb": "cfb4a49a57197b0df1b2e905", "generic": "cfb4a49a57197b0df1b2e905", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "cfb4a49a57197b0df1b2e905", +"motherduck": "cfb4a49a57197b0df1b2e905", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "cfb4a49a57197b0df1b2e905", "postgresql": "cfb4a49a57197b0df1b2e905", "snowflake": "cfb4a49a57197b0df1b2e905", @@ -2218,8 +2218,8 @@ "databricks": "8790273499dae4e6cbd2d034", "duckdb": "8790273499dae4e6cbd2d034", "generic": "8790273499dae4e6cbd2d034", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8790273499dae4e6cbd2d034", +"motherduck": "8790273499dae4e6cbd2d034", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8790273499dae4e6cbd2d034", "postgresql": "8790273499dae4e6cbd2d034", "snowflake": "9aff7fcc7d550e1a8d105471", @@ -2235,8 +2235,8 @@ "databricks": "e8605878c6fffc044556a858", "duckdb": "e8605878c6fffc044556a858", "generic": "e8605878c6fffc044556a858", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e8605878c6fffc044556a858", +"motherduck": "e8605878c6fffc044556a858", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e8605878c6fffc044556a858", "postgresql": "e8605878c6fffc044556a858", "snowflake": "e8605878c6fffc044556a858", @@ -2252,8 +2252,8 @@ "databricks": "355fafbc65133795e6c5e43b", "duckdb": "355fafbc65133795e6c5e43b", "generic": "355fafbc65133795e6c5e43b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "355fafbc65133795e6c5e43b", +"motherduck": "355fafbc65133795e6c5e43b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "355fafbc65133795e6c5e43b", "postgresql": "355fafbc65133795e6c5e43b", "snowflake": "355fafbc65133795e6c5e43b", @@ -2269,8 +2269,8 @@ "databricks": "0dd2a0d5610068e51e271b9f", "duckdb": "0dd2a0d5610068e51e271b9f", "generic": "0dd2a0d5610068e51e271b9f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0dd2a0d5610068e51e271b9f", +"motherduck": "0dd2a0d5610068e51e271b9f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b4df93b58afcfa82db9f544e", "postgresql": "b4df93b58afcfa82db9f544e", "snowflake": "0dd2a0d5610068e51e271b9f", @@ -2286,8 +2286,8 @@ "databricks": "0703bf71a54942bdaa28406e", "duckdb": "0703bf71a54942bdaa28406e", "generic": "0703bf71a54942bdaa28406e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0703bf71a54942bdaa28406e", +"motherduck": "0703bf71a54942bdaa28406e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0703bf71a54942bdaa28406e", "postgresql": "0703bf71a54942bdaa28406e", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -2303,8 +2303,8 @@ "databricks": "6212562845efbc753cb47471", "duckdb": "6212562845efbc753cb47471", "generic": "6212562845efbc753cb47471", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6212562845efbc753cb47471", +"motherduck": "6212562845efbc753cb47471", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "6212562845efbc753cb47471", "postgresql": "6212562845efbc753cb47471", "snowflake": "6212562845efbc753cb47471", @@ -2320,8 +2320,8 @@ "databricks": "e998cd8f0f74af0526b75ff0", "duckdb": "e998cd8f0f74af0526b75ff0", "generic": "e998cd8f0f74af0526b75ff0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e998cd8f0f74af0526b75ff0", +"motherduck": "e998cd8f0f74af0526b75ff0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e998cd8f0f74af0526b75ff0", "postgresql": "e998cd8f0f74af0526b75ff0", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -2337,8 +2337,8 @@ "databricks": "de89632e277dbc8c97115419", "duckdb": "de89632e277dbc8c97115419", "generic": "de89632e277dbc8c97115419", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "de89632e277dbc8c97115419", +"motherduck": "de89632e277dbc8c97115419", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5abf4ec799a2ec32fb10da7e", "postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", @@ -2354,8 +2354,8 @@ "databricks": "c50a316d091c821866e4bfba", "duckdb": "c50a316d091c821866e4bfba", "generic": "c50a316d091c821866e4bfba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -2371,8 +2371,8 @@ "databricks": "285056becb5d467811be7cd6", "duckdb": "0712c33a016f7dd6a37ee011", "generic": "285056becb5d467811be7cd6", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "285056becb5d467811be7cd6", +"motherduck": "0712c33a016f7dd6a37ee011", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "285056becb5d467811be7cd6", "postgresql": "285056becb5d467811be7cd6", "snowflake": "0712c33a016f7dd6a37ee011", @@ -2388,8 +2388,8 @@ "databricks": "b4c316e81db1cb6a277b5d27", "duckdb": "1655128a72b1db9af6a89df3", "generic": "1655128a72b1db9af6a89df3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1655128a72b1db9af6a89df3", +"motherduck": "1655128a72b1db9af6a89df3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1655128a72b1db9af6a89df3", "postgresql": "1655128a72b1db9af6a89df3", "snowflake": "1655128a72b1db9af6a89df3", @@ -2405,8 +2405,8 @@ "databricks": "e3bc1b8bad22d8104417b00f", "duckdb": "1d8b945fd793ecdcacb90c21", "generic": "1d8b945fd793ecdcacb90c21", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1d8b945fd793ecdcacb90c21", +"motherduck": "1d8b945fd793ecdcacb90c21", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3efe8bb57c98fcbe74096b3c", "postgresql": "3efe8bb57c98fcbe74096b3c", "snowflake": "1d8b945fd793ecdcacb90c21", @@ -2422,8 +2422,8 @@ "databricks": "3cf4b9eaf303451db16b56f0", "duckdb": "3cf4b9eaf303451db16b56f0", "generic": "3cf4b9eaf303451db16b56f0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3cf4b9eaf303451db16b56f0", +"motherduck": "3cf4b9eaf303451db16b56f0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3cf4b9eaf303451db16b56f0", "postgresql": "3cf4b9eaf303451db16b56f0", "snowflake": "3cf4b9eaf303451db16b56f0", @@ -2439,8 +2439,8 @@ "databricks": "e57ac1541fb51b362cd5b470", "duckdb": "e57ac1541fb51b362cd5b470", "generic": "e57ac1541fb51b362cd5b470", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e57ac1541fb51b362cd5b470", +"motherduck": "e57ac1541fb51b362cd5b470", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e57ac1541fb51b362cd5b470", "postgresql": "e57ac1541fb51b362cd5b470", "snowflake": "4b356542044f1267fcfc487a", @@ -2456,8 +2456,8 @@ "databricks": "1e75e5227bea0ae294ac7af1", "duckdb": "19bdd92ea0cc5fb028657510", "generic": "1e75e5227bea0ae294ac7af1", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "19bdd92ea0cc5fb028657510", +"motherduck": "19bdd92ea0cc5fb028657510", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "19bdd92ea0cc5fb028657510", "postgresql": "19bdd92ea0cc5fb028657510", "snowflake": "8ff614b7306c0edbe36ce00b", @@ -2473,8 +2473,8 @@ "databricks": "a52676af0c6b750acc33f021", "duckdb": "d81d8c19074af0e39239164d", "generic": "a52676af0c6b750acc33f021", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a52676af0c6b750acc33f021", +"motherduck": "d81d8c19074af0e39239164d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a52676af0c6b750acc33f021", "postgresql": "a52676af0c6b750acc33f021", "snowflake": "1fec266134381c760a254d16", @@ -2490,8 +2490,8 @@ "databricks": "3d23f811a03db519b598c815", "duckdb": "3d23f811a03db519b598c815", "generic": "3d23f811a03db519b598c815", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3d23f811a03db519b598c815", +"motherduck": "3d23f811a03db519b598c815", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3d23f811a03db519b598c815", "postgresql": "3d23f811a03db519b598c815", "snowflake": "3d23f811a03db519b598c815", @@ -2507,8 +2507,8 @@ "databricks": "4928771fd9e503a30457d160", "duckdb": "4928771fd9e503a30457d160", "generic": "4928771fd9e503a30457d160", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4928771fd9e503a30457d160", +"motherduck": "4928771fd9e503a30457d160", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "50ddfb168c08b09892261e82", "postgresql": "50ddfb168c08b09892261e82", "snowflake": "4928771fd9e503a30457d160", @@ -2524,8 +2524,8 @@ "databricks": "e280c4da8e0aefee0745af65", "duckdb": "e280c4da8e0aefee0745af65", "generic": "e280c4da8e0aefee0745af65", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e280c4da8e0aefee0745af65", +"motherduck": "e280c4da8e0aefee0745af65", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e280c4da8e0aefee0745af65", "postgresql": "e280c4da8e0aefee0745af65", "snowflake": "e280c4da8e0aefee0745af65", @@ -2541,8 +2541,8 @@ "databricks": "5aa51d84ac5e6c7a9306f348", "duckdb": "5aa51d84ac5e6c7a9306f348", "generic": "5aa51d84ac5e6c7a9306f348", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5aa51d84ac5e6c7a9306f348", +"motherduck": "5aa51d84ac5e6c7a9306f348", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5aa51d84ac5e6c7a9306f348", "postgresql": "5aa51d84ac5e6c7a9306f348", "snowflake": "5aa51d84ac5e6c7a9306f348", @@ -2558,8 +2558,8 @@ "databricks": "533aa8bbcadbe01c5e7a5ba7", "duckdb": "533aa8bbcadbe01c5e7a5ba7", "generic": "533aa8bbcadbe01c5e7a5ba7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "533aa8bbcadbe01c5e7a5ba7", +"motherduck": "533aa8bbcadbe01c5e7a5ba7", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "533aa8bbcadbe01c5e7a5ba7", "postgresql": "533aa8bbcadbe01c5e7a5ba7", "snowflake": "533aa8bbcadbe01c5e7a5ba7", @@ -2575,8 +2575,8 @@ "databricks": "e2879cf4770e7f48d2dd704a", "duckdb": "e2879cf4770e7f48d2dd704a", "generic": "e2879cf4770e7f48d2dd704a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e2879cf4770e7f48d2dd704a", +"motherduck": "e2879cf4770e7f48d2dd704a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2d4aea8dec5521e258596f3d", "postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", @@ -2592,8 +2592,8 @@ "databricks": "d638c2852078bf3facb9dc9e", "duckdb": "207acc46dcc150502155f4a3", "generic": "d638c2852078bf3facb9dc9e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "207acc46dcc150502155f4a3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "6c49aec265f259dd95b5310e", "postgresql": "6c49aec265f259dd95b5310e", "snowflake": "ef2c05747b795f92c0b523ee", @@ -2609,8 +2609,8 @@ "databricks": "ad35c4a2523ce8c2f57acf51", "duckdb": "ad35c4a2523ce8c2f57acf51", "generic": "ad35c4a2523ce8c2f57acf51", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ad35c4a2523ce8c2f57acf51", +"motherduck": "ad35c4a2523ce8c2f57acf51", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ad35c4a2523ce8c2f57acf51", "postgresql": "ad35c4a2523ce8c2f57acf51", "snowflake": "ad35c4a2523ce8c2f57acf51", @@ -2626,8 +2626,8 @@ "databricks": "8203bd5901a8ec71ff7c103e", "duckdb": "8203bd5901a8ec71ff7c103e", "generic": "8203bd5901a8ec71ff7c103e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8203bd5901a8ec71ff7c103e", +"motherduck": "8203bd5901a8ec71ff7c103e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8203bd5901a8ec71ff7c103e", "postgresql": "8203bd5901a8ec71ff7c103e", "snowflake": "8203bd5901a8ec71ff7c103e", @@ -2643,8 +2643,8 @@ "databricks": "f38ef32a483f1c4f89ae309e", "duckdb": "f38ef32a483f1c4f89ae309e", "generic": "f38ef32a483f1c4f89ae309e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f38ef32a483f1c4f89ae309e", +"motherduck": "f38ef32a483f1c4f89ae309e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f38ef32a483f1c4f89ae309e", "postgresql": "f38ef32a483f1c4f89ae309e", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -2660,8 +2660,8 @@ "databricks": "cd932c5de0731b5ff7bddd24", "duckdb": "cd932c5de0731b5ff7bddd24", "generic": "cd932c5de0731b5ff7bddd24", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "cd932c5de0731b5ff7bddd24", +"motherduck": "cd932c5de0731b5ff7bddd24", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "cd932c5de0731b5ff7bddd24", "postgresql": "cd932c5de0731b5ff7bddd24", "snowflake": "cd932c5de0731b5ff7bddd24", @@ -2677,8 +2677,8 @@ "databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", "generic": "fb4b6f5101f973d16ea8d1f3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fb4b6f5101f973d16ea8d1f3", +"motherduck": "70fb4728c3adcf6dbe4975d5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", @@ -2694,8 +2694,8 @@ "databricks": "19a1a80ffc06169f32694de8", "duckdb": "19a1a80ffc06169f32694de8", "generic": "19a1a80ffc06169f32694de8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "19a1a80ffc06169f32694de8", +"motherduck": "19a1a80ffc06169f32694de8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ab1f5f46e69284778bd66d58", "postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", @@ -2711,8 +2711,8 @@ "databricks": "f38ef32a483f1c4f89ae309e", "duckdb": "f38ef32a483f1c4f89ae309e", "generic": "f38ef32a483f1c4f89ae309e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f38ef32a483f1c4f89ae309e", +"motherduck": "f38ef32a483f1c4f89ae309e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f38ef32a483f1c4f89ae309e", "postgresql": "f38ef32a483f1c4f89ae309e", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -2728,8 +2728,8 @@ "databricks": "2e28de65a4b680fb5f4ebda9", "duckdb": "2e28de65a4b680fb5f4ebda9", "generic": "2e28de65a4b680fb5f4ebda9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2e28de65a4b680fb5f4ebda9", +"motherduck": "2e28de65a4b680fb5f4ebda9", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2e28de65a4b680fb5f4ebda9", "postgresql": "2e28de65a4b680fb5f4ebda9", "snowflake": "2e28de65a4b680fb5f4ebda9", @@ -2745,8 +2745,8 @@ "databricks": "ebc821a95b9286e22c2b26d0", "duckdb": "ebc821a95b9286e22c2b26d0", "generic": "ebc821a95b9286e22c2b26d0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ebc821a95b9286e22c2b26d0", +"motherduck": "ebc821a95b9286e22c2b26d0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ebc821a95b9286e22c2b26d0", "postgresql": "ebc821a95b9286e22c2b26d0", "snowflake": "ebc821a95b9286e22c2b26d0", @@ -2762,8 +2762,8 @@ "databricks": "79278352c17c9de0f5a28af5", "duckdb": "ace891b6844cb59efad42031", "generic": "79278352c17c9de0f5a28af5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "79278352c17c9de0f5a28af5", +"motherduck": "ace891b6844cb59efad42031", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "79278352c17c9de0f5a28af5", "postgresql": "79278352c17c9de0f5a28af5", "snowflake": "97a20344d811fb77c7690863", @@ -2779,8 +2779,8 @@ "databricks": "57922064a7dc546cbb50e42b", "duckdb": "57922064a7dc546cbb50e42b", "generic": "57922064a7dc546cbb50e42b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "57922064a7dc546cbb50e42b", +"motherduck": "57922064a7dc546cbb50e42b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "57922064a7dc546cbb50e42b", "postgresql": "57922064a7dc546cbb50e42b", "snowflake": "57922064a7dc546cbb50e42b", @@ -2796,8 +2796,8 @@ "databricks": "e08a957777bd278f921ca0ee", "duckdb": "e08a957777bd278f921ca0ee", "generic": "e08a957777bd278f921ca0ee", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e08a957777bd278f921ca0ee", +"motherduck": "e08a957777bd278f921ca0ee", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e08a957777bd278f921ca0ee", "postgresql": "e08a957777bd278f921ca0ee", "snowflake": "e08a957777bd278f921ca0ee", @@ -2813,8 +2813,8 @@ "databricks": "2785bf366213936ce93d91f2", "duckdb": "5980124ff4dad5573f810a21", "generic": "2785bf366213936ce93d91f2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "5980124ff4dad5573f810a21", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5980124ff4dad5573f810a21", "postgresql": "5980124ff4dad5573f810a21", "snowflake": "b53e32783b8b9274e350988b", @@ -2830,8 +2830,8 @@ "databricks": "055c9340448c8378561480dd", "duckdb": "055c9340448c8378561480dd", "generic": "055c9340448c8378561480dd", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "055c9340448c8378561480dd", +"motherduck": "055c9340448c8378561480dd", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "055c9340448c8378561480dd", "postgresql": "055c9340448c8378561480dd", "snowflake": "055c9340448c8378561480dd", @@ -2847,8 +2847,8 @@ "databricks": "ab8c248625776c7ac340119c", "duckdb": "ab8c248625776c7ac340119c", "generic": "ab8c248625776c7ac340119c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ab8c248625776c7ac340119c", +"motherduck": "ab8c248625776c7ac340119c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ab8c248625776c7ac340119c", "postgresql": "ab8c248625776c7ac340119c", "snowflake": "ab8c248625776c7ac340119c", @@ -2864,8 +2864,8 @@ "databricks": "d23bf939a7eb8e49c7e6867a", "duckdb": "d23bf939a7eb8e49c7e6867a", "generic": "d23bf939a7eb8e49c7e6867a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d23bf939a7eb8e49c7e6867a", +"motherduck": "d23bf939a7eb8e49c7e6867a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "71f4f8fbb5f5c537d895ceb4", "postgresql": "71f4f8fbb5f5c537d895ceb4", "snowflake": "d23bf939a7eb8e49c7e6867a", @@ -2881,8 +2881,8 @@ "databricks": "d79c3ac70093cb4cb531e71c", "duckdb": "d79c3ac70093cb4cb531e71c", "generic": "d79c3ac70093cb4cb531e71c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d79c3ac70093cb4cb531e71c", +"motherduck": "d79c3ac70093cb4cb531e71c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d79c3ac70093cb4cb531e71c", "postgresql": "d79c3ac70093cb4cb531e71c", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -2898,8 +2898,8 @@ "databricks": "3f71ab5f3c1c040da96f2cec", "duckdb": "3f71ab5f3c1c040da96f2cec", "generic": "3f71ab5f3c1c040da96f2cec", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3f71ab5f3c1c040da96f2cec", +"motherduck": "3f71ab5f3c1c040da96f2cec", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3f71ab5f3c1c040da96f2cec", "postgresql": "3f71ab5f3c1c040da96f2cec", "snowflake": "3f71ab5f3c1c040da96f2cec", @@ -2915,8 +2915,8 @@ "databricks": "782ae75a1700e1a48355b077", "duckdb": "782ae75a1700e1a48355b077", "generic": "782ae75a1700e1a48355b077", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "782ae75a1700e1a48355b077", +"motherduck": "782ae75a1700e1a48355b077", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "782ae75a1700e1a48355b077", "postgresql": "782ae75a1700e1a48355b077", "snowflake": "782ae75a1700e1a48355b077", @@ -2932,8 +2932,8 @@ "databricks": "28eb0d6de9724959ce5ba43e", "duckdb": "28eb0d6de9724959ce5ba43e", "generic": "28eb0d6de9724959ce5ba43e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "28eb0d6de9724959ce5ba43e", +"motherduck": "28eb0d6de9724959ce5ba43e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "28eb0d6de9724959ce5ba43e", "postgresql": "28eb0d6de9724959ce5ba43e", "snowflake": "28eb0d6de9724959ce5ba43e", @@ -2949,8 +2949,8 @@ "databricks": "dcd1c844f2d13f3b6788d1a6", "duckdb": "dcd1c844f2d13f3b6788d1a6", "generic": "dcd1c844f2d13f3b6788d1a6", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "dcd1c844f2d13f3b6788d1a6", +"motherduck": "dcd1c844f2d13f3b6788d1a6", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "dcd1c844f2d13f3b6788d1a6", "postgresql": "dcd1c844f2d13f3b6788d1a6", "snowflake": "dcd1c844f2d13f3b6788d1a6", @@ -2966,8 +2966,8 @@ "databricks": "304be1d51ad99bfab3750f57", "duckdb": "304be1d51ad99bfab3750f57", "generic": "304be1d51ad99bfab3750f57", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "304be1d51ad99bfab3750f57", +"motherduck": "304be1d51ad99bfab3750f57", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "304be1d51ad99bfab3750f57", "postgresql": "304be1d51ad99bfab3750f57", "snowflake": "304be1d51ad99bfab3750f57", @@ -2983,8 +2983,8 @@ "databricks": "3e42d732ee33bbd1424911a7", "duckdb": "7466f0581624f38e4d1e972d", "generic": "f25507ff493aa410f2d2a6d6", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f25507ff493aa410f2d2a6d6", +"motherduck": "7466f0581624f38e4d1e972d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f25507ff493aa410f2d2a6d6", "postgresql": "f25507ff493aa410f2d2a6d6", "snowflake": "60ad8b0a5cb3592bd3b0fc8f", @@ -3000,8 +3000,8 @@ "databricks": "f4c43a87a78970bca1f88b09", "duckdb": "f4c43a87a78970bca1f88b09", "generic": "f4c43a87a78970bca1f88b09", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f4c43a87a78970bca1f88b09", +"motherduck": "f4c43a87a78970bca1f88b09", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f4c43a87a78970bca1f88b09", "postgresql": "f4c43a87a78970bca1f88b09", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -3017,8 +3017,8 @@ "databricks": "2427f5522575cdc3d6a74e4c", "duckdb": "2427f5522575cdc3d6a74e4c", "generic": "2427f5522575cdc3d6a74e4c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2427f5522575cdc3d6a74e4c", +"motherduck": "2427f5522575cdc3d6a74e4c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2427f5522575cdc3d6a74e4c", "postgresql": "2427f5522575cdc3d6a74e4c", "snowflake": "2427f5522575cdc3d6a74e4c", @@ -3034,8 +3034,8 @@ "databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", "generic": "fb4b6f5101f973d16ea8d1f3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fb4b6f5101f973d16ea8d1f3", +"motherduck": "70fb4728c3adcf6dbe4975d5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", @@ -3051,8 +3051,8 @@ "databricks": "8191fff105ab0743bf9ffd39", "duckdb": "3af3f9124a8649e4f72030c1", "generic": "8191fff105ab0743bf9ffd39", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8191fff105ab0743bf9ffd39", +"motherduck": "3af3f9124a8649e4f72030c1", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "23cc34d931e18f55168723f0", "postgresql": "23cc34d931e18f55168723f0", "snowflake": "3af3f9124a8649e4f72030c1", @@ -3068,8 +3068,8 @@ "databricks": "e46d4be44ab9e500e9190d26", "duckdb": "e46d4be44ab9e500e9190d26", "generic": "e46d4be44ab9e500e9190d26", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e46d4be44ab9e500e9190d26", +"motherduck": "e46d4be44ab9e500e9190d26", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "251aba3fef503ce64556c701", "postgresql": "251aba3fef503ce64556c701", "snowflake": "e46d4be44ab9e500e9190d26", @@ -3085,8 +3085,8 @@ "databricks": "5c28019dce48a74708c90a76", "duckdb": "5c28019dce48a74708c90a76", "generic": "5c28019dce48a74708c90a76", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5c28019dce48a74708c90a76", +"motherduck": "5c28019dce48a74708c90a76", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5c28019dce48a74708c90a76", "postgresql": "5c28019dce48a74708c90a76", "snowflake": "a5656d70e208254d1d3e9b4f", @@ -3102,8 +3102,8 @@ "databricks": "0dcd6a19b7dc6f6b6d4a5006", "duckdb": "0dcd6a19b7dc6f6b6d4a5006", "generic": "0dcd6a19b7dc6f6b6d4a5006", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0dcd6a19b7dc6f6b6d4a5006", +"motherduck": "0dcd6a19b7dc6f6b6d4a5006", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0dcd6a19b7dc6f6b6d4a5006", "postgresql": "0dcd6a19b7dc6f6b6d4a5006", "snowflake": "0dcd6a19b7dc6f6b6d4a5006", @@ -3119,8 +3119,8 @@ "databricks": "c2700ca3573352821e09af03", "duckdb": "c2700ca3573352821e09af03", "generic": "c2700ca3573352821e09af03", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c2700ca3573352821e09af03", +"motherduck": "c2700ca3573352821e09af03", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c2700ca3573352821e09af03", "postgresql": "c2700ca3573352821e09af03", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -3136,8 +3136,8 @@ "databricks": "b561884132dbe7e42b719110", "duckdb": "b561884132dbe7e42b719110", "generic": "b561884132dbe7e42b719110", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b561884132dbe7e42b719110", +"motherduck": "b561884132dbe7e42b719110", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5aa51d84ac5e6c7a9306f348", "postgresql": "5aa51d84ac5e6c7a9306f348", "snowflake": "b561884132dbe7e42b719110", @@ -3153,8 +3153,8 @@ "databricks": "d7b74701deb8b87c32deb7a6", "duckdb": "d7b74701deb8b87c32deb7a6", "generic": "d7b74701deb8b87c32deb7a6", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d7b74701deb8b87c32deb7a6", +"motherduck": "d7b74701deb8b87c32deb7a6", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d7b74701deb8b87c32deb7a6", "postgresql": "d7b74701deb8b87c32deb7a6", "snowflake": "d7b74701deb8b87c32deb7a6", @@ -3170,8 +3170,8 @@ "databricks": "a6b64a929834c52f65384447", "duckdb": "a6b64a929834c52f65384447", "generic": "a6b64a929834c52f65384447", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a6b64a929834c52f65384447", +"motherduck": "a6b64a929834c52f65384447", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a6b64a929834c52f65384447", "postgresql": "a6b64a929834c52f65384447", "snowflake": "a6b64a929834c52f65384447", @@ -3187,8 +3187,8 @@ "databricks": "a52676af0c6b750acc33f021", "duckdb": "d81d8c19074af0e39239164d", "generic": "a52676af0c6b750acc33f021", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a52676af0c6b750acc33f021", +"motherduck": "d81d8c19074af0e39239164d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a52676af0c6b750acc33f021", "postgresql": "a52676af0c6b750acc33f021", "snowflake": "1fec266134381c760a254d16", @@ -3204,8 +3204,8 @@ "databricks": "62368fc15cefd5807807b209", "duckdb": "62368fc15cefd5807807b209", "generic": "62368fc15cefd5807807b209", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "62368fc15cefd5807807b209", +"motherduck": "62368fc15cefd5807807b209", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "62368fc15cefd5807807b209", "postgresql": "62368fc15cefd5807807b209", "snowflake": "62368fc15cefd5807807b209", @@ -3221,8 +3221,8 @@ "databricks": "fbbf6dbffd81c1b899b2f41b", "duckdb": "fbbf6dbffd81c1b899b2f41b", "generic": "fbbf6dbffd81c1b899b2f41b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fbbf6dbffd81c1b899b2f41b", +"motherduck": "fbbf6dbffd81c1b899b2f41b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "fbbf6dbffd81c1b899b2f41b", "postgresql": "fbbf6dbffd81c1b899b2f41b", "snowflake": "fbbf6dbffd81c1b899b2f41b", @@ -3238,8 +3238,8 @@ "databricks": "e57ac1541fb51b362cd5b470", "duckdb": "e57ac1541fb51b362cd5b470", "generic": "e57ac1541fb51b362cd5b470", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e57ac1541fb51b362cd5b470", +"motherduck": "e57ac1541fb51b362cd5b470", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e57ac1541fb51b362cd5b470", "postgresql": "e57ac1541fb51b362cd5b470", "snowflake": "4b356542044f1267fcfc487a", @@ -3255,8 +3255,8 @@ "databricks": "a2c63b7e92b578944818d750", "duckdb": "a2c63b7e92b578944818d750", "generic": "a2c63b7e92b578944818d750", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a2c63b7e92b578944818d750", +"motherduck": "a2c63b7e92b578944818d750", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a2c63b7e92b578944818d750", "postgresql": "a2c63b7e92b578944818d750", "snowflake": "a2c63b7e92b578944818d750", @@ -3272,8 +3272,8 @@ "databricks": "a3d6f52ec64a728636793faa", "duckdb": "a3d6f52ec64a728636793faa", "generic": "a3d6f52ec64a728636793faa", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a3d6f52ec64a728636793faa", +"motherduck": "a3d6f52ec64a728636793faa", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a3d6f52ec64a728636793faa", "postgresql": "a3d6f52ec64a728636793faa", "snowflake": "a3d6f52ec64a728636793faa", @@ -3289,8 +3289,8 @@ "databricks": "dcb00b7053ffe69fe3ce99cd", "duckdb": "c7b697bbfa82dd2821ad64e9", "generic": "c7b697bbfa82dd2821ad64e9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c7b697bbfa82dd2821ad64e9", +"motherduck": "c7b697bbfa82dd2821ad64e9", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c7b697bbfa82dd2821ad64e9", "postgresql": "c7b697bbfa82dd2821ad64e9", "snowflake": "c7b697bbfa82dd2821ad64e9", @@ -3306,8 +3306,8 @@ "databricks": "717cf79ef777823eb3576deb", "duckdb": "717cf79ef777823eb3576deb", "generic": "717cf79ef777823eb3576deb", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "717cf79ef777823eb3576deb", +"motherduck": "717cf79ef777823eb3576deb", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "717cf79ef777823eb3576deb", "postgresql": "717cf79ef777823eb3576deb", "snowflake": "717cf79ef777823eb3576deb", @@ -3323,8 +3323,8 @@ "databricks": "c1dad3a1ff61e56cb10b9cdf", "duckdb": "c1dad3a1ff61e56cb10b9cdf", "generic": "c1dad3a1ff61e56cb10b9cdf", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c1dad3a1ff61e56cb10b9cdf", +"motherduck": "c1dad3a1ff61e56cb10b9cdf", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c1dad3a1ff61e56cb10b9cdf", "postgresql": "c1dad3a1ff61e56cb10b9cdf", "snowflake": "c1dad3a1ff61e56cb10b9cdf", @@ -3340,8 +3340,8 @@ "databricks": "79278352c17c9de0f5a28af5", "duckdb": "79278352c17c9de0f5a28af5", "generic": "79278352c17c9de0f5a28af5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "79278352c17c9de0f5a28af5", +"motherduck": "79278352c17c9de0f5a28af5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "79278352c17c9de0f5a28af5", "postgresql": "79278352c17c9de0f5a28af5", "snowflake": "97a20344d811fb77c7690863", @@ -3357,8 +3357,8 @@ "databricks": "17a6d1702d6997aa4e447f46", "duckdb": "755657fbcced1de4551fb370", "generic": "17a6d1702d6997aa4e447f46", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "17a6d1702d6997aa4e447f46", +"motherduck": "755657fbcced1de4551fb370", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "17a6d1702d6997aa4e447f46", "postgresql": "17a6d1702d6997aa4e447f46", "snowflake": "17a6d1702d6997aa4e447f46", @@ -3374,8 +3374,8 @@ "databricks": "c05190b1f2b5d7003e7076da", "duckdb": "c05190b1f2b5d7003e7076da", "generic": "c05190b1f2b5d7003e7076da", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c05190b1f2b5d7003e7076da", +"motherduck": "c05190b1f2b5d7003e7076da", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c05190b1f2b5d7003e7076da", "postgresql": "c05190b1f2b5d7003e7076da", "snowflake": "c05190b1f2b5d7003e7076da", @@ -3391,8 +3391,8 @@ "databricks": "ebe2513b6cb4001e42cc464b", "duckdb": "fa45b7e4a9ef3b5a9c5781af", "generic": "ebe2513b6cb4001e42cc464b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ebe2513b6cb4001e42cc464b", +"motherduck": "fa45b7e4a9ef3b5a9c5781af", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "fa45b7e4a9ef3b5a9c5781af", "postgresql": "fa45b7e4a9ef3b5a9c5781af", "snowflake": "fa45b7e4a9ef3b5a9c5781af", @@ -3408,8 +3408,8 @@ "databricks": "1bdf5908ca249b2331a0427c", "duckdb": "1bdf5908ca249b2331a0427c", "generic": "1bdf5908ca249b2331a0427c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1bdf5908ca249b2331a0427c", +"motherduck": "1bdf5908ca249b2331a0427c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1bdf5908ca249b2331a0427c", "postgresql": "1bdf5908ca249b2331a0427c", "snowflake": "8ff614b7306c0edbe36ce00b", @@ -3425,8 +3425,8 @@ "databricks": "bb7975dcb1f2ac7cdd149b0b", "duckdb": "bb7975dcb1f2ac7cdd149b0b", "generic": "bb7975dcb1f2ac7cdd149b0b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "bb7975dcb1f2ac7cdd149b0b", +"motherduck": "bb7975dcb1f2ac7cdd149b0b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "bb7975dcb1f2ac7cdd149b0b", "postgresql": "bb7975dcb1f2ac7cdd149b0b", "snowflake": "bb7975dcb1f2ac7cdd149b0b", @@ -3442,8 +3442,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1eb7fb4e8cf75773c5573562", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1eb7fb4e8cf75773c5573562", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -3459,8 +3459,8 @@ "databricks": "74ed18299daa6faf5c9533e2", "duckdb": "74ed18299daa6faf5c9533e2", "generic": "74ed18299daa6faf5c9533e2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "74ed18299daa6faf5c9533e2", +"motherduck": "74ed18299daa6faf5c9533e2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "74ed18299daa6faf5c9533e2", "postgresql": "74ed18299daa6faf5c9533e2", "snowflake": "74ed18299daa6faf5c9533e2", @@ -3476,8 +3476,8 @@ "databricks": "60a23b7dcae6c79f4de83068", "duckdb": "60a23b7dcae6c79f4de83068", "generic": "60a23b7dcae6c79f4de83068", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "60a23b7dcae6c79f4de83068", +"motherduck": "60a23b7dcae6c79f4de83068", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "60a23b7dcae6c79f4de83068", "postgresql": "60a23b7dcae6c79f4de83068", "snowflake": "60a23b7dcae6c79f4de83068", @@ -3493,8 +3493,8 @@ "databricks": "0f799a95a4f5ef502897f02c", "duckdb": "0f799a95a4f5ef502897f02c", "generic": "0f799a95a4f5ef502897f02c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0f799a95a4f5ef502897f02c", +"motherduck": "0f799a95a4f5ef502897f02c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0f799a95a4f5ef502897f02c", "postgresql": "0f799a95a4f5ef502897f02c", "snowflake": "0f799a95a4f5ef502897f02c", @@ -3510,8 +3510,8 @@ "databricks": "5d0a19a893dbf0c1e6e9f15c", "duckdb": "5d0a19a893dbf0c1e6e9f15c", "generic": "5d0a19a893dbf0c1e6e9f15c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5d0a19a893dbf0c1e6e9f15c", +"motherduck": "5d0a19a893dbf0c1e6e9f15c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5d0a19a893dbf0c1e6e9f15c", "postgresql": "5d0a19a893dbf0c1e6e9f15c", "snowflake": "5d0a19a893dbf0c1e6e9f15c", @@ -3527,8 +3527,8 @@ "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fe23594c781e79cbb0c8927a", +"motherduck": "fe23594c781e79cbb0c8927a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8a3f094f18fb4047f0febf59", "postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", @@ -3544,8 +3544,8 @@ "databricks": "855e2727b4a8cb5d2711f102", "duckdb": "855e2727b4a8cb5d2711f102", "generic": "855e2727b4a8cb5d2711f102", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "855e2727b4a8cb5d2711f102", +"motherduck": "855e2727b4a8cb5d2711f102", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "855e2727b4a8cb5d2711f102", "postgresql": "855e2727b4a8cb5d2711f102", "snowflake": "855e2727b4a8cb5d2711f102", @@ -3561,8 +3561,8 @@ "databricks": "79278352c17c9de0f5a28af5", "duckdb": "1ebac487198b6c90b027faab", "generic": "79278352c17c9de0f5a28af5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "79278352c17c9de0f5a28af5", +"motherduck": "1ebac487198b6c90b027faab", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "79278352c17c9de0f5a28af5", "postgresql": "79278352c17c9de0f5a28af5", "snowflake": "97a20344d811fb77c7690863", @@ -3578,8 +3578,8 @@ "databricks": "9c2cff8be7225cf79883ca65", "duckdb": "9c2cff8be7225cf79883ca65", "generic": "9c2cff8be7225cf79883ca65", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9c2cff8be7225cf79883ca65", +"motherduck": "9c2cff8be7225cf79883ca65", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9c2cff8be7225cf79883ca65", "postgresql": "9c2cff8be7225cf79883ca65", "snowflake": "9c2cff8be7225cf79883ca65", @@ -3595,8 +3595,8 @@ "databricks": "74f50f8ec759a3cc65ac027d", "duckdb": "74f50f8ec759a3cc65ac027d", "generic": "74f50f8ec759a3cc65ac027d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "74f50f8ec759a3cc65ac027d", +"motherduck": "74f50f8ec759a3cc65ac027d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "74f50f8ec759a3cc65ac027d", "postgresql": "74f50f8ec759a3cc65ac027d", "snowflake": "74f50f8ec759a3cc65ac027d", @@ -3612,8 +3612,8 @@ "databricks": "3dbbe73ee4310c73030e8110", "duckdb": "3dbbe73ee4310c73030e8110", "generic": "3dbbe73ee4310c73030e8110", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3dbbe73ee4310c73030e8110", +"motherduck": "3dbbe73ee4310c73030e8110", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3dbbe73ee4310c73030e8110", "postgresql": "3dbbe73ee4310c73030e8110", "snowflake": "f43dac38f34a1c0618a3f812", @@ -3629,8 +3629,8 @@ "databricks": "0dcd6a19b7dc6f6b6d4a5006", "duckdb": "0dcd6a19b7dc6f6b6d4a5006", "generic": "0dcd6a19b7dc6f6b6d4a5006", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0dcd6a19b7dc6f6b6d4a5006", +"motherduck": "0dcd6a19b7dc6f6b6d4a5006", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0dcd6a19b7dc6f6b6d4a5006", "postgresql": "0dcd6a19b7dc6f6b6d4a5006", "snowflake": "0dcd6a19b7dc6f6b6d4a5006", @@ -3646,8 +3646,8 @@ "databricks": "7ffb4db35f5f9871e8c9a084", "duckdb": "7ffb4db35f5f9871e8c9a084", "generic": "7ffb4db35f5f9871e8c9a084", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "7ffb4db35f5f9871e8c9a084", +"motherduck": "7ffb4db35f5f9871e8c9a084", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7ffb4db35f5f9871e8c9a084", "postgresql": "7ffb4db35f5f9871e8c9a084", "snowflake": "7ffb4db35f5f9871e8c9a084", @@ -3663,8 +3663,8 @@ "databricks": "d79c3ac70093cb4cb531e71c", "duckdb": "d79c3ac70093cb4cb531e71c", "generic": "d79c3ac70093cb4cb531e71c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d79c3ac70093cb4cb531e71c", +"motherduck": "d79c3ac70093cb4cb531e71c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d79c3ac70093cb4cb531e71c", "postgresql": "d79c3ac70093cb4cb531e71c", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -3680,8 +3680,8 @@ "databricks": "da0625706f12b764a6912278", "duckdb": "3147a9e06daa6de4a4df5abc", "generic": "da0625706f12b764a6912278", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "da0625706f12b764a6912278", +"motherduck": "3147a9e06daa6de4a4df5abc", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3147a9e06daa6de4a4df5abc", "postgresql": "3147a9e06daa6de4a4df5abc", "snowflake": "67b5874a2c569226c490211f", @@ -3697,8 +3697,8 @@ "databricks": "02e0b2ed25f32a8d09350a23", "duckdb": "7afe04283391fb5bd9bd1f6e", "generic": "6fad076e9d13c0d09c490402", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "7afe04283391fb5bd9bd1f6e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7afe04283391fb5bd9bd1f6e", "postgresql": "7afe04283391fb5bd9bd1f6e", "snowflake": "dddc37362726cca4f2353a8d", @@ -3714,8 +3714,8 @@ "databricks": "802f1c1fcb97ddd5679172d2", "duckdb": "802f1c1fcb97ddd5679172d2", "generic": "802f1c1fcb97ddd5679172d2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "802f1c1fcb97ddd5679172d2", +"motherduck": "802f1c1fcb97ddd5679172d2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "802f1c1fcb97ddd5679172d2", "postgresql": "802f1c1fcb97ddd5679172d2", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -3731,8 +3731,8 @@ "databricks": "7552f1f36764fa5333dc3b01", "duckdb": "71cce31a35d7039dcd2aa86d", "generic": "71cce31a35d7039dcd2aa86d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "71cce31a35d7039dcd2aa86d", +"motherduck": "71cce31a35d7039dcd2aa86d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "71cce31a35d7039dcd2aa86d", "postgresql": "71cce31a35d7039dcd2aa86d", "snowflake": "aa1ac5b2c999b497d54fa520", @@ -3748,8 +3748,8 @@ "databricks": "aae2b1fe6f2d9807ecde412b", "duckdb": "aae2b1fe6f2d9807ecde412b", "generic": "aae2b1fe6f2d9807ecde412b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "aae2b1fe6f2d9807ecde412b", +"motherduck": "aae2b1fe6f2d9807ecde412b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "aae2b1fe6f2d9807ecde412b", "postgresql": "aae2b1fe6f2d9807ecde412b", "snowflake": "aae2b1fe6f2d9807ecde412b", @@ -3765,8 +3765,8 @@ "databricks": "4169d9651c2a3667fc561f32", "duckdb": "4169d9651c2a3667fc561f32", "generic": "4169d9651c2a3667fc561f32", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4169d9651c2a3667fc561f32", +"motherduck": "4169d9651c2a3667fc561f32", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "4169d9651c2a3667fc561f32", "postgresql": "4169d9651c2a3667fc561f32", "snowflake": "b210f8eb26c37b422f786c6b", @@ -3782,8 +3782,8 @@ "databricks": "8441d00030df06ad6a5534d3", "duckdb": "8441d00030df06ad6a5534d3", "generic": "8441d00030df06ad6a5534d3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8441d00030df06ad6a5534d3", +"motherduck": "8441d00030df06ad6a5534d3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8441d00030df06ad6a5534d3", "postgresql": "8441d00030df06ad6a5534d3", "snowflake": "97a20344d811fb77c7690863", @@ -3799,8 +3799,8 @@ "databricks": "6533f23a0660791970dbbbc8", "duckdb": "6533f23a0660791970dbbbc8", "generic": "6533f23a0660791970dbbbc8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6533f23a0660791970dbbbc8", +"motherduck": "6533f23a0660791970dbbbc8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "6533f23a0660791970dbbbc8", "postgresql": "6533f23a0660791970dbbbc8", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -3816,8 +3816,8 @@ "databricks": "6d07fc2f489d414e7eb90262", "duckdb": "9d0ae138704cb97e3fe4d105", "generic": "9d0ae138704cb97e3fe4d105", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9d0ae138704cb97e3fe4d105", +"motherduck": "9d0ae138704cb97e3fe4d105", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9d0ae138704cb97e3fe4d105", "postgresql": "9d0ae138704cb97e3fe4d105", "snowflake": "9d0ae138704cb97e3fe4d105", @@ -3833,8 +3833,8 @@ "databricks": "de89632e277dbc8c97115419", "duckdb": "de89632e277dbc8c97115419", "generic": "de89632e277dbc8c97115419", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "de89632e277dbc8c97115419", +"motherduck": "de89632e277dbc8c97115419", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5abf4ec799a2ec32fb10da7e", "postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", @@ -3850,8 +3850,8 @@ "databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", "generic": "fb4b6f5101f973d16ea8d1f3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fb4b6f5101f973d16ea8d1f3", +"motherduck": "70fb4728c3adcf6dbe4975d5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", @@ -3867,8 +3867,8 @@ "databricks": "8441d00030df06ad6a5534d3", "duckdb": "2c44dc35fd1ef8701101802e", "generic": "8441d00030df06ad6a5534d3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8441d00030df06ad6a5534d3", +"motherduck": "2c44dc35fd1ef8701101802e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8441d00030df06ad6a5534d3", "postgresql": "8441d00030df06ad6a5534d3", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -3884,8 +3884,8 @@ "databricks": "9917d6c8c04307304904313b", "duckdb": "9917d6c8c04307304904313b", "generic": "9917d6c8c04307304904313b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9917d6c8c04307304904313b", +"motherduck": "9917d6c8c04307304904313b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9917d6c8c04307304904313b", "postgresql": "9917d6c8c04307304904313b", "snowflake": "9917d6c8c04307304904313b", @@ -3901,8 +3901,8 @@ "databricks": "38b2ec94bf02be40007ce525", "duckdb": "8a3f094f18fb4047f0febf59", "generic": "38b2ec94bf02be40007ce525", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "38b2ec94bf02be40007ce525", +"motherduck": "8a3f094f18fb4047f0febf59", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "850deea990b52b82b36a9848", "postgresql": "850deea990b52b82b36a9848", "snowflake": "e51e1eecca3e36c76e34498f", @@ -3918,8 +3918,8 @@ "databricks": "4b356542044f1267fcfc487a", "duckdb": "4b356542044f1267fcfc487a", "generic": "4b356542044f1267fcfc487a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4b356542044f1267fcfc487a", +"motherduck": "4b356542044f1267fcfc487a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a5656d70e208254d1d3e9b4f", "postgresql": "a5656d70e208254d1d3e9b4f", "snowflake": "4b356542044f1267fcfc487a", @@ -3935,8 +3935,8 @@ "databricks": "ebc821a95b9286e22c2b26d0", "duckdb": "ebc821a95b9286e22c2b26d0", "generic": "ebc821a95b9286e22c2b26d0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ebc821a95b9286e22c2b26d0", +"motherduck": "ebc821a95b9286e22c2b26d0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ebc821a95b9286e22c2b26d0", "postgresql": "ebc821a95b9286e22c2b26d0", "snowflake": "ebc821a95b9286e22c2b26d0", @@ -3952,8 +3952,8 @@ "databricks": "afc451102e0ebe7f507119b9", "duckdb": "afc451102e0ebe7f507119b9", "generic": "afc451102e0ebe7f507119b9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "afc451102e0ebe7f507119b9", +"motherduck": "afc451102e0ebe7f507119b9", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ff3f47e5455500158628a360", "postgresql": "ff3f47e5455500158628a360", "snowflake": "afc451102e0ebe7f507119b9", @@ -3969,8 +3969,8 @@ "databricks": "74ed18299daa6faf5c9533e2", "duckdb": "74ed18299daa6faf5c9533e2", "generic": "74ed18299daa6faf5c9533e2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "74ed18299daa6faf5c9533e2", +"motherduck": "74ed18299daa6faf5c9533e2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "74ed18299daa6faf5c9533e2", "postgresql": "74ed18299daa6faf5c9533e2", "snowflake": "74ed18299daa6faf5c9533e2", @@ -3986,8 +3986,8 @@ "databricks": "f43dac38f34a1c0618a3f812", "duckdb": "f43dac38f34a1c0618a3f812", "generic": "f43dac38f34a1c0618a3f812", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f43dac38f34a1c0618a3f812", +"motherduck": "f43dac38f34a1c0618a3f812", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f43dac38f34a1c0618a3f812", "postgresql": "f43dac38f34a1c0618a3f812", "snowflake": "f43dac38f34a1c0618a3f812", @@ -4003,8 +4003,8 @@ "databricks": "2bfa352725f9d8a91250f75e", "duckdb": "2bfa352725f9d8a91250f75e", "generic": "2bfa352725f9d8a91250f75e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2bfa352725f9d8a91250f75e", +"motherduck": "2bfa352725f9d8a91250f75e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2bfa352725f9d8a91250f75e", "postgresql": "2bfa352725f9d8a91250f75e", "snowflake": "2bfa352725f9d8a91250f75e", @@ -4020,8 +4020,8 @@ "databricks": "60b39a68a18438603dd5496d", "duckdb": "60b39a68a18438603dd5496d", "generic": "60b39a68a18438603dd5496d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "60b39a68a18438603dd5496d", +"motherduck": "60b39a68a18438603dd5496d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7a007e6ff9e9465c9d7fadea", "postgresql": "7a007e6ff9e9465c9d7fadea", "snowflake": "60b39a68a18438603dd5496d", @@ -4037,8 +4037,8 @@ "databricks": "b561884132dbe7e42b719110", "duckdb": "b561884132dbe7e42b719110", "generic": "b561884132dbe7e42b719110", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b561884132dbe7e42b719110", +"motherduck": "b561884132dbe7e42b719110", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5aa51d84ac5e6c7a9306f348", "postgresql": "5aa51d84ac5e6c7a9306f348", "snowflake": "b561884132dbe7e42b719110", @@ -4054,8 +4054,8 @@ "databricks": "0b9d9879e289ea9a6a3e3e70", "duckdb": "0b9d9879e289ea9a6a3e3e70", "generic": "0b9d9879e289ea9a6a3e3e70", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0b9d9879e289ea9a6a3e3e70", +"motherduck": "0b9d9879e289ea9a6a3e3e70", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0b9d9879e289ea9a6a3e3e70", "postgresql": "0b9d9879e289ea9a6a3e3e70", "snowflake": "0b9d9879e289ea9a6a3e3e70", @@ -4071,8 +4071,8 @@ "databricks": "3f71ab5f3c1c040da96f2cec", "duckdb": "3f71ab5f3c1c040da96f2cec", "generic": "3f71ab5f3c1c040da96f2cec", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3f71ab5f3c1c040da96f2cec", +"motherduck": "3f71ab5f3c1c040da96f2cec", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3f71ab5f3c1c040da96f2cec", "postgresql": "3f71ab5f3c1c040da96f2cec", "snowflake": "3f71ab5f3c1c040da96f2cec", @@ -4088,8 +4088,8 @@ "databricks": "c6197f1da8168ac56d61807b", "duckdb": "c6197f1da8168ac56d61807b", "generic": "c6197f1da8168ac56d61807b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c6197f1da8168ac56d61807b", +"motherduck": "c6197f1da8168ac56d61807b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c6197f1da8168ac56d61807b", "postgresql": "c6197f1da8168ac56d61807b", "snowflake": "c6197f1da8168ac56d61807b", @@ -4105,8 +4105,8 @@ "databricks": "38b2ec94bf02be40007ce525", "duckdb": "38b2ec94bf02be40007ce525", "generic": "38b2ec94bf02be40007ce525", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "38b2ec94bf02be40007ce525", +"motherduck": "38b2ec94bf02be40007ce525", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "850deea990b52b82b36a9848", "postgresql": "850deea990b52b82b36a9848", "snowflake": "38b2ec94bf02be40007ce525", @@ -4122,8 +4122,8 @@ "databricks": "e998cd8f0f74af0526b75ff0", "duckdb": "e998cd8f0f74af0526b75ff0", "generic": "e998cd8f0f74af0526b75ff0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e998cd8f0f74af0526b75ff0", +"motherduck": "e998cd8f0f74af0526b75ff0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e998cd8f0f74af0526b75ff0", "postgresql": "e998cd8f0f74af0526b75ff0", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -4139,8 +4139,8 @@ "databricks": "58b79aaeb9587eafe21bc119", "duckdb": "58b79aaeb9587eafe21bc119", "generic": "58b79aaeb9587eafe21bc119", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "58b79aaeb9587eafe21bc119", +"motherduck": "58b79aaeb9587eafe21bc119", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "58b79aaeb9587eafe21bc119", "postgresql": "58b79aaeb9587eafe21bc119", "snowflake": "58b79aaeb9587eafe21bc119", @@ -4156,8 +4156,8 @@ "databricks": "776e4bf23c8ef10ffaceade2", "duckdb": "776e4bf23c8ef10ffaceade2", "generic": "776e4bf23c8ef10ffaceade2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "776e4bf23c8ef10ffaceade2", +"motherduck": "776e4bf23c8ef10ffaceade2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5d06b10843e3e6487fb9906a", "postgresql": "5d06b10843e3e6487fb9906a", "snowflake": "776e4bf23c8ef10ffaceade2", @@ -4173,8 +4173,8 @@ "databricks": "73d31c6e78cdc3b595b9c4e9", "duckdb": "73d31c6e78cdc3b595b9c4e9", "generic": "73d31c6e78cdc3b595b9c4e9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "73d31c6e78cdc3b595b9c4e9", +"motherduck": "73d31c6e78cdc3b595b9c4e9", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "73d31c6e78cdc3b595b9c4e9", "postgresql": "73d31c6e78cdc3b595b9c4e9", "snowflake": "73d31c6e78cdc3b595b9c4e9", @@ -4190,8 +4190,8 @@ "databricks": "57922064a7dc546cbb50e42b", "duckdb": "57922064a7dc546cbb50e42b", "generic": "57922064a7dc546cbb50e42b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "57922064a7dc546cbb50e42b", +"motherduck": "57922064a7dc546cbb50e42b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "57922064a7dc546cbb50e42b", "postgresql": "57922064a7dc546cbb50e42b", "snowflake": "57922064a7dc546cbb50e42b", @@ -4207,8 +4207,8 @@ "databricks": "e408f2d8251e1675179187e6", "duckdb": "e408f2d8251e1675179187e6", "generic": "e408f2d8251e1675179187e6", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e408f2d8251e1675179187e6", +"motherduck": "e408f2d8251e1675179187e6", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "04bfa329b4dc3b5c43ad2560", "postgresql": "04bfa329b4dc3b5c43ad2560", "snowflake": "e408f2d8251e1675179187e6", @@ -4224,8 +4224,8 @@ "databricks": "aab0389e2b5470136660bf8a", "duckdb": "aab0389e2b5470136660bf8a", "generic": "aab0389e2b5470136660bf8a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "aab0389e2b5470136660bf8a", +"motherduck": "aab0389e2b5470136660bf8a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f16105cbc61d547e6c08efc8", "postgresql": "f16105cbc61d547e6c08efc8", "snowflake": "97a20344d811fb77c7690863", @@ -4241,8 +4241,8 @@ "databricks": "5c528c7b4329aeb692ec6e94", "duckdb": "5c528c7b4329aeb692ec6e94", "generic": "5c528c7b4329aeb692ec6e94", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5c528c7b4329aeb692ec6e94", +"motherduck": "5c528c7b4329aeb692ec6e94", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5c528c7b4329aeb692ec6e94", "postgresql": "5c528c7b4329aeb692ec6e94", "snowflake": "5c528c7b4329aeb692ec6e94", @@ -4258,8 +4258,8 @@ "databricks": "96ecb0bd0c27167bf4e31a08", "duckdb": "96ecb0bd0c27167bf4e31a08", "generic": "96ecb0bd0c27167bf4e31a08", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "96ecb0bd0c27167bf4e31a08", +"motherduck": "96ecb0bd0c27167bf4e31a08", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "96ecb0bd0c27167bf4e31a08", "postgresql": "96ecb0bd0c27167bf4e31a08", "snowflake": "96ecb0bd0c27167bf4e31a08", @@ -4275,8 +4275,8 @@ "databricks": "b46dfa976d5ba04f95bea32b", "duckdb": "b46dfa976d5ba04f95bea32b", "generic": "b46dfa976d5ba04f95bea32b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b46dfa976d5ba04f95bea32b", +"motherduck": "b46dfa976d5ba04f95bea32b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "34dcdfe3e3afe6140e1353e0", "postgresql": "34dcdfe3e3afe6140e1353e0", "snowflake": "b46dfa976d5ba04f95bea32b", @@ -4292,8 +4292,8 @@ "databricks": "fbcb21692163c35ec690c1a4", "duckdb": "f8262a8fca64e4491f151610", "generic": "fbcb21692163c35ec690c1a4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fbcb21692163c35ec690c1a4", +"motherduck": "f8262a8fca64e4491f151610", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "fbcb21692163c35ec690c1a4", "postgresql": "fbcb21692163c35ec690c1a4", "snowflake": "f8262a8fca64e4491f151610", @@ -4309,8 +4309,8 @@ "databricks": "fdd892c7bbc36567f9619175", "duckdb": "b694aa93969368381cb9d9bd", "generic": "fdd892c7bbc36567f9619175", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fdd892c7bbc36567f9619175", +"motherduck": "b694aa93969368381cb9d9bd", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b694aa93969368381cb9d9bd", "postgresql": "b694aa93969368381cb9d9bd", "snowflake": "58d5f123164e58c358315897", @@ -4326,8 +4326,8 @@ "databricks": "e6ffdece2977be55da73b59a", "duckdb": "c50a316d091c821866e4bfba", "generic": "e6ffdece2977be55da73b59a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e6ffdece2977be55da73b59a", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -4343,8 +4343,8 @@ "databricks": "a49cf160806465e17f3bb015", "duckdb": "2a0e1cbf59d282b31816d003", "generic": "2a0e1cbf59d282b31816d003", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2a0e1cbf59d282b31816d003", +"motherduck": "2a0e1cbf59d282b31816d003", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2a0e1cbf59d282b31816d003", "postgresql": "2a0e1cbf59d282b31816d003", "snowflake": "8cdd1c724939d35c854480a1", @@ -4360,8 +4360,8 @@ "databricks": "2bfa352725f9d8a91250f75e", "duckdb": "2bfa352725f9d8a91250f75e", "generic": "2bfa352725f9d8a91250f75e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2bfa352725f9d8a91250f75e", +"motherduck": "2bfa352725f9d8a91250f75e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2bfa352725f9d8a91250f75e", "postgresql": "2bfa352725f9d8a91250f75e", "snowflake": "2bfa352725f9d8a91250f75e", @@ -4377,8 +4377,8 @@ "databricks": "e33f1ebea03ff1cb2c6e78c4", "duckdb": "401e8475dd39d667a86332fd", "generic": "401e8475dd39d667a86332fd", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "401e8475dd39d667a86332fd", +"motherduck": "401e8475dd39d667a86332fd", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "401e8475dd39d667a86332fd", "postgresql": "401e8475dd39d667a86332fd", "snowflake": "5f8d9b6f22793dc26cf2fd62", @@ -4394,8 +4394,8 @@ "databricks": "5aa51d84ac5e6c7a9306f348", "duckdb": "5aa51d84ac5e6c7a9306f348", "generic": "5aa51d84ac5e6c7a9306f348", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5aa51d84ac5e6c7a9306f348", +"motherduck": "5aa51d84ac5e6c7a9306f348", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5aa51d84ac5e6c7a9306f348", "postgresql": "5aa51d84ac5e6c7a9306f348", "snowflake": "5aa51d84ac5e6c7a9306f348", @@ -4411,8 +4411,8 @@ "databricks": "2bfa352725f9d8a91250f75e", "duckdb": "2bfa352725f9d8a91250f75e", "generic": "2bfa352725f9d8a91250f75e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2bfa352725f9d8a91250f75e", +"motherduck": "2bfa352725f9d8a91250f75e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2bfa352725f9d8a91250f75e", "postgresql": "2bfa352725f9d8a91250f75e", "snowflake": "2bfa352725f9d8a91250f75e", @@ -4428,8 +4428,8 @@ "databricks": "87fed6659d6324c5ef0c6ee2", "duckdb": "c344d36a6751b773df74ee3c", "generic": "87fed6659d6324c5ef0c6ee2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "87fed6659d6324c5ef0c6ee2", +"motherduck": "c344d36a6751b773df74ee3c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "87fed6659d6324c5ef0c6ee2", "postgresql": "87fed6659d6324c5ef0c6ee2", "snowflake": "87fed6659d6324c5ef0c6ee2", @@ -4445,8 +4445,8 @@ "databricks": "de89632e277dbc8c97115419", "duckdb": "de89632e277dbc8c97115419", "generic": "de89632e277dbc8c97115419", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "de89632e277dbc8c97115419", +"motherduck": "de89632e277dbc8c97115419", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5abf4ec799a2ec32fb10da7e", "postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", @@ -4462,8 +4462,8 @@ "databricks": "da0625706f12b764a6912278", "duckdb": "3147a9e06daa6de4a4df5abc", "generic": "da0625706f12b764a6912278", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "da0625706f12b764a6912278", +"motherduck": "3147a9e06daa6de4a4df5abc", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3147a9e06daa6de4a4df5abc", "postgresql": "3147a9e06daa6de4a4df5abc", "snowflake": "67b5874a2c569226c490211f", @@ -4479,8 +4479,8 @@ "databricks": "b2598e21f41a4625fa001e55", "duckdb": "b2598e21f41a4625fa001e55", "generic": "b2598e21f41a4625fa001e55", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b2598e21f41a4625fa001e55", +"motherduck": "b2598e21f41a4625fa001e55", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b2598e21f41a4625fa001e55", "postgresql": "b2598e21f41a4625fa001e55", "snowflake": "b2598e21f41a4625fa001e55", @@ -4496,8 +4496,8 @@ "databricks": "e62af34c2d31bc9ab880629e", "duckdb": "8a7aa7fa75696327320e0add", "generic": "8a7aa7fa75696327320e0add", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8a7aa7fa75696327320e0add", +"motherduck": "8a7aa7fa75696327320e0add", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8a7aa7fa75696327320e0add", "postgresql": "8a7aa7fa75696327320e0add", "snowflake": "8a7aa7fa75696327320e0add", @@ -4513,8 +4513,8 @@ "databricks": "d61c83ef8bfdf043fa672cb5", "duckdb": "d61c83ef8bfdf043fa672cb5", "generic": "d61c83ef8bfdf043fa672cb5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d61c83ef8bfdf043fa672cb5", +"motherduck": "d61c83ef8bfdf043fa672cb5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d61c83ef8bfdf043fa672cb5", "postgresql": "d61c83ef8bfdf043fa672cb5", "snowflake": "97a20344d811fb77c7690863", @@ -4530,8 +4530,8 @@ "databricks": "aae2b1fe6f2d9807ecde412b", "duckdb": "aae2b1fe6f2d9807ecde412b", "generic": "aae2b1fe6f2d9807ecde412b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "aae2b1fe6f2d9807ecde412b", +"motherduck": "aae2b1fe6f2d9807ecde412b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "aae2b1fe6f2d9807ecde412b", "postgresql": "aae2b1fe6f2d9807ecde412b", "snowflake": "aae2b1fe6f2d9807ecde412b", @@ -4547,8 +4547,8 @@ "databricks": "f7fca17b74cc50ae3de8c826", "duckdb": "f7fca17b74cc50ae3de8c826", "generic": "f7fca17b74cc50ae3de8c826", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f7fca17b74cc50ae3de8c826", +"motherduck": "f7fca17b74cc50ae3de8c826", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f7fca17b74cc50ae3de8c826", "postgresql": "f7fca17b74cc50ae3de8c826", "snowflake": "55155766823b6a3a5e882fc2", @@ -4564,8 +4564,8 @@ "databricks": "95050a5e73c2bff179a785b7", "duckdb": "95050a5e73c2bff179a785b7", "generic": "95050a5e73c2bff179a785b7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "95050a5e73c2bff179a785b7", +"motherduck": "95050a5e73c2bff179a785b7", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e197cb69fc5e5754a25d4038", "postgresql": "e197cb69fc5e5754a25d4038", "snowflake": "95050a5e73c2bff179a785b7", @@ -4581,8 +4581,8 @@ "databricks": "ebc821a95b9286e22c2b26d0", "duckdb": "ebc821a95b9286e22c2b26d0", "generic": "ebc821a95b9286e22c2b26d0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ebc821a95b9286e22c2b26d0", +"motherduck": "ebc821a95b9286e22c2b26d0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ebc821a95b9286e22c2b26d0", "postgresql": "ebc821a95b9286e22c2b26d0", "snowflake": "ebc821a95b9286e22c2b26d0", @@ -4598,8 +4598,8 @@ "databricks": "e46d4be44ab9e500e9190d26", "duckdb": "e46d4be44ab9e500e9190d26", "generic": "e46d4be44ab9e500e9190d26", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e46d4be44ab9e500e9190d26", +"motherduck": "e46d4be44ab9e500e9190d26", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "251aba3fef503ce64556c701", "postgresql": "251aba3fef503ce64556c701", "snowflake": "e46d4be44ab9e500e9190d26", @@ -4615,8 +4615,8 @@ "databricks": "9a66317fb8d040bf991502c0", "duckdb": "314ed506f24e92300a425780", "generic": "9a66317fb8d040bf991502c0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9a66317fb8d040bf991502c0", +"motherduck": "314ed506f24e92300a425780", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "314ed506f24e92300a425780", "postgresql": "314ed506f24e92300a425780", "snowflake": "314ed506f24e92300a425780", @@ -4632,8 +4632,8 @@ "databricks": "f4c43a87a78970bca1f88b09", "duckdb": "f4c43a87a78970bca1f88b09", "generic": "f4c43a87a78970bca1f88b09", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f4c43a87a78970bca1f88b09", +"motherduck": "f4c43a87a78970bca1f88b09", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f4c43a87a78970bca1f88b09", "postgresql": "f4c43a87a78970bca1f88b09", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -4649,8 +4649,8 @@ "databricks": "f8d53ecf4ba59d519bb15d38", "duckdb": "f8d53ecf4ba59d519bb15d38", "generic": "f8d53ecf4ba59d519bb15d38", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f8d53ecf4ba59d519bb15d38", +"motherduck": "f8d53ecf4ba59d519bb15d38", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "61d79925ac143bbc1051c8fa", "postgresql": "61d79925ac143bbc1051c8fa", "snowflake": "f8d53ecf4ba59d519bb15d38", @@ -4666,8 +4666,8 @@ "databricks": "d6e37cd37419bfb26e09b080", "duckdb": "d6e37cd37419bfb26e09b080", "generic": "d6e37cd37419bfb26e09b080", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d6e37cd37419bfb26e09b080", +"motherduck": "d6e37cd37419bfb26e09b080", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d6e37cd37419bfb26e09b080", "postgresql": "d6e37cd37419bfb26e09b080", "snowflake": "d6e37cd37419bfb26e09b080", @@ -4683,8 +4683,8 @@ "databricks": "94f6df33eccb97170eccb01b", "duckdb": "94f6df33eccb97170eccb01b", "generic": "94f6df33eccb97170eccb01b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "94f6df33eccb97170eccb01b", +"motherduck": "94f6df33eccb97170eccb01b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "94f6df33eccb97170eccb01b", "postgresql": "94f6df33eccb97170eccb01b", "snowflake": "94f6df33eccb97170eccb01b", @@ -4700,8 +4700,8 @@ "databricks": "8c0b6775b8364e21d86e6e85", "duckdb": "8c0b6775b8364e21d86e6e85", "generic": "8c0b6775b8364e21d86e6e85", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8c0b6775b8364e21d86e6e85", +"motherduck": "8c0b6775b8364e21d86e6e85", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8c0b6775b8364e21d86e6e85", "postgresql": "8c0b6775b8364e21d86e6e85", "snowflake": "8c0b6775b8364e21d86e6e85", @@ -4717,8 +4717,8 @@ "databricks": "2038903a8b2174e3b3525f45", "duckdb": "2038903a8b2174e3b3525f45", "generic": "2038903a8b2174e3b3525f45", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2038903a8b2174e3b3525f45", +"motherduck": "2038903a8b2174e3b3525f45", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2038903a8b2174e3b3525f45", "postgresql": "2038903a8b2174e3b3525f45", "snowflake": "2038903a8b2174e3b3525f45", @@ -4734,8 +4734,8 @@ "databricks": "009acb58d411c47680971462", "duckdb": "bfb659237d70eae4d07a6d10", "generic": "009acb58d411c47680971462", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "009acb58d411c47680971462", +"motherduck": "bfb659237d70eae4d07a6d10", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "bfb659237d70eae4d07a6d10", "postgresql": "bfb659237d70eae4d07a6d10", "snowflake": "bfb659237d70eae4d07a6d10", @@ -4751,8 +4751,8 @@ "databricks": "e0faa139ea3b65b1356bd333", "duckdb": "e0faa139ea3b65b1356bd333", "generic": "e0faa139ea3b65b1356bd333", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e0faa139ea3b65b1356bd333", +"motherduck": "e0faa139ea3b65b1356bd333", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e0faa139ea3b65b1356bd333", "postgresql": "e0faa139ea3b65b1356bd333", "snowflake": "e0faa139ea3b65b1356bd333", @@ -4768,8 +4768,8 @@ "databricks": "a3acd7ba35e81a800f967bd3", "duckdb": "42d4a23d9a52e79d0c3458d3", "generic": "a3acd7ba35e81a800f967bd3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a3acd7ba35e81a800f967bd3", +"motherduck": "42d4a23d9a52e79d0c3458d3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "42d4a23d9a52e79d0c3458d3", "postgresql": "42d4a23d9a52e79d0c3458d3", "snowflake": "a3acd7ba35e81a800f967bd3", @@ -4785,8 +4785,8 @@ "databricks": "2795ccedbfd3fa0bed5fdfcd", "duckdb": "2795ccedbfd3fa0bed5fdfcd", "generic": "2795ccedbfd3fa0bed5fdfcd", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2795ccedbfd3fa0bed5fdfcd", +"motherduck": "2795ccedbfd3fa0bed5fdfcd", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2795ccedbfd3fa0bed5fdfcd", "postgresql": "2795ccedbfd3fa0bed5fdfcd", "snowflake": "2795ccedbfd3fa0bed5fdfcd", @@ -4802,8 +4802,8 @@ "databricks": "de89632e277dbc8c97115419", "duckdb": "5abf4ec799a2ec32fb10da7e", "generic": "de89632e277dbc8c97115419", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "de89632e277dbc8c97115419", +"motherduck": "5abf4ec799a2ec32fb10da7e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5abf4ec799a2ec32fb10da7e", "postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", @@ -4819,8 +4819,8 @@ "databricks": "4a014523269fae92f3ad421a", "duckdb": "4a014523269fae92f3ad421a", "generic": "4a014523269fae92f3ad421a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4a014523269fae92f3ad421a", +"motherduck": "4a014523269fae92f3ad421a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "4a014523269fae92f3ad421a", "postgresql": "4a014523269fae92f3ad421a", "snowflake": "4a014523269fae92f3ad421a", @@ -4836,8 +4836,8 @@ "databricks": "684469f7d4198e43e63072ce", "duckdb": "684469f7d4198e43e63072ce", "generic": "684469f7d4198e43e63072ce", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "684469f7d4198e43e63072ce", +"motherduck": "684469f7d4198e43e63072ce", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "684469f7d4198e43e63072ce", "postgresql": "684469f7d4198e43e63072ce", "snowflake": "684469f7d4198e43e63072ce", @@ -4853,8 +4853,8 @@ "databricks": "e998cd8f0f74af0526b75ff0", "duckdb": "e998cd8f0f74af0526b75ff0", "generic": "e998cd8f0f74af0526b75ff0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e998cd8f0f74af0526b75ff0", +"motherduck": "e998cd8f0f74af0526b75ff0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e998cd8f0f74af0526b75ff0", "postgresql": "e998cd8f0f74af0526b75ff0", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -4870,8 +4870,8 @@ "databricks": "1a804f317a3e9a0016925b04", "duckdb": "110327dc45eac0e8d54a96dc", "generic": "110327dc45eac0e8d54a96dc", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "110327dc45eac0e8d54a96dc", +"motherduck": "110327dc45eac0e8d54a96dc", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "110327dc45eac0e8d54a96dc", "postgresql": "110327dc45eac0e8d54a96dc", "snowflake": "110327dc45eac0e8d54a96dc", @@ -4887,8 +4887,8 @@ "databricks": "4b356542044f1267fcfc487a", "duckdb": "5abf4ec799a2ec32fb10da7e", "generic": "4b356542044f1267fcfc487a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4b356542044f1267fcfc487a", +"motherduck": "5abf4ec799a2ec32fb10da7e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a5656d70e208254d1d3e9b4f", "postgresql": "a5656d70e208254d1d3e9b4f", "snowflake": "a5656d70e208254d1d3e9b4f", @@ -4904,8 +4904,8 @@ "databricks": "d2c97d221599d038d9875873", "duckdb": "f7fca17b74cc50ae3de8c826", "generic": "d2c97d221599d038d9875873", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d2c97d221599d038d9875873", +"motherduck": "f7fca17b74cc50ae3de8c826", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "55155766823b6a3a5e882fc2", "postgresql": "55155766823b6a3a5e882fc2", "snowflake": "55155766823b6a3a5e882fc2", @@ -4921,8 +4921,8 @@ "databricks": "4d02ddd0ab80d0d46bf48a5f", "duckdb": "4d02ddd0ab80d0d46bf48a5f", "generic": "4d02ddd0ab80d0d46bf48a5f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4d02ddd0ab80d0d46bf48a5f", +"motherduck": "4d02ddd0ab80d0d46bf48a5f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "4d02ddd0ab80d0d46bf48a5f", "postgresql": "4d02ddd0ab80d0d46bf48a5f", "snowflake": "4d02ddd0ab80d0d46bf48a5f", @@ -4938,8 +4938,8 @@ "databricks": "18db5afe2c1e80d1ee652b54", "duckdb": "18db5afe2c1e80d1ee652b54", "generic": "18db5afe2c1e80d1ee652b54", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "18db5afe2c1e80d1ee652b54", +"motherduck": "18db5afe2c1e80d1ee652b54", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "47d64a5994d42a2b577166ea", "postgresql": "47d64a5994d42a2b577166ea", "snowflake": "18db5afe2c1e80d1ee652b54", @@ -4955,8 +4955,8 @@ "databricks": "c50a316d091c821866e4bfba", "duckdb": "c50a316d091c821866e4bfba", "generic": "c50a316d091c821866e4bfba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -4972,8 +4972,8 @@ "databricks": "d5af64f521c46f0f5c6d1ad0", "duckdb": "d5af64f521c46f0f5c6d1ad0", "generic": "d5af64f521c46f0f5c6d1ad0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d5af64f521c46f0f5c6d1ad0", +"motherduck": "d5af64f521c46f0f5c6d1ad0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d5af64f521c46f0f5c6d1ad0", "postgresql": "d5af64f521c46f0f5c6d1ad0", "snowflake": "d5af64f521c46f0f5c6d1ad0", @@ -4989,8 +4989,8 @@ "databricks": "6e4f718d80b641c205a88635", "duckdb": "38e28682c0f47e13e642ab1c", "generic": "38e28682c0f47e13e642ab1c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "38e28682c0f47e13e642ab1c", +"motherduck": "38e28682c0f47e13e642ab1c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "38e28682c0f47e13e642ab1c", "postgresql": "38e28682c0f47e13e642ab1c", "snowflake": "41cf14d12db9a99ffd9a4514", @@ -5006,8 +5006,8 @@ "databricks": "558da38ec4d9d5d6c72f60cf", "duckdb": "0a735e1254c48785122ee5a3", "generic": "558da38ec4d9d5d6c72f60cf", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "558da38ec4d9d5d6c72f60cf", +"motherduck": "0a735e1254c48785122ee5a3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "558da38ec4d9d5d6c72f60cf", "postgresql": "558da38ec4d9d5d6c72f60cf", "snowflake": "558da38ec4d9d5d6c72f60cf", @@ -5023,8 +5023,8 @@ "databricks": "c16b4b4f382469fb186a9022", "duckdb": "c16b4b4f382469fb186a9022", "generic": "c16b4b4f382469fb186a9022", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c16b4b4f382469fb186a9022", +"motherduck": "c16b4b4f382469fb186a9022", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c16b4b4f382469fb186a9022", "postgresql": "c16b4b4f382469fb186a9022", "snowflake": "c16b4b4f382469fb186a9022", @@ -5040,8 +5040,8 @@ "databricks": "38b2ec94bf02be40007ce525", "duckdb": "38b2ec94bf02be40007ce525", "generic": "38b2ec94bf02be40007ce525", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "38b2ec94bf02be40007ce525", +"motherduck": "38b2ec94bf02be40007ce525", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "850deea990b52b82b36a9848", "postgresql": "850deea990b52b82b36a9848", "snowflake": "38b2ec94bf02be40007ce525", @@ -5057,8 +5057,8 @@ "databricks": "9e4a50a8aab648a9737f7d0c", "duckdb": "9e4a50a8aab648a9737f7d0c", "generic": "9e4a50a8aab648a9737f7d0c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9e4a50a8aab648a9737f7d0c", +"motherduck": "9e4a50a8aab648a9737f7d0c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "36d29258901a4d8c296baa68", "postgresql": "36d29258901a4d8c296baa68", "snowflake": "9e4a50a8aab648a9737f7d0c", @@ -5074,8 +5074,8 @@ "databricks": "f43dac38f34a1c0618a3f812", "duckdb": "f43dac38f34a1c0618a3f812", "generic": "f43dac38f34a1c0618a3f812", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f43dac38f34a1c0618a3f812", +"motherduck": "f43dac38f34a1c0618a3f812", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f43dac38f34a1c0618a3f812", "postgresql": "f43dac38f34a1c0618a3f812", "snowflake": "f43dac38f34a1c0618a3f812", @@ -5091,8 +5091,8 @@ "databricks": "e8605878c6fffc044556a858", "duckdb": "e8605878c6fffc044556a858", "generic": "e8605878c6fffc044556a858", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e8605878c6fffc044556a858", +"motherduck": "e8605878c6fffc044556a858", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e8605878c6fffc044556a858", "postgresql": "e8605878c6fffc044556a858", "snowflake": "e8605878c6fffc044556a858", @@ -5108,8 +5108,8 @@ "databricks": "e8ef65ce7616ceeafffe41f6", "duckdb": "e8ef65ce7616ceeafffe41f6", "generic": "e8ef65ce7616ceeafffe41f6", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e8ef65ce7616ceeafffe41f6", +"motherduck": "e8ef65ce7616ceeafffe41f6", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e8ef65ce7616ceeafffe41f6", "postgresql": "e8ef65ce7616ceeafffe41f6", "snowflake": "e8ef65ce7616ceeafffe41f6", @@ -5125,8 +5125,8 @@ "databricks": "77ffdb80029bd6e2cca768ad", "duckdb": "77ffdb80029bd6e2cca768ad", "generic": "77ffdb80029bd6e2cca768ad", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "77ffdb80029bd6e2cca768ad", +"motherduck": "77ffdb80029bd6e2cca768ad", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "77ffdb80029bd6e2cca768ad", "postgresql": "77ffdb80029bd6e2cca768ad", "snowflake": "77ffdb80029bd6e2cca768ad", @@ -5142,8 +5142,8 @@ "databricks": "c4346d0257336219f1d26890", "duckdb": "c4346d0257336219f1d26890", "generic": "c4346d0257336219f1d26890", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c4346d0257336219f1d26890", +"motherduck": "c4346d0257336219f1d26890", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c4346d0257336219f1d26890", "postgresql": "c4346d0257336219f1d26890", "snowflake": "4e24a80b20b9dee0cbbfac17", @@ -5159,8 +5159,8 @@ "databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", "generic": "fb4b6f5101f973d16ea8d1f3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fb4b6f5101f973d16ea8d1f3", +"motherduck": "70fb4728c3adcf6dbe4975d5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", @@ -5176,8 +5176,8 @@ "databricks": "3e611643f0beab4953c4e09b", "duckdb": "3e611643f0beab4953c4e09b", "generic": "3e611643f0beab4953c4e09b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3e611643f0beab4953c4e09b", +"motherduck": "3e611643f0beab4953c4e09b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3e611643f0beab4953c4e09b", "postgresql": "3e611643f0beab4953c4e09b", "snowflake": "3e611643f0beab4953c4e09b", @@ -5193,8 +5193,8 @@ "databricks": "776e4bf23c8ef10ffaceade2", "duckdb": "776e4bf23c8ef10ffaceade2", "generic": "776e4bf23c8ef10ffaceade2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "776e4bf23c8ef10ffaceade2", +"motherduck": "776e4bf23c8ef10ffaceade2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5d06b10843e3e6487fb9906a", "postgresql": "5d06b10843e3e6487fb9906a", "snowflake": "776e4bf23c8ef10ffaceade2", @@ -5210,8 +5210,8 @@ "databricks": "3915f3f73328f0838497b2d5", "duckdb": "3915f3f73328f0838497b2d5", "generic": "3915f3f73328f0838497b2d5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3915f3f73328f0838497b2d5", +"motherduck": "3915f3f73328f0838497b2d5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3915f3f73328f0838497b2d5", "postgresql": "3915f3f73328f0838497b2d5", "snowflake": "f43dac38f34a1c0618a3f812", @@ -5227,8 +5227,8 @@ "databricks": "5abf4ec799a2ec32fb10da7e", "duckdb": "5abf4ec799a2ec32fb10da7e", "generic": "5abf4ec799a2ec32fb10da7e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5abf4ec799a2ec32fb10da7e", +"motherduck": "5abf4ec799a2ec32fb10da7e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5abf4ec799a2ec32fb10da7e", "postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "a5656d70e208254d1d3e9b4f", @@ -5244,8 +5244,8 @@ "databricks": "eed0645c1e86fa1a6e35ccaa", "duckdb": "eed0645c1e86fa1a6e35ccaa", "generic": "eed0645c1e86fa1a6e35ccaa", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "eed0645c1e86fa1a6e35ccaa", +"motherduck": "eed0645c1e86fa1a6e35ccaa", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "eed0645c1e86fa1a6e35ccaa", "postgresql": "eed0645c1e86fa1a6e35ccaa", "snowflake": "97a20344d811fb77c7690863", @@ -5261,8 +5261,8 @@ "databricks": "e6ffdece2977be55da73b59a", "duckdb": "c50a316d091c821866e4bfba", "generic": "e6ffdece2977be55da73b59a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e6ffdece2977be55da73b59a", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -5278,8 +5278,8 @@ "databricks": "962ea05bab624c1f8c259a65", "duckdb": "962ea05bab624c1f8c259a65", "generic": "962ea05bab624c1f8c259a65", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "962ea05bab624c1f8c259a65", +"motherduck": "962ea05bab624c1f8c259a65", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "962ea05bab624c1f8c259a65", "postgresql": "962ea05bab624c1f8c259a65", "snowflake": "962ea05bab624c1f8c259a65", @@ -5295,8 +5295,8 @@ "databricks": "d638c2852078bf3facb9dc9e", "duckdb": "207acc46dcc150502155f4a3", "generic": "d638c2852078bf3facb9dc9e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "207acc46dcc150502155f4a3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "6c49aec265f259dd95b5310e", "postgresql": "6c49aec265f259dd95b5310e", "snowflake": "ef2c05747b795f92c0b523ee", @@ -5312,8 +5312,8 @@ "databricks": "e2879cf4770e7f48d2dd704a", "duckdb": "9d006c5299feb04426ce31ac", "generic": "e2879cf4770e7f48d2dd704a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e2879cf4770e7f48d2dd704a", +"motherduck": "9d006c5299feb04426ce31ac", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2d4aea8dec5521e258596f3d", "postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", @@ -5329,8 +5329,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1eb7fb4e8cf75773c5573562", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1eb7fb4e8cf75773c5573562", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -5346,8 +5346,8 @@ "databricks": "6a726882dbf443e39240fc33", "duckdb": "6a726882dbf443e39240fc33", "generic": "6a726882dbf443e39240fc33", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6a726882dbf443e39240fc33", +"motherduck": "6a726882dbf443e39240fc33", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "6a726882dbf443e39240fc33", "postgresql": "6a726882dbf443e39240fc33", "snowflake": "6a726882dbf443e39240fc33", @@ -5363,8 +5363,8 @@ "databricks": "c4860fb394ecca5e4515f1c9", "duckdb": "c4860fb394ecca5e4515f1c9", "generic": "c4860fb394ecca5e4515f1c9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c4860fb394ecca5e4515f1c9", +"motherduck": "c4860fb394ecca5e4515f1c9", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c4860fb394ecca5e4515f1c9", "postgresql": "c4860fb394ecca5e4515f1c9", "snowflake": "c4860fb394ecca5e4515f1c9", @@ -5380,8 +5380,8 @@ "databricks": "9a63ad6168418af23d9b6fd1", "duckdb": "363159f251e264604338e690", "generic": "9a63ad6168418af23d9b6fd1", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9a63ad6168418af23d9b6fd1", +"motherduck": "363159f251e264604338e690", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f276e907770196606577c932", "postgresql": "f276e907770196606577c932", "snowflake": "363159f251e264604338e690", @@ -5397,8 +5397,8 @@ "databricks": "fe1d4d12e4b12d78f4ce545d", "duckdb": "fe1d4d12e4b12d78f4ce545d", "generic": "fe1d4d12e4b12d78f4ce545d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fe1d4d12e4b12d78f4ce545d", +"motherduck": "fe1d4d12e4b12d78f4ce545d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "fe1d4d12e4b12d78f4ce545d", "postgresql": "fe1d4d12e4b12d78f4ce545d", "snowflake": "fe1d4d12e4b12d78f4ce545d", @@ -5414,8 +5414,8 @@ "databricks": "5829c1427d562e21c1ed4dfe", "duckdb": "5829c1427d562e21c1ed4dfe", "generic": "5829c1427d562e21c1ed4dfe", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5829c1427d562e21c1ed4dfe", +"motherduck": "5829c1427d562e21c1ed4dfe", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "348b6977d03b636ecd10e63d", "postgresql": "348b6977d03b636ecd10e63d", "snowflake": "5829c1427d562e21c1ed4dfe", @@ -5431,8 +5431,8 @@ "databricks": "79278352c17c9de0f5a28af5", "duckdb": "79278352c17c9de0f5a28af5", "generic": "79278352c17c9de0f5a28af5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "79278352c17c9de0f5a28af5", +"motherduck": "79278352c17c9de0f5a28af5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "79278352c17c9de0f5a28af5", "postgresql": "79278352c17c9de0f5a28af5", "snowflake": "97a20344d811fb77c7690863", @@ -5448,8 +5448,8 @@ "databricks": "87673dc143bcd33d8688dc34", "duckdb": "e3ba57c3380e7ca53675e905", "generic": "87673dc143bcd33d8688dc34", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "87673dc143bcd33d8688dc34", +"motherduck": "e3ba57c3380e7ca53675e905", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "87673dc143bcd33d8688dc34", "postgresql": "87673dc143bcd33d8688dc34", "snowflake": "0161e12cd4d2efd1b5557348", @@ -5465,8 +5465,8 @@ "databricks": "70f3d045595fbd43219efb36", "duckdb": "1fe8a2d5a41383bd8e502808", "generic": "70f3d045595fbd43219efb36", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "70f3d045595fbd43219efb36", +"motherduck": "1fe8a2d5a41383bd8e502808", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3c17ab67cd8c034fc4911ef2", "postgresql": "3c17ab67cd8c034fc4911ef2", "snowflake": "1fe8a2d5a41383bd8e502808", @@ -5482,8 +5482,8 @@ "databricks": "e46d4be44ab9e500e9190d26", "duckdb": "e46d4be44ab9e500e9190d26", "generic": "e46d4be44ab9e500e9190d26", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e46d4be44ab9e500e9190d26", +"motherduck": "e46d4be44ab9e500e9190d26", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "251aba3fef503ce64556c701", "postgresql": "251aba3fef503ce64556c701", "snowflake": "e46d4be44ab9e500e9190d26", @@ -5499,8 +5499,8 @@ "databricks": "1c58068460e5c94bc2f83d53", "duckdb": "1c58068460e5c94bc2f83d53", "generic": "1c58068460e5c94bc2f83d53", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1c58068460e5c94bc2f83d53", +"motherduck": "1c58068460e5c94bc2f83d53", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1c58068460e5c94bc2f83d53", "postgresql": "1c58068460e5c94bc2f83d53", "snowflake": "1c58068460e5c94bc2f83d53", @@ -5516,8 +5516,8 @@ "databricks": "79278352c17c9de0f5a28af5", "duckdb": "79278352c17c9de0f5a28af5", "generic": "79278352c17c9de0f5a28af5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "79278352c17c9de0f5a28af5", +"motherduck": "79278352c17c9de0f5a28af5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "79278352c17c9de0f5a28af5", "postgresql": "79278352c17c9de0f5a28af5", "snowflake": "97a20344d811fb77c7690863", @@ -5533,8 +5533,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1eb7fb4e8cf75773c5573562", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1eb7fb4e8cf75773c5573562", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -5550,8 +5550,8 @@ "databricks": "26e762643db86e7646e57cba", "duckdb": "26e762643db86e7646e57cba", "generic": "26e762643db86e7646e57cba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "26e762643db86e7646e57cba", +"motherduck": "26e762643db86e7646e57cba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "26e762643db86e7646e57cba", "postgresql": "26e762643db86e7646e57cba", "snowflake": "26e762643db86e7646e57cba", @@ -5567,8 +5567,8 @@ "databricks": "19a1a80ffc06169f32694de8", "duckdb": "19a1a80ffc06169f32694de8", "generic": "19a1a80ffc06169f32694de8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "19a1a80ffc06169f32694de8", +"motherduck": "19a1a80ffc06169f32694de8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ab1f5f46e69284778bd66d58", "postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", @@ -5584,8 +5584,8 @@ "databricks": "7c0baf18eb7c47a1c83b71c3", "duckdb": "243652b8dd9a0f3ac3501ed4", "generic": "f74ed75cd28860b40bc2c2e4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "243652b8dd9a0f3ac3501ed4", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "240398aee89da87a2f8274c9", "postgresql": "240398aee89da87a2f8274c9", "snowflake": "7422d281eba97abeec05a617", @@ -5601,8 +5601,8 @@ "databricks": "1bdf5908ca249b2331a0427c", "duckdb": "1bdf5908ca249b2331a0427c", "generic": "1bdf5908ca249b2331a0427c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1bdf5908ca249b2331a0427c", +"motherduck": "1bdf5908ca249b2331a0427c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1bdf5908ca249b2331a0427c", "postgresql": "1bdf5908ca249b2331a0427c", "snowflake": "8ff614b7306c0edbe36ce00b", @@ -5618,8 +5618,8 @@ "databricks": "d725148d39793a0cbb9a6013", "duckdb": "d725148d39793a0cbb9a6013", "generic": "d725148d39793a0cbb9a6013", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d725148d39793a0cbb9a6013", +"motherduck": "d725148d39793a0cbb9a6013", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d725148d39793a0cbb9a6013", "postgresql": "d725148d39793a0cbb9a6013", "snowflake": "d725148d39793a0cbb9a6013", @@ -5635,8 +5635,8 @@ "databricks": "fe0bedb7dda0e9247a51fdee", "duckdb": "fe0bedb7dda0e9247a51fdee", "generic": "fe0bedb7dda0e9247a51fdee", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fe0bedb7dda0e9247a51fdee", +"motherduck": "fe0bedb7dda0e9247a51fdee", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "fe0bedb7dda0e9247a51fdee", "postgresql": "fe0bedb7dda0e9247a51fdee", "snowflake": "fe0bedb7dda0e9247a51fdee", @@ -5652,8 +5652,8 @@ "databricks": "8441d00030df06ad6a5534d3", "duckdb": "2c44dc35fd1ef8701101802e", "generic": "8441d00030df06ad6a5534d3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8441d00030df06ad6a5534d3", +"motherduck": "2c44dc35fd1ef8701101802e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8441d00030df06ad6a5534d3", "postgresql": "8441d00030df06ad6a5534d3", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -5669,8 +5669,8 @@ "databricks": "c4346d0257336219f1d26890", "duckdb": "c4346d0257336219f1d26890", "generic": "c4346d0257336219f1d26890", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c4346d0257336219f1d26890", +"motherduck": "c4346d0257336219f1d26890", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "19bdd92ea0cc5fb028657510", "postgresql": "19bdd92ea0cc5fb028657510", "snowflake": "4e24a80b20b9dee0cbbfac17", @@ -5686,8 +5686,8 @@ "databricks": "73ed909ac0c80ba48ae6d17c", "duckdb": "73ed909ac0c80ba48ae6d17c", "generic": "73ed909ac0c80ba48ae6d17c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "73ed909ac0c80ba48ae6d17c", +"motherduck": "73ed909ac0c80ba48ae6d17c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "73ed909ac0c80ba48ae6d17c", "postgresql": "73ed909ac0c80ba48ae6d17c", "snowflake": "73ed909ac0c80ba48ae6d17c", @@ -5703,8 +5703,8 @@ "databricks": "445eba49a8d5ccbd6775e1b6", "duckdb": "445eba49a8d5ccbd6775e1b6", "generic": "445eba49a8d5ccbd6775e1b6", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "445eba49a8d5ccbd6775e1b6", +"motherduck": "445eba49a8d5ccbd6775e1b6", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "445eba49a8d5ccbd6775e1b6", "postgresql": "445eba49a8d5ccbd6775e1b6", "snowflake": "445eba49a8d5ccbd6775e1b6", @@ -5720,8 +5720,8 @@ "databricks": "097487e169d70127caa10537", "duckdb": "097487e169d70127caa10537", "generic": "097487e169d70127caa10537", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "097487e169d70127caa10537", +"motherduck": "097487e169d70127caa10537", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ff580202d964731766475fad", "postgresql": "ff580202d964731766475fad", "snowflake": "097487e169d70127caa10537", @@ -5737,8 +5737,8 @@ "databricks": "5aa51d84ac5e6c7a9306f348", "duckdb": "5aa51d84ac5e6c7a9306f348", "generic": "5aa51d84ac5e6c7a9306f348", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5aa51d84ac5e6c7a9306f348", +"motherduck": "5aa51d84ac5e6c7a9306f348", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5aa51d84ac5e6c7a9306f348", "postgresql": "5aa51d84ac5e6c7a9306f348", "snowflake": "5aa51d84ac5e6c7a9306f348", @@ -5754,8 +5754,8 @@ "databricks": "383310937b9286f8d0206ea4", "duckdb": "383310937b9286f8d0206ea4", "generic": "383310937b9286f8d0206ea4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "383310937b9286f8d0206ea4", +"motherduck": "383310937b9286f8d0206ea4", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "383310937b9286f8d0206ea4", "postgresql": "383310937b9286f8d0206ea4", "snowflake": "383310937b9286f8d0206ea4", @@ -5771,8 +5771,8 @@ "databricks": "e3bc1b8bad22d8104417b00f", "duckdb": "3efe8bb57c98fcbe74096b3c", "generic": "3efe8bb57c98fcbe74096b3c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1d8b945fd793ecdcacb90c21", +"motherduck": "3efe8bb57c98fcbe74096b3c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "111af35848fe500ec2ae54b2", "postgresql": "111af35848fe500ec2ae54b2", "snowflake": "1d8b945fd793ecdcacb90c21", @@ -5788,8 +5788,8 @@ "databricks": "a428302e4848ffe877284c84", "duckdb": "a428302e4848ffe877284c84", "generic": "a428302e4848ffe877284c84", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a428302e4848ffe877284c84", +"motherduck": "a428302e4848ffe877284c84", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a428302e4848ffe877284c84", "postgresql": "a428302e4848ffe877284c84", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -5805,8 +5805,8 @@ "databricks": "bbb97828cd28c64282709796", "duckdb": "bbb97828cd28c64282709796", "generic": "bbb97828cd28c64282709796", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "bbb97828cd28c64282709796", +"motherduck": "bbb97828cd28c64282709796", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b210f8eb26c37b422f786c6b", "postgresql": "b210f8eb26c37b422f786c6b", "snowflake": "bbb97828cd28c64282709796", @@ -5822,8 +5822,8 @@ "databricks": "33edbb37fe70dd760e97e937", "duckdb": "33edbb37fe70dd760e97e937", "generic": "33edbb37fe70dd760e97e937", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "33edbb37fe70dd760e97e937", +"motherduck": "33edbb37fe70dd760e97e937", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "33edbb37fe70dd760e97e937", "postgresql": "33edbb37fe70dd760e97e937", "snowflake": "33edbb37fe70dd760e97e937", @@ -5839,8 +5839,8 @@ "databricks": "19a1a80ffc06169f32694de8", "duckdb": "19a1a80ffc06169f32694de8", "generic": "19a1a80ffc06169f32694de8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "19a1a80ffc06169f32694de8", +"motherduck": "19a1a80ffc06169f32694de8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "19a1a80ffc06169f32694de8", "postgresql": "19a1a80ffc06169f32694de8", "snowflake": "19a1a80ffc06169f32694de8", @@ -5856,8 +5856,8 @@ "databricks": "c50a316d091c821866e4bfba", "duckdb": "c50a316d091c821866e4bfba", "generic": "c50a316d091c821866e4bfba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -5873,8 +5873,8 @@ "databricks": "a2d22becd3f08974db215cfe", "duckdb": "d81d8c19074af0e39239164d", "generic": "a2d22becd3f08974db215cfe", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a2d22becd3f08974db215cfe", +"motherduck": "d81d8c19074af0e39239164d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a2d22becd3f08974db215cfe", "postgresql": "a2d22becd3f08974db215cfe", "snowflake": "1fec266134381c760a254d16", @@ -5890,8 +5890,8 @@ "databricks": "c50a316d091c821866e4bfba", "duckdb": "c50a316d091c821866e4bfba", "generic": "c50a316d091c821866e4bfba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -5907,8 +5907,8 @@ "databricks": "558da38ec4d9d5d6c72f60cf", "duckdb": "0a735e1254c48785122ee5a3", "generic": "558da38ec4d9d5d6c72f60cf", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "558da38ec4d9d5d6c72f60cf", +"motherduck": "0a735e1254c48785122ee5a3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "558da38ec4d9d5d6c72f60cf", "postgresql": "558da38ec4d9d5d6c72f60cf", "snowflake": "558da38ec4d9d5d6c72f60cf", @@ -5924,8 +5924,8 @@ "databricks": "38742651e9a5007a4262fae8", "duckdb": "38742651e9a5007a4262fae8", "generic": "38742651e9a5007a4262fae8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "38742651e9a5007a4262fae8", +"motherduck": "38742651e9a5007a4262fae8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c5bb8c8a6d4b467b51e54a73", "postgresql": "c5bb8c8a6d4b467b51e54a73", "snowflake": "38742651e9a5007a4262fae8", @@ -5941,8 +5941,8 @@ "databricks": "de89632e277dbc8c97115419", "duckdb": "de89632e277dbc8c97115419", "generic": "de89632e277dbc8c97115419", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "de89632e277dbc8c97115419", +"motherduck": "de89632e277dbc8c97115419", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5abf4ec799a2ec32fb10da7e", "postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", @@ -5958,8 +5958,8 @@ "databricks": "f6e9bda6dd824d392effea47", "duckdb": "67646dac6abfc002a1e7cacf", "generic": "cdf6c5188287878061490c2a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5598594492668be60871af14", +"motherduck": "67646dac6abfc002a1e7cacf", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "cdf6c5188287878061490c2a", "postgresql": "cdf6c5188287878061490c2a", "snowflake": "5c643769adf301c8da08643f", @@ -5975,8 +5975,8 @@ "databricks": "e7455ff80a579a99488df79b", "duckdb": "e7455ff80a579a99488df79b", "generic": "e7455ff80a579a99488df79b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e7455ff80a579a99488df79b", +"motherduck": "e7455ff80a579a99488df79b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e7455ff80a579a99488df79b", "postgresql": "e7455ff80a579a99488df79b", "snowflake": "e7455ff80a579a99488df79b", @@ -5992,8 +5992,8 @@ "databricks": "2c6b371b030464f525066893", "duckdb": "2c6b371b030464f525066893", "generic": "2c6b371b030464f525066893", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2c6b371b030464f525066893", +"motherduck": "2c6b371b030464f525066893", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3feb479faeec777c2ade8afd", "postgresql": "3feb479faeec777c2ade8afd", "snowflake": "2c6b371b030464f525066893", @@ -6009,8 +6009,8 @@ "databricks": "881106a4a0d2de0ff7c56373", "duckdb": "881106a4a0d2de0ff7c56373", "generic": "881106a4a0d2de0ff7c56373", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "881106a4a0d2de0ff7c56373", +"motherduck": "881106a4a0d2de0ff7c56373", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "cf167ec395fc843602e3b1e0", "postgresql": "cf167ec395fc843602e3b1e0", "snowflake": "881106a4a0d2de0ff7c56373", @@ -6026,8 +6026,8 @@ "databricks": "461923a0d34defd0c726d6e5", "duckdb": "461923a0d34defd0c726d6e5", "generic": "461923a0d34defd0c726d6e5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "461923a0d34defd0c726d6e5", +"motherduck": "461923a0d34defd0c726d6e5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "461923a0d34defd0c726d6e5", "postgresql": "461923a0d34defd0c726d6e5", "snowflake": "461923a0d34defd0c726d6e5", @@ -6043,8 +6043,8 @@ "databricks": "d2c97d221599d038d9875873", "duckdb": "d2c97d221599d038d9875873", "generic": "d2c97d221599d038d9875873", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d2c97d221599d038d9875873", +"motherduck": "d2c97d221599d038d9875873", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "55155766823b6a3a5e882fc2", "postgresql": "55155766823b6a3a5e882fc2", "snowflake": "d2c97d221599d038d9875873", @@ -6060,8 +6060,8 @@ "databricks": "07aa3fe366bb8a3f270ea3d3", "duckdb": "07aa3fe366bb8a3f270ea3d3", "generic": "07aa3fe366bb8a3f270ea3d3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "07aa3fe366bb8a3f270ea3d3", +"motherduck": "07aa3fe366bb8a3f270ea3d3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "07aa3fe366bb8a3f270ea3d3", "postgresql": "07aa3fe366bb8a3f270ea3d3", "snowflake": "07aa3fe366bb8a3f270ea3d3", @@ -6077,8 +6077,8 @@ "databricks": "d47776cdb5b03cf4b1e7b63c", "duckdb": "d47776cdb5b03cf4b1e7b63c", "generic": "d47776cdb5b03cf4b1e7b63c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d47776cdb5b03cf4b1e7b63c", +"motherduck": "d47776cdb5b03cf4b1e7b63c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d47776cdb5b03cf4b1e7b63c", "postgresql": "d47776cdb5b03cf4b1e7b63c", "snowflake": "d47776cdb5b03cf4b1e7b63c", @@ -6094,8 +6094,8 @@ "databricks": "eed0645c1e86fa1a6e35ccaa", "duckdb": "eed0645c1e86fa1a6e35ccaa", "generic": "eed0645c1e86fa1a6e35ccaa", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "eed0645c1e86fa1a6e35ccaa", +"motherduck": "eed0645c1e86fa1a6e35ccaa", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "eed0645c1e86fa1a6e35ccaa", "postgresql": "eed0645c1e86fa1a6e35ccaa", "snowflake": "97a20344d811fb77c7690863", @@ -6111,8 +6111,8 @@ "databricks": "c9b810639795a7bfab91b98d", "duckdb": "a428302e4848ffe877284c84", "generic": "c9b810639795a7bfab91b98d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c9b810639795a7bfab91b98d", +"motherduck": "a428302e4848ffe877284c84", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a428302e4848ffe877284c84", "postgresql": "a428302e4848ffe877284c84", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -6128,8 +6128,8 @@ "databricks": "3959952046272deb40567543", "duckdb": "3959952046272deb40567543", "generic": "3959952046272deb40567543", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3959952046272deb40567543", +"motherduck": "3959952046272deb40567543", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3959952046272deb40567543", "postgresql": "3959952046272deb40567543", "snowflake": "3959952046272deb40567543", @@ -6145,8 +6145,8 @@ "databricks": "c426d93641829fa3d8f49b8d", "duckdb": "c426d93641829fa3d8f49b8d", "generic": "c426d93641829fa3d8f49b8d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c426d93641829fa3d8f49b8d", +"motherduck": "c426d93641829fa3d8f49b8d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c426d93641829fa3d8f49b8d", "postgresql": "c426d93641829fa3d8f49b8d", "snowflake": "c426d93641829fa3d8f49b8d", @@ -6162,8 +6162,8 @@ "databricks": "da33de41a707d0e067e415fd", "duckdb": "da33de41a707d0e067e415fd", "generic": "da33de41a707d0e067e415fd", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "da33de41a707d0e067e415fd", +"motherduck": "da33de41a707d0e067e415fd", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "da33de41a707d0e067e415fd", "postgresql": "da33de41a707d0e067e415fd", "snowflake": "da33de41a707d0e067e415fd", @@ -6179,8 +6179,8 @@ "databricks": "da0625706f12b764a6912278", "duckdb": "3147a9e06daa6de4a4df5abc", "generic": "da0625706f12b764a6912278", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "da0625706f12b764a6912278", +"motherduck": "3147a9e06daa6de4a4df5abc", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3147a9e06daa6de4a4df5abc", "postgresql": "3147a9e06daa6de4a4df5abc", "snowflake": "67b5874a2c569226c490211f", @@ -6196,8 +6196,8 @@ "databricks": "e2879cf4770e7f48d2dd704a", "duckdb": "e2879cf4770e7f48d2dd704a", "generic": "e2879cf4770e7f48d2dd704a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e2879cf4770e7f48d2dd704a", +"motherduck": "e2879cf4770e7f48d2dd704a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2d4aea8dec5521e258596f3d", "postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", @@ -6213,8 +6213,8 @@ "databricks": "c50a316d091c821866e4bfba", "duckdb": "c50a316d091c821866e4bfba", "generic": "c50a316d091c821866e4bfba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -6230,8 +6230,8 @@ "databricks": "be15dde1d6e0b4bb3f6da66f", "duckdb": "be15dde1d6e0b4bb3f6da66f", "generic": "be15dde1d6e0b4bb3f6da66f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "be15dde1d6e0b4bb3f6da66f", +"motherduck": "be15dde1d6e0b4bb3f6da66f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "be15dde1d6e0b4bb3f6da66f", "postgresql": "be15dde1d6e0b4bb3f6da66f", "snowflake": "be15dde1d6e0b4bb3f6da66f", @@ -6247,8 +6247,8 @@ "databricks": "5aa51d84ac5e6c7a9306f348", "duckdb": "5aa51d84ac5e6c7a9306f348", "generic": "5aa51d84ac5e6c7a9306f348", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5aa51d84ac5e6c7a9306f348", +"motherduck": "5aa51d84ac5e6c7a9306f348", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5aa51d84ac5e6c7a9306f348", "postgresql": "5aa51d84ac5e6c7a9306f348", "snowflake": "5aa51d84ac5e6c7a9306f348", @@ -6264,8 +6264,8 @@ "databricks": "73151c1398b30e5c5d18f4ce", "duckdb": "73151c1398b30e5c5d18f4ce", "generic": "73151c1398b30e5c5d18f4ce", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "73151c1398b30e5c5d18f4ce", +"motherduck": "73151c1398b30e5c5d18f4ce", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f68b7dbd74ec7c3b9f3066ef", "postgresql": "f68b7dbd74ec7c3b9f3066ef", "snowflake": "73151c1398b30e5c5d18f4ce", @@ -6281,8 +6281,8 @@ "databricks": "e3d9a113277b9c69432b32d1", "duckdb": "e3d9a113277b9c69432b32d1", "generic": "e3d9a113277b9c69432b32d1", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e3d9a113277b9c69432b32d1", +"motherduck": "e3d9a113277b9c69432b32d1", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e3d9a113277b9c69432b32d1", "postgresql": "e3d9a113277b9c69432b32d1", "snowflake": "e3d9a113277b9c69432b32d1", @@ -6298,8 +6298,8 @@ "databricks": "b137c998bdaa2555d912e8fa", "duckdb": "27f973a6b2da27a79f5c0074", "generic": "b137c998bdaa2555d912e8fa", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b137c998bdaa2555d912e8fa", +"motherduck": "27f973a6b2da27a79f5c0074", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b137c998bdaa2555d912e8fa", "postgresql": "b137c998bdaa2555d912e8fa", "snowflake": "b137c998bdaa2555d912e8fa", @@ -6315,8 +6315,8 @@ "databricks": "7a924f1a13854aa3260f3f57", "duckdb": "7a924f1a13854aa3260f3f57", "generic": "7a924f1a13854aa3260f3f57", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "7a924f1a13854aa3260f3f57", +"motherduck": "7a924f1a13854aa3260f3f57", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7a924f1a13854aa3260f3f57", "postgresql": "7a924f1a13854aa3260f3f57", "snowflake": "7a924f1a13854aa3260f3f57", @@ -6332,8 +6332,8 @@ "databricks": "d90a78129f7050a540984f6f", "duckdb": "66f7ca02161349d637fa7d4e", "generic": "66f7ca02161349d637fa7d4e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "66f7ca02161349d637fa7d4e", +"motherduck": "66f7ca02161349d637fa7d4e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "66f7ca02161349d637fa7d4e", "postgresql": "66f7ca02161349d637fa7d4e", "snowflake": "66f7ca02161349d637fa7d4e", @@ -6349,8 +6349,8 @@ "databricks": "104d3e046c47032c37f947b8", "duckdb": "104d3e046c47032c37f947b8", "generic": "104d3e046c47032c37f947b8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "104d3e046c47032c37f947b8", +"motherduck": "104d3e046c47032c37f947b8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "104d3e046c47032c37f947b8", "postgresql": "104d3e046c47032c37f947b8", "snowflake": "104d3e046c47032c37f947b8", @@ -6366,8 +6366,8 @@ "databricks": "79278352c17c9de0f5a28af5", "duckdb": "79278352c17c9de0f5a28af5", "generic": "79278352c17c9de0f5a28af5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "79278352c17c9de0f5a28af5", +"motherduck": "79278352c17c9de0f5a28af5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "79278352c17c9de0f5a28af5", "postgresql": "79278352c17c9de0f5a28af5", "snowflake": "97a20344d811fb77c7690863", @@ -6383,8 +6383,8 @@ "databricks": "79278352c17c9de0f5a28af5", "duckdb": "79278352c17c9de0f5a28af5", "generic": "79278352c17c9de0f5a28af5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "79278352c17c9de0f5a28af5", +"motherduck": "79278352c17c9de0f5a28af5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "79278352c17c9de0f5a28af5", "postgresql": "79278352c17c9de0f5a28af5", "snowflake": "97a20344d811fb77c7690863", @@ -6400,8 +6400,8 @@ "databricks": "f3150219c38dba6b9f922447", "duckdb": "f3150219c38dba6b9f922447", "generic": "f3150219c38dba6b9f922447", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f3150219c38dba6b9f922447", +"motherduck": "f3150219c38dba6b9f922447", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f3150219c38dba6b9f922447", "postgresql": "f3150219c38dba6b9f922447", "snowflake": "f3150219c38dba6b9f922447", @@ -6417,8 +6417,8 @@ "databricks": "f6140be64a327a15b42b923d", "duckdb": "f6140be64a327a15b42b923d", "generic": "f6140be64a327a15b42b923d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f6140be64a327a15b42b923d", +"motherduck": "f6140be64a327a15b42b923d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f6140be64a327a15b42b923d", "postgresql": "f6140be64a327a15b42b923d", "snowflake": "f6140be64a327a15b42b923d", @@ -6434,8 +6434,8 @@ "databricks": "e408f2d8251e1675179187e6", "duckdb": "e408f2d8251e1675179187e6", "generic": "e408f2d8251e1675179187e6", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e408f2d8251e1675179187e6", +"motherduck": "e408f2d8251e1675179187e6", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "04bfa329b4dc3b5c43ad2560", "postgresql": "04bfa329b4dc3b5c43ad2560", "snowflake": "e408f2d8251e1675179187e6", @@ -6451,8 +6451,8 @@ "databricks": "3959952046272deb40567543", "duckdb": "3959952046272deb40567543", "generic": "3959952046272deb40567543", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3959952046272deb40567543", +"motherduck": "3959952046272deb40567543", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3959952046272deb40567543", "postgresql": "3959952046272deb40567543", "snowflake": "3959952046272deb40567543", @@ -6468,8 +6468,8 @@ "databricks": "ebc821a95b9286e22c2b26d0", "duckdb": "ebc821a95b9286e22c2b26d0", "generic": "ebc821a95b9286e22c2b26d0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ebc821a95b9286e22c2b26d0", +"motherduck": "ebc821a95b9286e22c2b26d0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ebc821a95b9286e22c2b26d0", "postgresql": "ebc821a95b9286e22c2b26d0", "snowflake": "ebc821a95b9286e22c2b26d0", @@ -6485,8 +6485,8 @@ "databricks": "2e8f6dfec4597fb9373329e7", "duckdb": "087a9417cb511ff7bcfd6ccb", "generic": "2e8f6dfec4597fb9373329e7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2e8f6dfec4597fb9373329e7", +"motherduck": "087a9417cb511ff7bcfd6ccb", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "383f8922f4b6f167d384037d", "postgresql": "383f8922f4b6f167d384037d", "snowflake": "383f8922f4b6f167d384037d", @@ -6502,8 +6502,8 @@ "databricks": "a428302e4848ffe877284c84", "duckdb": "a428302e4848ffe877284c84", "generic": "a428302e4848ffe877284c84", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a428302e4848ffe877284c84", +"motherduck": "a428302e4848ffe877284c84", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a428302e4848ffe877284c84", "postgresql": "a428302e4848ffe877284c84", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -6519,8 +6519,8 @@ "databricks": "5d0a19a893dbf0c1e6e9f15c", "duckdb": "5d0a19a893dbf0c1e6e9f15c", "generic": "5d0a19a893dbf0c1e6e9f15c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5d0a19a893dbf0c1e6e9f15c", +"motherduck": "5d0a19a893dbf0c1e6e9f15c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5d0a19a893dbf0c1e6e9f15c", "postgresql": "5d0a19a893dbf0c1e6e9f15c", "snowflake": "5d0a19a893dbf0c1e6e9f15c", @@ -6536,8 +6536,8 @@ "databricks": "2460e6e632163bbe7ecaf8a2", "duckdb": "2460e6e632163bbe7ecaf8a2", "generic": "2460e6e632163bbe7ecaf8a2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2460e6e632163bbe7ecaf8a2", +"motherduck": "2460e6e632163bbe7ecaf8a2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2460e6e632163bbe7ecaf8a2", "postgresql": "2460e6e632163bbe7ecaf8a2", "snowflake": "2460e6e632163bbe7ecaf8a2", @@ -6553,8 +6553,8 @@ "databricks": "38b2ec94bf02be40007ce525", "duckdb": "38b2ec94bf02be40007ce525", "generic": "38b2ec94bf02be40007ce525", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "38b2ec94bf02be40007ce525", +"motherduck": "38b2ec94bf02be40007ce525", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "850deea990b52b82b36a9848", "postgresql": "850deea990b52b82b36a9848", "snowflake": "38b2ec94bf02be40007ce525", @@ -6570,8 +6570,8 @@ "databricks": "3b0d47536653277b7d98e328", "duckdb": "3b0d47536653277b7d98e328", "generic": "3b0d47536653277b7d98e328", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3b0d47536653277b7d98e328", +"motherduck": "3b0d47536653277b7d98e328", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3b0d47536653277b7d98e328", "postgresql": "3b0d47536653277b7d98e328", "snowflake": "3b0d47536653277b7d98e328", @@ -6587,8 +6587,8 @@ "databricks": "8352e528bde15549f0d0d870", "duckdb": "da08a000bb111759556a61cc", "generic": "df518e3c53cd9a166efacdb4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "73477f3a6e7e1fd4288ea98f", +"motherduck": "da08a000bb111759556a61cc", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "df518e3c53cd9a166efacdb4", "postgresql": "df518e3c53cd9a166efacdb4", "snowflake": "58581fd57ff3a9d0f74420dc", @@ -6604,8 +6604,8 @@ "databricks": "1f9deff638e476a7f168220b", "duckdb": "1f9deff638e476a7f168220b", "generic": "1f9deff638e476a7f168220b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1f9deff638e476a7f168220b", +"motherduck": "1f9deff638e476a7f168220b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1f9deff638e476a7f168220b", "postgresql": "1f9deff638e476a7f168220b", "snowflake": "1f9deff638e476a7f168220b", @@ -6621,8 +6621,8 @@ "databricks": "ebfad3a0b99f87bcac831ded", "duckdb": "ebfad3a0b99f87bcac831ded", "generic": "ebfad3a0b99f87bcac831ded", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ebfad3a0b99f87bcac831ded", +"motherduck": "ebfad3a0b99f87bcac831ded", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ebfad3a0b99f87bcac831ded", "postgresql": "ebfad3a0b99f87bcac831ded", "snowflake": "ebfad3a0b99f87bcac831ded", @@ -6638,8 +6638,8 @@ "databricks": "4281b54de06f589b3507035a", "duckdb": "5620e893a908c6743b863bf6", "generic": "5620e893a908c6743b863bf6", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5620e893a908c6743b863bf6", +"motherduck": "5620e893a908c6743b863bf6", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5620e893a908c6743b863bf6", "postgresql": "5620e893a908c6743b863bf6", "snowflake": "5620e893a908c6743b863bf6", @@ -6655,8 +6655,8 @@ "databricks": "48b9d23d0bb10c99b1325f6b", "duckdb": "48b9d23d0bb10c99b1325f6b", "generic": "48b9d23d0bb10c99b1325f6b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "48b9d23d0bb10c99b1325f6b", +"motherduck": "48b9d23d0bb10c99b1325f6b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "48b9d23d0bb10c99b1325f6b", "postgresql": "48b9d23d0bb10c99b1325f6b", "snowflake": "48b9d23d0bb10c99b1325f6b", @@ -6672,8 +6672,8 @@ "databricks": "8f0cf40e26c5ef0cbf1da302", "duckdb": "024a48135994929163b8f97b", "generic": "8f0cf40e26c5ef0cbf1da302", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "024a48135994929163b8f97b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "024a48135994929163b8f97b", "postgresql": "024a48135994929163b8f97b", "snowflake": "cc36c0fadc1a5838c69f0b9e", @@ -6689,8 +6689,8 @@ "databricks": "4d0f0db1a550ed1e0da1a043", "duckdb": "4d0f0db1a550ed1e0da1a043", "generic": "4d0f0db1a550ed1e0da1a043", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4d0f0db1a550ed1e0da1a043", +"motherduck": "4d0f0db1a550ed1e0da1a043", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e47320d1030c166a05193bc4", "postgresql": "e47320d1030c166a05193bc4", "snowflake": "4d0f0db1a550ed1e0da1a043", @@ -6706,8 +6706,8 @@ "databricks": "1275b5b0c52f69cf592c004b", "duckdb": "1275b5b0c52f69cf592c004b", "generic": "1275b5b0c52f69cf592c004b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1275b5b0c52f69cf592c004b", +"motherduck": "1275b5b0c52f69cf592c004b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1275b5b0c52f69cf592c004b", "postgresql": "1275b5b0c52f69cf592c004b", "snowflake": "1275b5b0c52f69cf592c004b", @@ -6723,8 +6723,8 @@ "databricks": "fb5907c56dd752ac5f1dbd95", "duckdb": "fb5907c56dd752ac5f1dbd95", "generic": "fb5907c56dd752ac5f1dbd95", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fb5907c56dd752ac5f1dbd95", +"motherduck": "fb5907c56dd752ac5f1dbd95", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "fb5907c56dd752ac5f1dbd95", "postgresql": "fb5907c56dd752ac5f1dbd95", "snowflake": "fb5907c56dd752ac5f1dbd95", @@ -6740,8 +6740,8 @@ "databricks": "c82815abe30b3d099903f838", "duckdb": "c82815abe30b3d099903f838", "generic": "c82815abe30b3d099903f838", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c82815abe30b3d099903f838", +"motherduck": "c82815abe30b3d099903f838", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c82815abe30b3d099903f838", "postgresql": "c82815abe30b3d099903f838", "snowflake": "c82815abe30b3d099903f838", @@ -6757,8 +6757,8 @@ "databricks": "182a094bae8a9a33f67845ad", "duckdb": "182a094bae8a9a33f67845ad", "generic": "182a094bae8a9a33f67845ad", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "182a094bae8a9a33f67845ad", +"motherduck": "182a094bae8a9a33f67845ad", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "182a094bae8a9a33f67845ad", "postgresql": "182a094bae8a9a33f67845ad", "snowflake": "182a094bae8a9a33f67845ad", @@ -6774,8 +6774,8 @@ "databricks": "f4c43a87a78970bca1f88b09", "duckdb": "f4c43a87a78970bca1f88b09", "generic": "f4c43a87a78970bca1f88b09", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f4c43a87a78970bca1f88b09", +"motherduck": "f4c43a87a78970bca1f88b09", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f4c43a87a78970bca1f88b09", "postgresql": "f4c43a87a78970bca1f88b09", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -6791,8 +6791,8 @@ "databricks": "330fec4d6c49ac503e302b43", "duckdb": "330fec4d6c49ac503e302b43", "generic": "330fec4d6c49ac503e302b43", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "330fec4d6c49ac503e302b43", +"motherduck": "330fec4d6c49ac503e302b43", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f38ef32a483f1c4f89ae309e", "postgresql": "f38ef32a483f1c4f89ae309e", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -6808,8 +6808,8 @@ "databricks": "669084d1ea704ae5b8ee05c2", "duckdb": "669084d1ea704ae5b8ee05c2", "generic": "669084d1ea704ae5b8ee05c2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "669084d1ea704ae5b8ee05c2", +"motherduck": "669084d1ea704ae5b8ee05c2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "93c1b5b5960e26479baf88d0", "postgresql": "93c1b5b5960e26479baf88d0", "snowflake": "669084d1ea704ae5b8ee05c2", @@ -6825,8 +6825,8 @@ "databricks": "8203bd5901a8ec71ff7c103e", "duckdb": "8203bd5901a8ec71ff7c103e", "generic": "8203bd5901a8ec71ff7c103e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8203bd5901a8ec71ff7c103e", +"motherduck": "8203bd5901a8ec71ff7c103e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8203bd5901a8ec71ff7c103e", "postgresql": "8203bd5901a8ec71ff7c103e", "snowflake": "8203bd5901a8ec71ff7c103e", @@ -6842,8 +6842,8 @@ "databricks": "9ff8ffdba7614214fe44bd7f", "duckdb": "9ff8ffdba7614214fe44bd7f", "generic": "9ff8ffdba7614214fe44bd7f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9ff8ffdba7614214fe44bd7f", +"motherduck": "9ff8ffdba7614214fe44bd7f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "12a93f1979dd590479e084a8", "postgresql": "12a93f1979dd590479e084a8", "snowflake": "9ff8ffdba7614214fe44bd7f", @@ -6859,8 +6859,8 @@ "databricks": "336fd1b7a1f7d8105c29280c", "duckdb": "0baaca265927140414f32b37", "generic": "336fd1b7a1f7d8105c29280c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "336fd1b7a1f7d8105c29280c", +"motherduck": "0baaca265927140414f32b37", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9ea4932505663b43f527adad", "postgresql": "9ea4932505663b43f527adad", "snowflake": "336fd1b7a1f7d8105c29280c", @@ -6876,8 +6876,8 @@ "databricks": "ebe2513b6cb4001e42cc464b", "duckdb": "fa45b7e4a9ef3b5a9c5781af", "generic": "ebe2513b6cb4001e42cc464b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ebe2513b6cb4001e42cc464b", +"motherduck": "fa45b7e4a9ef3b5a9c5781af", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "fa45b7e4a9ef3b5a9c5781af", "postgresql": "fa45b7e4a9ef3b5a9c5781af", "snowflake": "fa45b7e4a9ef3b5a9c5781af", @@ -6893,8 +6893,8 @@ "databricks": "a22797fa0ef22aadba4ac3ec", "duckdb": "a22797fa0ef22aadba4ac3ec", "generic": "a22797fa0ef22aadba4ac3ec", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a22797fa0ef22aadba4ac3ec", +"motherduck": "a22797fa0ef22aadba4ac3ec", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a22797fa0ef22aadba4ac3ec", "postgresql": "a22797fa0ef22aadba4ac3ec", "snowflake": "a22797fa0ef22aadba4ac3ec", @@ -6910,8 +6910,8 @@ "databricks": "881106a4a0d2de0ff7c56373", "duckdb": "881106a4a0d2de0ff7c56373", "generic": "881106a4a0d2de0ff7c56373", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "881106a4a0d2de0ff7c56373", +"motherduck": "881106a4a0d2de0ff7c56373", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "cf167ec395fc843602e3b1e0", "postgresql": "cf167ec395fc843602e3b1e0", "snowflake": "881106a4a0d2de0ff7c56373", @@ -6927,8 +6927,8 @@ "databricks": "2705f19942a4194a72576911", "duckdb": "2705f19942a4194a72576911", "generic": "2705f19942a4194a72576911", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2705f19942a4194a72576911", +"motherduck": "2705f19942a4194a72576911", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5226d7e2a62c7650a87d6e84", "postgresql": "5226d7e2a62c7650a87d6e84", "snowflake": "2705f19942a4194a72576911", @@ -6944,8 +6944,8 @@ "databricks": "79483dab720e706b74936744", "duckdb": "79483dab720e706b74936744", "generic": "79483dab720e706b74936744", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "79483dab720e706b74936744", +"motherduck": "79483dab720e706b74936744", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "79483dab720e706b74936744", "postgresql": "79483dab720e706b74936744", "snowflake": "79483dab720e706b74936744", @@ -6961,8 +6961,8 @@ "databricks": "7032aa8254308ba815883957", "duckdb": "7032aa8254308ba815883957", "generic": "7032aa8254308ba815883957", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "7032aa8254308ba815883957", +"motherduck": "7032aa8254308ba815883957", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7032aa8254308ba815883957", "postgresql": "7032aa8254308ba815883957", "snowflake": "7032aa8254308ba815883957", @@ -6978,8 +6978,8 @@ "databricks": "6f763a99e9c69109526b40c0", "duckdb": "6f763a99e9c69109526b40c0", "generic": "6f763a99e9c69109526b40c0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6f763a99e9c69109526b40c0", +"motherduck": "6f763a99e9c69109526b40c0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5226d7e2a62c7650a87d6e84", "postgresql": "5226d7e2a62c7650a87d6e84", "snowflake": "2705f19942a4194a72576911", @@ -6995,8 +6995,8 @@ "databricks": "93edc94c44e0732b1be558b1", "duckdb": "d9a22eb846c0561f4d74700b", "generic": "93edc94c44e0732b1be558b1", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "93edc94c44e0732b1be558b1", +"motherduck": "d9a22eb846c0561f4d74700b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d9a22eb846c0561f4d74700b", "postgresql": "d9a22eb846c0561f4d74700b", "snowflake": "9b5f44707b5f4764b82741be", @@ -7012,8 +7012,8 @@ "databricks": "ce3765fd18ee6aff1394ac23", "duckdb": "ce3765fd18ee6aff1394ac23", "generic": "ce3765fd18ee6aff1394ac23", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ce3765fd18ee6aff1394ac23", +"motherduck": "ce3765fd18ee6aff1394ac23", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ce3765fd18ee6aff1394ac23", "postgresql": "ce3765fd18ee6aff1394ac23", "snowflake": "ce3765fd18ee6aff1394ac23", @@ -7029,8 +7029,8 @@ "databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", "generic": "fb4b6f5101f973d16ea8d1f3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fb4b6f5101f973d16ea8d1f3", +"motherduck": "70fb4728c3adcf6dbe4975d5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", @@ -7046,8 +7046,8 @@ "databricks": "78b2cf275c640af54feeca31", "duckdb": "78b2cf275c640af54feeca31", "generic": "78b2cf275c640af54feeca31", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "78b2cf275c640af54feeca31", +"motherduck": "78b2cf275c640af54feeca31", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "78b2cf275c640af54feeca31", "postgresql": "78b2cf275c640af54feeca31", "snowflake": "8ff614b7306c0edbe36ce00b", @@ -7063,8 +7063,8 @@ "databricks": "87eaac3f093587d1d34762da", "duckdb": "87eaac3f093587d1d34762da", "generic": "87eaac3f093587d1d34762da", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "87eaac3f093587d1d34762da", +"motherduck": "87eaac3f093587d1d34762da", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "87eaac3f093587d1d34762da", "postgresql": "87eaac3f093587d1d34762da", "snowflake": "87eaac3f093587d1d34762da", @@ -7080,8 +7080,8 @@ "databricks": "5d25d5e13f81700741695ed4", "duckdb": "5d25d5e13f81700741695ed4", "generic": "5d25d5e13f81700741695ed4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5d25d5e13f81700741695ed4", +"motherduck": "5d25d5e13f81700741695ed4", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "62131fb5b065f5de2e74a0f1", "postgresql": "62131fb5b065f5de2e74a0f1", "snowflake": "5d25d5e13f81700741695ed4", @@ -7097,8 +7097,8 @@ "databricks": "aab0389e2b5470136660bf8a", "duckdb": "aab0389e2b5470136660bf8a", "generic": "aab0389e2b5470136660bf8a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "aab0389e2b5470136660bf8a", +"motherduck": "aab0389e2b5470136660bf8a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f16105cbc61d547e6c08efc8", "postgresql": "f16105cbc61d547e6c08efc8", "snowflake": "97a20344d811fb77c7690863", @@ -7114,8 +7114,8 @@ "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fe23594c781e79cbb0c8927a", +"motherduck": "fe23594c781e79cbb0c8927a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8a3f094f18fb4047f0febf59", "postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", @@ -7131,8 +7131,8 @@ "databricks": "97cf3d84c55104df9751ec06", "duckdb": "97cf3d84c55104df9751ec06", "generic": "97cf3d84c55104df9751ec06", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "97cf3d84c55104df9751ec06", +"motherduck": "97cf3d84c55104df9751ec06", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "97cf3d84c55104df9751ec06", "postgresql": "97cf3d84c55104df9751ec06", "snowflake": "97cf3d84c55104df9751ec06", @@ -7148,8 +7148,8 @@ "databricks": "0703bf71a54942bdaa28406e", "duckdb": "0703bf71a54942bdaa28406e", "generic": "0703bf71a54942bdaa28406e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0703bf71a54942bdaa28406e", +"motherduck": "0703bf71a54942bdaa28406e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0703bf71a54942bdaa28406e", "postgresql": "0703bf71a54942bdaa28406e", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -7165,8 +7165,8 @@ "databricks": "18db5afe2c1e80d1ee652b54", "duckdb": "18db5afe2c1e80d1ee652b54", "generic": "18db5afe2c1e80d1ee652b54", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "18db5afe2c1e80d1ee652b54", +"motherduck": "18db5afe2c1e80d1ee652b54", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "18db5afe2c1e80d1ee652b54", "postgresql": "18db5afe2c1e80d1ee652b54", "snowflake": "18db5afe2c1e80d1ee652b54", @@ -7182,8 +7182,8 @@ "databricks": "5e46ad7c70253689a5c6bb7f", "duckdb": "5e46ad7c70253689a5c6bb7f", "generic": "5e46ad7c70253689a5c6bb7f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5e46ad7c70253689a5c6bb7f", +"motherduck": "5e46ad7c70253689a5c6bb7f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5e46ad7c70253689a5c6bb7f", "postgresql": "5e46ad7c70253689a5c6bb7f", "snowflake": "5e46ad7c70253689a5c6bb7f", @@ -7199,8 +7199,8 @@ "databricks": "da0625706f12b764a6912278", "duckdb": "3147a9e06daa6de4a4df5abc", "generic": "da0625706f12b764a6912278", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "da0625706f12b764a6912278", +"motherduck": "3147a9e06daa6de4a4df5abc", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3147a9e06daa6de4a4df5abc", "postgresql": "3147a9e06daa6de4a4df5abc", "snowflake": "67b5874a2c569226c490211f", @@ -7216,8 +7216,8 @@ "databricks": "8789309b90fc585ddbbe90f9", "duckdb": "8789309b90fc585ddbbe90f9", "generic": "8789309b90fc585ddbbe90f9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8789309b90fc585ddbbe90f9", +"motherduck": "8789309b90fc585ddbbe90f9", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8789309b90fc585ddbbe90f9", "postgresql": "8789309b90fc585ddbbe90f9", "snowflake": "8789309b90fc585ddbbe90f9", @@ -7233,8 +7233,8 @@ "databricks": "5990107b647ce24001477492", "duckdb": "5990107b647ce24001477492", "generic": "5990107b647ce24001477492", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5990107b647ce24001477492", +"motherduck": "5990107b647ce24001477492", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5990107b647ce24001477492", "postgresql": "5990107b647ce24001477492", "snowflake": "5990107b647ce24001477492", @@ -7250,8 +7250,8 @@ "databricks": "8441d00030df06ad6a5534d3", "duckdb": "8441d00030df06ad6a5534d3", "generic": "8441d00030df06ad6a5534d3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8441d00030df06ad6a5534d3", +"motherduck": "8441d00030df06ad6a5534d3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8441d00030df06ad6a5534d3", "postgresql": "8441d00030df06ad6a5534d3", "snowflake": "97a20344d811fb77c7690863", @@ -7267,8 +7267,8 @@ "databricks": "f15c56475abfc73488abe63b", "duckdb": "f15c56475abfc73488abe63b", "generic": "f15c56475abfc73488abe63b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f15c56475abfc73488abe63b", +"motherduck": "f15c56475abfc73488abe63b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f15c56475abfc73488abe63b", "postgresql": "f15c56475abfc73488abe63b", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -7284,8 +7284,8 @@ "databricks": "e2879cf4770e7f48d2dd704a", "duckdb": "e2879cf4770e7f48d2dd704a", "generic": "e2879cf4770e7f48d2dd704a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e2879cf4770e7f48d2dd704a", +"motherduck": "e2879cf4770e7f48d2dd704a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2d4aea8dec5521e258596f3d", "postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", @@ -7301,8 +7301,8 @@ "databricks": "45310b1154cae517b4caedff", "duckdb": "8317b957016bdf11135b4836", "generic": "45310b1154cae517b4caedff", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "45310b1154cae517b4caedff", +"motherduck": "8317b957016bdf11135b4836", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "45310b1154cae517b4caedff", "postgresql": "45310b1154cae517b4caedff", "snowflake": "c2976683dc7003c9f4e8d7d0", @@ -7318,8 +7318,8 @@ "databricks": "325bc7668cc9012c12196cbf", "duckdb": "f77bad2342e566026c6930f9", "generic": "325bc7668cc9012c12196cbf", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "325bc7668cc9012c12196cbf", +"motherduck": "f77bad2342e566026c6930f9", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "6031cfaf0d57e25f8a111524", "postgresql": "6031cfaf0d57e25f8a111524", "snowflake": "6031cfaf0d57e25f8a111524", @@ -7335,8 +7335,8 @@ "databricks": "eea1002d88cc68fad7424aca", "duckdb": "c729c38d2df6efedc6288247", "generic": "eea1002d88cc68fad7424aca", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "eea1002d88cc68fad7424aca", +"motherduck": "c729c38d2df6efedc6288247", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "eea1002d88cc68fad7424aca", "postgresql": "eea1002d88cc68fad7424aca", "snowflake": "eea1002d88cc68fad7424aca", @@ -7352,8 +7352,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1ebac487198b6c90b027faab", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1ebac487198b6c90b027faab", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -7369,8 +7369,8 @@ "databricks": "1a804f317a3e9a0016925b04", "duckdb": "110327dc45eac0e8d54a96dc", "generic": "110327dc45eac0e8d54a96dc", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "110327dc45eac0e8d54a96dc", +"motherduck": "110327dc45eac0e8d54a96dc", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "110327dc45eac0e8d54a96dc", "postgresql": "110327dc45eac0e8d54a96dc", "snowflake": "110327dc45eac0e8d54a96dc", @@ -7386,8 +7386,8 @@ "databricks": "8562dfdaf4f03fa06585b288", "duckdb": "8562dfdaf4f03fa06585b288", "generic": "8562dfdaf4f03fa06585b288", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8562dfdaf4f03fa06585b288", +"motherduck": "8562dfdaf4f03fa06585b288", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8562dfdaf4f03fa06585b288", "postgresql": "8562dfdaf4f03fa06585b288", "snowflake": "8562dfdaf4f03fa06585b288", @@ -7403,8 +7403,8 @@ "databricks": "2bfa352725f9d8a91250f75e", "duckdb": "2bfa352725f9d8a91250f75e", "generic": "2bfa352725f9d8a91250f75e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2bfa352725f9d8a91250f75e", +"motherduck": "2bfa352725f9d8a91250f75e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2bfa352725f9d8a91250f75e", "postgresql": "2bfa352725f9d8a91250f75e", "snowflake": "2bfa352725f9d8a91250f75e", @@ -7420,8 +7420,8 @@ "databricks": "38b2ec94bf02be40007ce525", "duckdb": "38b2ec94bf02be40007ce525", "generic": "38b2ec94bf02be40007ce525", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "38b2ec94bf02be40007ce525", +"motherduck": "38b2ec94bf02be40007ce525", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "850deea990b52b82b36a9848", "postgresql": "850deea990b52b82b36a9848", "snowflake": "38b2ec94bf02be40007ce525", @@ -7437,8 +7437,8 @@ "databricks": "a428302e4848ffe877284c84", "duckdb": "a428302e4848ffe877284c84", "generic": "a428302e4848ffe877284c84", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a428302e4848ffe877284c84", +"motherduck": "a428302e4848ffe877284c84", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a428302e4848ffe877284c84", "postgresql": "a428302e4848ffe877284c84", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -7454,8 +7454,8 @@ "databricks": "0111ff4826a58e7709648841", "duckdb": "a0f9ede1ffaaa874ee2066b3", "generic": "a0f9ede1ffaaa874ee2066b3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a0f9ede1ffaaa874ee2066b3", +"motherduck": "a0f9ede1ffaaa874ee2066b3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "be386e1639549e84c97742c5", "postgresql": "be386e1639549e84c97742c5", "snowflake": "8c43a62af28ed0ed164b472b", @@ -7471,8 +7471,8 @@ "databricks": "733b04338829c5844270d25e", "duckdb": "733b04338829c5844270d25e", "generic": "733b04338829c5844270d25e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "733b04338829c5844270d25e", +"motherduck": "733b04338829c5844270d25e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "733b04338829c5844270d25e", "postgresql": "733b04338829c5844270d25e", "snowflake": "733b04338829c5844270d25e", @@ -7488,8 +7488,8 @@ "databricks": "de89632e277dbc8c97115419", "duckdb": "5abf4ec799a2ec32fb10da7e", "generic": "de89632e277dbc8c97115419", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "de89632e277dbc8c97115419", +"motherduck": "5abf4ec799a2ec32fb10da7e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5abf4ec799a2ec32fb10da7e", "postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "a5656d70e208254d1d3e9b4f", @@ -7505,8 +7505,8 @@ "databricks": "08736485547ba339930697e9", "duckdb": "08736485547ba339930697e9", "generic": "08736485547ba339930697e9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "08736485547ba339930697e9", +"motherduck": "08736485547ba339930697e9", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "eecd452b4f5f3f4133d0bd0a", "postgresql": "eecd452b4f5f3f4133d0bd0a", "snowflake": "08736485547ba339930697e9", @@ -7522,8 +7522,8 @@ "databricks": "7cd31289a5b99b5a5e24492d", "duckdb": "7cd31289a5b99b5a5e24492d", "generic": "7cd31289a5b99b5a5e24492d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "7cd31289a5b99b5a5e24492d", +"motherduck": "7cd31289a5b99b5a5e24492d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c5bdaf88df7ef3d9ae693106", "postgresql": "c5bdaf88df7ef3d9ae693106", "snowflake": "7cd31289a5b99b5a5e24492d", @@ -7539,8 +7539,8 @@ "databricks": "a9d9e03f8e981cec9dca7694", "duckdb": "a9d9e03f8e981cec9dca7694", "generic": "a9d9e03f8e981cec9dca7694", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a9d9e03f8e981cec9dca7694", +"motherduck": "a9d9e03f8e981cec9dca7694", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a9d9e03f8e981cec9dca7694", "postgresql": "a9d9e03f8e981cec9dca7694", "snowflake": "a9d9e03f8e981cec9dca7694", @@ -7556,8 +7556,8 @@ "databricks": "8b8d5f75ba6dec68b4cebc6b", "duckdb": "8b8d5f75ba6dec68b4cebc6b", "generic": "8b8d5f75ba6dec68b4cebc6b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8b8d5f75ba6dec68b4cebc6b", +"motherduck": "8b8d5f75ba6dec68b4cebc6b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7914189ff18b0e70ddf7d042", "postgresql": "7914189ff18b0e70ddf7d042", "snowflake": "8b8d5f75ba6dec68b4cebc6b", @@ -7573,8 +7573,8 @@ "databricks": "3959952046272deb40567543", "duckdb": "3959952046272deb40567543", "generic": "3959952046272deb40567543", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3959952046272deb40567543", +"motherduck": "3959952046272deb40567543", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3959952046272deb40567543", "postgresql": "3959952046272deb40567543", "snowflake": "3959952046272deb40567543", @@ -7590,8 +7590,8 @@ "databricks": "8441d00030df06ad6a5534d3", "duckdb": "c5d56d00f80f43bfec07a7c2", "generic": "8441d00030df06ad6a5534d3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8441d00030df06ad6a5534d3", +"motherduck": "c5d56d00f80f43bfec07a7c2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8441d00030df06ad6a5534d3", "postgresql": "8441d00030df06ad6a5534d3", "snowflake": "97a20344d811fb77c7690863", @@ -7607,8 +7607,8 @@ "databricks": "251aba3fef503ce64556c701", "duckdb": "251aba3fef503ce64556c701", "generic": "251aba3fef503ce64556c701", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "251aba3fef503ce64556c701", +"motherduck": "251aba3fef503ce64556c701", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "251aba3fef503ce64556c701", "postgresql": "251aba3fef503ce64556c701", "snowflake": "c50a316d091c821866e4bfba", @@ -7624,8 +7624,8 @@ "databricks": "5855bf076c2cc936300b1e0a", "duckdb": "5855bf076c2cc936300b1e0a", "generic": "5855bf076c2cc936300b1e0a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5855bf076c2cc936300b1e0a", +"motherduck": "5855bf076c2cc936300b1e0a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5855bf076c2cc936300b1e0a", "postgresql": "5855bf076c2cc936300b1e0a", "snowflake": "5855bf076c2cc936300b1e0a", @@ -7641,8 +7641,8 @@ "databricks": "3f998a9ab68d8b2514840489", "duckdb": "f2ed52f6bf46b4db9343551c", "generic": "3f998a9ab68d8b2514840489", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3f998a9ab68d8b2514840489", +"motherduck": "f2ed52f6bf46b4db9343551c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7ce41cc5375b618de22b6a4e", "postgresql": "7ce41cc5375b618de22b6a4e", "snowflake": "f2ed52f6bf46b4db9343551c", @@ -7658,8 +7658,8 @@ "databricks": "e2879cf4770e7f48d2dd704a", "duckdb": "e2879cf4770e7f48d2dd704a", "generic": "e2879cf4770e7f48d2dd704a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e2879cf4770e7f48d2dd704a", +"motherduck": "e2879cf4770e7f48d2dd704a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2d4aea8dec5521e258596f3d", "postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", @@ -7675,8 +7675,8 @@ "databricks": "42f8fe02ce664d09ff39167c", "duckdb": "42f8fe02ce664d09ff39167c", "generic": "42f8fe02ce664d09ff39167c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "42f8fe02ce664d09ff39167c", +"motherduck": "42f8fe02ce664d09ff39167c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "42f8fe02ce664d09ff39167c", "postgresql": "42f8fe02ce664d09ff39167c", "snowflake": "42f8fe02ce664d09ff39167c", @@ -7692,8 +7692,8 @@ "databricks": "d5ce089ec505733f59838ffd", "duckdb": "4ebb6333caac069be16c795d", "generic": "d5ce089ec505733f59838ffd", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d5ce089ec505733f59838ffd", +"motherduck": "4ebb6333caac069be16c795d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "4ebb6333caac069be16c795d", "postgresql": "4ebb6333caac069be16c795d", "snowflake": "4ebb6333caac069be16c795d", @@ -7709,8 +7709,8 @@ "databricks": "c844c96e1e3ffc72d5f90df9", "duckdb": "8a3f094f18fb4047f0febf59", "generic": "c844c96e1e3ffc72d5f90df9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "28eb0d6de9724959ce5ba43e", +"motherduck": "8a3f094f18fb4047f0febf59", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c844c96e1e3ffc72d5f90df9", "postgresql": "c844c96e1e3ffc72d5f90df9", "snowflake": "c844c96e1e3ffc72d5f90df9", @@ -7726,8 +7726,8 @@ "databricks": "a428302e4848ffe877284c84", "duckdb": "a428302e4848ffe877284c84", "generic": "a428302e4848ffe877284c84", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a428302e4848ffe877284c84", +"motherduck": "a428302e4848ffe877284c84", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a428302e4848ffe877284c84", "postgresql": "a428302e4848ffe877284c84", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -7743,8 +7743,8 @@ "databricks": "8441d00030df06ad6a5534d3", "duckdb": "8441d00030df06ad6a5534d3", "generic": "8441d00030df06ad6a5534d3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8441d00030df06ad6a5534d3", +"motherduck": "8441d00030df06ad6a5534d3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8441d00030df06ad6a5534d3", "postgresql": "8441d00030df06ad6a5534d3", "snowflake": "97a20344d811fb77c7690863", @@ -7760,8 +7760,8 @@ "databricks": "3915f3f73328f0838497b2d5", "duckdb": "3915f3f73328f0838497b2d5", "generic": "3915f3f73328f0838497b2d5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3915f3f73328f0838497b2d5", +"motherduck": "3915f3f73328f0838497b2d5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3915f3f73328f0838497b2d5", "postgresql": "3915f3f73328f0838497b2d5", "snowflake": "f43dac38f34a1c0618a3f812", @@ -7777,8 +7777,8 @@ "databricks": "52e79b501b430a4c2b097344", "duckdb": "52e79b501b430a4c2b097344", "generic": "52e79b501b430a4c2b097344", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "52e79b501b430a4c2b097344", +"motherduck": "52e79b501b430a4c2b097344", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "52e79b501b430a4c2b097344", "postgresql": "52e79b501b430a4c2b097344", "snowflake": "52e79b501b430a4c2b097344", @@ -7794,8 +7794,8 @@ "databricks": "75c45736873ac5c99a62bc20", "duckdb": "13f1401e7cd410600a4c9485", "generic": "75c45736873ac5c99a62bc20", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "75c45736873ac5c99a62bc20", +"motherduck": "13f1401e7cd410600a4c9485", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "13f1401e7cd410600a4c9485", "postgresql": "13f1401e7cd410600a4c9485", "snowflake": "9b5f44707b5f4764b82741be", @@ -7811,8 +7811,8 @@ "databricks": "d2c97d221599d038d9875873", "duckdb": "d2c97d221599d038d9875873", "generic": "d2c97d221599d038d9875873", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d2c97d221599d038d9875873", +"motherduck": "d2c97d221599d038d9875873", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "55155766823b6a3a5e882fc2", "postgresql": "55155766823b6a3a5e882fc2", "snowflake": "d2c97d221599d038d9875873", @@ -7828,8 +7828,8 @@ "databricks": "802f1c1fcb97ddd5679172d2", "duckdb": "802f1c1fcb97ddd5679172d2", "generic": "802f1c1fcb97ddd5679172d2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "802f1c1fcb97ddd5679172d2", +"motherduck": "802f1c1fcb97ddd5679172d2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "802f1c1fcb97ddd5679172d2", "postgresql": "802f1c1fcb97ddd5679172d2", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -7845,8 +7845,8 @@ "databricks": "60a23b7dcae6c79f4de83068", "duckdb": "60a23b7dcae6c79f4de83068", "generic": "60a23b7dcae6c79f4de83068", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "60a23b7dcae6c79f4de83068", +"motherduck": "60a23b7dcae6c79f4de83068", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "60a23b7dcae6c79f4de83068", "postgresql": "60a23b7dcae6c79f4de83068", "snowflake": "60a23b7dcae6c79f4de83068", @@ -7862,8 +7862,8 @@ "databricks": "4b356542044f1267fcfc487a", "duckdb": "4b356542044f1267fcfc487a", "generic": "4b356542044f1267fcfc487a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4b356542044f1267fcfc487a", +"motherduck": "4b356542044f1267fcfc487a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a5656d70e208254d1d3e9b4f", "postgresql": "a5656d70e208254d1d3e9b4f", "snowflake": "4b356542044f1267fcfc487a", @@ -7879,8 +7879,8 @@ "databricks": "3315bb12afea7b182498ce28", "duckdb": "3315bb12afea7b182498ce28", "generic": "3315bb12afea7b182498ce28", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3315bb12afea7b182498ce28", +"motherduck": "3315bb12afea7b182498ce28", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3315bb12afea7b182498ce28", "postgresql": "3315bb12afea7b182498ce28", "snowflake": "3315bb12afea7b182498ce28", @@ -7896,8 +7896,8 @@ "databricks": "916f1a306af797bd328aec64", "duckdb": "2844656bba2e199281ab8d9b", "generic": "2844656bba2e199281ab8d9b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2844656bba2e199281ab8d9b", +"motherduck": "2844656bba2e199281ab8d9b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2844656bba2e199281ab8d9b", "postgresql": "2844656bba2e199281ab8d9b", "snowflake": "2844656bba2e199281ab8d9b", @@ -7913,8 +7913,8 @@ "databricks": "e62af34c2d31bc9ab880629e", "duckdb": "8a7aa7fa75696327320e0add", "generic": "8a7aa7fa75696327320e0add", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8a7aa7fa75696327320e0add", +"motherduck": "8a7aa7fa75696327320e0add", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8a7aa7fa75696327320e0add", "postgresql": "8a7aa7fa75696327320e0add", "snowflake": "8a7aa7fa75696327320e0add", @@ -7930,8 +7930,8 @@ "databricks": "cbea30a905a72d3b17dc7211", "duckdb": "cbea30a905a72d3b17dc7211", "generic": "cbea30a905a72d3b17dc7211", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "cbea30a905a72d3b17dc7211", +"motherduck": "cbea30a905a72d3b17dc7211", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "cbea30a905a72d3b17dc7211", "postgresql": "cbea30a905a72d3b17dc7211", "snowflake": "7d597ede6f7cf27affdde1b0", @@ -7947,8 +7947,8 @@ "databricks": "f15c56475abfc73488abe63b", "duckdb": "f15c56475abfc73488abe63b", "generic": "f15c56475abfc73488abe63b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f15c56475abfc73488abe63b", +"motherduck": "f15c56475abfc73488abe63b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f15c56475abfc73488abe63b", "postgresql": "f15c56475abfc73488abe63b", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -7964,8 +7964,8 @@ "databricks": "e8b6e0bd2ded7b9eec6209e4", "duckdb": "e8b6e0bd2ded7b9eec6209e4", "generic": "e8b6e0bd2ded7b9eec6209e4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e8b6e0bd2ded7b9eec6209e4", +"motherduck": "e8b6e0bd2ded7b9eec6209e4", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e8b6e0bd2ded7b9eec6209e4", "postgresql": "e8b6e0bd2ded7b9eec6209e4", "snowflake": "e8b6e0bd2ded7b9eec6209e4", @@ -7981,8 +7981,8 @@ "databricks": "e3bc1b8bad22d8104417b00f", "duckdb": "1d8b945fd793ecdcacb90c21", "generic": "1d8b945fd793ecdcacb90c21", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1d8b945fd793ecdcacb90c21", +"motherduck": "1d8b945fd793ecdcacb90c21", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3efe8bb57c98fcbe74096b3c", "postgresql": "3efe8bb57c98fcbe74096b3c", "snowflake": "1d8b945fd793ecdcacb90c21", @@ -7998,8 +7998,8 @@ "databricks": "4e10d2f9eb8994ead08d1761", "duckdb": "4e10d2f9eb8994ead08d1761", "generic": "4e10d2f9eb8994ead08d1761", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4e10d2f9eb8994ead08d1761", +"motherduck": "4e10d2f9eb8994ead08d1761", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f7f38c204fc2a5b7e7986a31", "postgresql": "f7f38c204fc2a5b7e7986a31", "snowflake": "4e10d2f9eb8994ead08d1761", @@ -8015,8 +8015,8 @@ "databricks": "3f71ab5f3c1c040da96f2cec", "duckdb": "3f71ab5f3c1c040da96f2cec", "generic": "3f71ab5f3c1c040da96f2cec", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3f71ab5f3c1c040da96f2cec", +"motherduck": "3f71ab5f3c1c040da96f2cec", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3f71ab5f3c1c040da96f2cec", "postgresql": "3f71ab5f3c1c040da96f2cec", "snowflake": "3f71ab5f3c1c040da96f2cec", @@ -8032,8 +8032,8 @@ "databricks": "46ec3e19e05a6cb29854ccac", "duckdb": "35e216b6921bf843994f2eea", "generic": "46ec3e19e05a6cb29854ccac", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "46ec3e19e05a6cb29854ccac", +"motherduck": "35e216b6921bf843994f2eea", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "46ec3e19e05a6cb29854ccac", "postgresql": "46ec3e19e05a6cb29854ccac", "snowflake": "46ec3e19e05a6cb29854ccac", @@ -8049,8 +8049,8 @@ "databricks": "d4b89e61a66aeb076db92954", "duckdb": "7515c0db413239044adef879", "generic": "d4b89e61a66aeb076db92954", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d4b89e61a66aeb076db92954", +"motherduck": "7515c0db413239044adef879", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7515c0db413239044adef879", "postgresql": "7515c0db413239044adef879", "snowflake": "67b5874a2c569226c490211f", @@ -8066,8 +8066,8 @@ "databricks": "a428302e4848ffe877284c84", "duckdb": "a428302e4848ffe877284c84", "generic": "a428302e4848ffe877284c84", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a428302e4848ffe877284c84", +"motherduck": "a428302e4848ffe877284c84", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a428302e4848ffe877284c84", "postgresql": "a428302e4848ffe877284c84", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -8083,8 +8083,8 @@ "databricks": "4662a1758442ce44b830269f", "duckdb": "4662a1758442ce44b830269f", "generic": "4662a1758442ce44b830269f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4662a1758442ce44b830269f", +"motherduck": "4662a1758442ce44b830269f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "4662a1758442ce44b830269f", "postgresql": "4662a1758442ce44b830269f", "snowflake": "4662a1758442ce44b830269f", @@ -8100,8 +8100,8 @@ "databricks": "a13763a505b15f311cd22d08", "duckdb": "a55ce7977ac874a3b204217e", "generic": "a13763a505b15f311cd22d08", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a13763a505b15f311cd22d08", +"motherduck": "a55ce7977ac874a3b204217e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a13763a505b15f311cd22d08", "postgresql": "a13763a505b15f311cd22d08", "snowflake": "5a4429eb67014c1c8f9e10d4", @@ -8117,8 +8117,8 @@ "databricks": "62368fc15cefd5807807b209", "duckdb": "62368fc15cefd5807807b209", "generic": "62368fc15cefd5807807b209", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "62368fc15cefd5807807b209", +"motherduck": "62368fc15cefd5807807b209", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "62368fc15cefd5807807b209", "postgresql": "62368fc15cefd5807807b209", "snowflake": "62368fc15cefd5807807b209", @@ -8134,8 +8134,8 @@ "databricks": "bbb97828cd28c64282709796", "duckdb": "bbb97828cd28c64282709796", "generic": "bbb97828cd28c64282709796", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "bbb97828cd28c64282709796", +"motherduck": "bbb97828cd28c64282709796", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b210f8eb26c37b422f786c6b", "postgresql": "b210f8eb26c37b422f786c6b", "snowflake": "bbb97828cd28c64282709796", @@ -8151,8 +8151,8 @@ "databricks": "3f71ab5f3c1c040da96f2cec", "duckdb": "3f71ab5f3c1c040da96f2cec", "generic": "3f71ab5f3c1c040da96f2cec", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3f71ab5f3c1c040da96f2cec", +"motherduck": "3f71ab5f3c1c040da96f2cec", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3f71ab5f3c1c040da96f2cec", "postgresql": "3f71ab5f3c1c040da96f2cec", "snowflake": "3f71ab5f3c1c040da96f2cec", @@ -8168,8 +8168,8 @@ "databricks": "38b2ec94bf02be40007ce525", "duckdb": "38b2ec94bf02be40007ce525", "generic": "38b2ec94bf02be40007ce525", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "38b2ec94bf02be40007ce525", +"motherduck": "38b2ec94bf02be40007ce525", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "850deea990b52b82b36a9848", "postgresql": "850deea990b52b82b36a9848", "snowflake": "38b2ec94bf02be40007ce525", @@ -8185,8 +8185,8 @@ "databricks": "f2c7751cca55dc87c36b6fc2", "duckdb": "02861a11fe2b8f1a7351cb12", "generic": "f2c7751cca55dc87c36b6fc2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f2c7751cca55dc87c36b6fc2", +"motherduck": "02861a11fe2b8f1a7351cb12", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f2c7751cca55dc87c36b6fc2", "postgresql": "f2c7751cca55dc87c36b6fc2", "snowflake": "5b65e3054ea3260a0a6dd837", @@ -8202,8 +8202,8 @@ "databricks": "efe77000c8cb77e8bcc49328", "duckdb": "d1a9a884331f069d89b0935f", "generic": "d1a9a884331f069d89b0935f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d1a9a884331f069d89b0935f", +"motherduck": "d1a9a884331f069d89b0935f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d1a9a884331f069d89b0935f", "postgresql": "d1a9a884331f069d89b0935f", "snowflake": "d1a9a884331f069d89b0935f", @@ -8219,8 +8219,8 @@ "databricks": "ebc821a95b9286e22c2b26d0", "duckdb": "ebc821a95b9286e22c2b26d0", "generic": "ebc821a95b9286e22c2b26d0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ebc821a95b9286e22c2b26d0", +"motherduck": "ebc821a95b9286e22c2b26d0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ebc821a95b9286e22c2b26d0", "postgresql": "ebc821a95b9286e22c2b26d0", "snowflake": "ebc821a95b9286e22c2b26d0", @@ -8236,8 +8236,8 @@ "databricks": "bce60580ef3a1dc4ce569fff", "duckdb": "bce60580ef3a1dc4ce569fff", "generic": "bce60580ef3a1dc4ce569fff", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "bce60580ef3a1dc4ce569fff", +"motherduck": "bce60580ef3a1dc4ce569fff", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "eee236ea1427bdddbaf088ac", "postgresql": "eee236ea1427bdddbaf088ac", "snowflake": "bce60580ef3a1dc4ce569fff", @@ -8253,8 +8253,8 @@ "databricks": "aab0389e2b5470136660bf8a", "duckdb": "aab0389e2b5470136660bf8a", "generic": "aab0389e2b5470136660bf8a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "aab0389e2b5470136660bf8a", +"motherduck": "aab0389e2b5470136660bf8a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f16105cbc61d547e6c08efc8", "postgresql": "f16105cbc61d547e6c08efc8", "snowflake": "97a20344d811fb77c7690863", @@ -8270,8 +8270,8 @@ "databricks": "cc33965a669608ec39e8c246", "duckdb": "cc33965a669608ec39e8c246", "generic": "cc33965a669608ec39e8c246", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "cc33965a669608ec39e8c246", +"motherduck": "cc33965a669608ec39e8c246", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "cc33965a669608ec39e8c246", "postgresql": "cc33965a669608ec39e8c246", "snowflake": "cc33965a669608ec39e8c246", @@ -8287,8 +8287,8 @@ "databricks": "4381456d770bb42050076d00", "duckdb": "4381456d770bb42050076d00", "generic": "4381456d770bb42050076d00", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4381456d770bb42050076d00", +"motherduck": "4381456d770bb42050076d00", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1f89b04f39a23f492cb6e5eb", "postgresql": "1f89b04f39a23f492cb6e5eb", "snowflake": "4381456d770bb42050076d00", @@ -8304,8 +8304,8 @@ "databricks": "a34c63be87b77d81239d0b4b", "duckdb": "a34c63be87b77d81239d0b4b", "generic": "a34c63be87b77d81239d0b4b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a34c63be87b77d81239d0b4b", +"motherduck": "a34c63be87b77d81239d0b4b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a34c63be87b77d81239d0b4b", "postgresql": "a34c63be87b77d81239d0b4b", "snowflake": "a34c63be87b77d81239d0b4b", @@ -8321,8 +8321,8 @@ "databricks": "d61c83ef8bfdf043fa672cb5", "duckdb": "d61c83ef8bfdf043fa672cb5", "generic": "d61c83ef8bfdf043fa672cb5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d61c83ef8bfdf043fa672cb5", +"motherduck": "d61c83ef8bfdf043fa672cb5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d61c83ef8bfdf043fa672cb5", "postgresql": "d61c83ef8bfdf043fa672cb5", "snowflake": "97a20344d811fb77c7690863", @@ -8338,8 +8338,8 @@ "databricks": "0703bf71a54942bdaa28406e", "duckdb": "0703bf71a54942bdaa28406e", "generic": "0703bf71a54942bdaa28406e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0703bf71a54942bdaa28406e", +"motherduck": "0703bf71a54942bdaa28406e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0703bf71a54942bdaa28406e", "postgresql": "0703bf71a54942bdaa28406e", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -8355,8 +8355,8 @@ "databricks": "0d58e20cd83f047b3b85cd1c", "duckdb": "0d58e20cd83f047b3b85cd1c", "generic": "0d58e20cd83f047b3b85cd1c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0d58e20cd83f047b3b85cd1c", +"motherduck": "0d58e20cd83f047b3b85cd1c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0d58e20cd83f047b3b85cd1c", "postgresql": "0d58e20cd83f047b3b85cd1c", "snowflake": "0d58e20cd83f047b3b85cd1c", @@ -8372,8 +8372,8 @@ "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fe23594c781e79cbb0c8927a", +"motherduck": "fe23594c781e79cbb0c8927a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8a3f094f18fb4047f0febf59", "postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", @@ -8389,8 +8389,8 @@ "databricks": "dba9493579be12efd69318ac", "duckdb": "63e20906fe3504356e04d05e", "generic": "dba9493579be12efd69318ac", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "63e20906fe3504356e04d05e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "63e20906fe3504356e04d05e", "postgresql": "63e20906fe3504356e04d05e", "snowflake": "30696690153e91e7ae197982", @@ -8406,8 +8406,8 @@ "databricks": "7914189ff18b0e70ddf7d042", "duckdb": "7914189ff18b0e70ddf7d042", "generic": "7914189ff18b0e70ddf7d042", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "7914189ff18b0e70ddf7d042", +"motherduck": "7914189ff18b0e70ddf7d042", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7914189ff18b0e70ddf7d042", "postgresql": "7914189ff18b0e70ddf7d042", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -8423,8 +8423,8 @@ "databricks": "558da38ec4d9d5d6c72f60cf", "duckdb": "919b7ec6a7b49d8527b784cd", "generic": "558da38ec4d9d5d6c72f60cf", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "558da38ec4d9d5d6c72f60cf", +"motherduck": "919b7ec6a7b49d8527b784cd", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "558da38ec4d9d5d6c72f60cf", "postgresql": "558da38ec4d9d5d6c72f60cf", "snowflake": "919b7ec6a7b49d8527b784cd", @@ -8440,8 +8440,8 @@ "databricks": "42cd2b5892468e185781348b", "duckdb": "42cd2b5892468e185781348b", "generic": "42cd2b5892468e185781348b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "42cd2b5892468e185781348b", +"motherduck": "42cd2b5892468e185781348b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "42cd2b5892468e185781348b", "postgresql": "42cd2b5892468e185781348b", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -8457,8 +8457,8 @@ "databricks": "46ec3e19e05a6cb29854ccac", "duckdb": "35e216b6921bf843994f2eea", "generic": "46ec3e19e05a6cb29854ccac", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "46ec3e19e05a6cb29854ccac", +"motherduck": "35e216b6921bf843994f2eea", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "46ec3e19e05a6cb29854ccac", "postgresql": "46ec3e19e05a6cb29854ccac", "snowflake": "46ec3e19e05a6cb29854ccac", @@ -8474,8 +8474,8 @@ "databricks": "eed0645c1e86fa1a6e35ccaa", "duckdb": "eed0645c1e86fa1a6e35ccaa", "generic": "eed0645c1e86fa1a6e35ccaa", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "eed0645c1e86fa1a6e35ccaa", +"motherduck": "eed0645c1e86fa1a6e35ccaa", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "eed0645c1e86fa1a6e35ccaa", "postgresql": "eed0645c1e86fa1a6e35ccaa", "snowflake": "97a20344d811fb77c7690863", @@ -8491,8 +8491,8 @@ "databricks": "a428302e4848ffe877284c84", "duckdb": "a428302e4848ffe877284c84", "generic": "a428302e4848ffe877284c84", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a428302e4848ffe877284c84", +"motherduck": "a428302e4848ffe877284c84", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a428302e4848ffe877284c84", "postgresql": "a428302e4848ffe877284c84", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -8508,8 +8508,8 @@ "databricks": "e57ac1541fb51b362cd5b470", "duckdb": "e57ac1541fb51b362cd5b470", "generic": "e57ac1541fb51b362cd5b470", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e57ac1541fb51b362cd5b470", +"motherduck": "e57ac1541fb51b362cd5b470", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e57ac1541fb51b362cd5b470", "postgresql": "e57ac1541fb51b362cd5b470", "snowflake": "4b356542044f1267fcfc487a", @@ -8525,8 +8525,8 @@ "databricks": "0f3d33062aebbc5edec7b64f", "duckdb": "0f3d33062aebbc5edec7b64f", "generic": "0f3d33062aebbc5edec7b64f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0f3d33062aebbc5edec7b64f", +"motherduck": "0f3d33062aebbc5edec7b64f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0f3d33062aebbc5edec7b64f", "postgresql": "0f3d33062aebbc5edec7b64f", "snowflake": "0f3d33062aebbc5edec7b64f", @@ -8542,8 +8542,8 @@ "databricks": "b2c9b6ebca6c097f3caa0aa8", "duckdb": "b2c9b6ebca6c097f3caa0aa8", "generic": "b2c9b6ebca6c097f3caa0aa8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b2c9b6ebca6c097f3caa0aa8", +"motherduck": "b2c9b6ebca6c097f3caa0aa8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b2c9b6ebca6c097f3caa0aa8", "postgresql": "b2c9b6ebca6c097f3caa0aa8", "snowflake": "b2c9b6ebca6c097f3caa0aa8", @@ -8559,8 +8559,8 @@ "databricks": "c21a08c5ec73e14b4d409300", "duckdb": "c21a08c5ec73e14b4d409300", "generic": "c21a08c5ec73e14b4d409300", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c21a08c5ec73e14b4d409300", +"motherduck": "c21a08c5ec73e14b4d409300", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c21a08c5ec73e14b4d409300", "postgresql": "c21a08c5ec73e14b4d409300", "snowflake": "c21a08c5ec73e14b4d409300", @@ -8576,8 +8576,8 @@ "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fe23594c781e79cbb0c8927a", +"motherduck": "fe23594c781e79cbb0c8927a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8a3f094f18fb4047f0febf59", "postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", @@ -8593,8 +8593,8 @@ "databricks": "88582da8542d7d1abf567520", "duckdb": "88582da8542d7d1abf567520", "generic": "88582da8542d7d1abf567520", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "88582da8542d7d1abf567520", +"motherduck": "88582da8542d7d1abf567520", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "88582da8542d7d1abf567520", "postgresql": "88582da8542d7d1abf567520", "snowflake": "88582da8542d7d1abf567520", @@ -8610,8 +8610,8 @@ "databricks": "8a10388471810a2bd58f7686", "duckdb": "8a10388471810a2bd58f7686", "generic": "8a10388471810a2bd58f7686", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8a10388471810a2bd58f7686", +"motherduck": "8a10388471810a2bd58f7686", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8a10388471810a2bd58f7686", "postgresql": "8a10388471810a2bd58f7686", "snowflake": "8a10388471810a2bd58f7686", @@ -8627,8 +8627,8 @@ "databricks": "ebc821a95b9286e22c2b26d0", "duckdb": "ebc821a95b9286e22c2b26d0", "generic": "ebc821a95b9286e22c2b26d0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ebc821a95b9286e22c2b26d0", +"motherduck": "ebc821a95b9286e22c2b26d0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ebc821a95b9286e22c2b26d0", "postgresql": "ebc821a95b9286e22c2b26d0", "snowflake": "ebc821a95b9286e22c2b26d0", @@ -8644,8 +8644,8 @@ "databricks": "64fc778308b798a730b1d5c9", "duckdb": "64fc778308b798a730b1d5c9", "generic": "64fc778308b798a730b1d5c9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "64fc778308b798a730b1d5c9", +"motherduck": "64fc778308b798a730b1d5c9", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c8ca7a940ad228dd836cad6f", "postgresql": "c8ca7a940ad228dd836cad6f", "snowflake": "64fc778308b798a730b1d5c9", @@ -8661,8 +8661,8 @@ "databricks": "679a90349a55ba7765c07664", "duckdb": "679a90349a55ba7765c07664", "generic": "679a90349a55ba7765c07664", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "679a90349a55ba7765c07664", +"motherduck": "679a90349a55ba7765c07664", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "679a90349a55ba7765c07664", "postgresql": "679a90349a55ba7765c07664", "snowflake": "679a90349a55ba7765c07664", @@ -8678,8 +8678,8 @@ "databricks": "3f71ab5f3c1c040da96f2cec", "duckdb": "3f71ab5f3c1c040da96f2cec", "generic": "3f71ab5f3c1c040da96f2cec", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3f71ab5f3c1c040da96f2cec", +"motherduck": "3f71ab5f3c1c040da96f2cec", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3f71ab5f3c1c040da96f2cec", "postgresql": "3f71ab5f3c1c040da96f2cec", "snowflake": "3f71ab5f3c1c040da96f2cec", @@ -8695,8 +8695,8 @@ "databricks": "da0625706f12b764a6912278", "duckdb": "3147a9e06daa6de4a4df5abc", "generic": "da0625706f12b764a6912278", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "da0625706f12b764a6912278", +"motherduck": "3147a9e06daa6de4a4df5abc", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3147a9e06daa6de4a4df5abc", "postgresql": "3147a9e06daa6de4a4df5abc", "snowflake": "67b5874a2c569226c490211f", @@ -8712,8 +8712,8 @@ "databricks": "3b16d8eb223ae1333ecbc2bf", "duckdb": "ff99ba717583eafc6fdd9442", "generic": "3b16d8eb223ae1333ecbc2bf", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3b16d8eb223ae1333ecbc2bf", +"motherduck": "ff99ba717583eafc6fdd9442", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "39ac50273811909ae35979b7", "postgresql": "39ac50273811909ae35979b7", "snowflake": "ca443b97e784bced05b62f0b", @@ -8729,8 +8729,8 @@ "databricks": "e7683084f9cefaf3748cf526", "duckdb": "e7683084f9cefaf3748cf526", "generic": "e7683084f9cefaf3748cf526", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e7683084f9cefaf3748cf526", +"motherduck": "e7683084f9cefaf3748cf526", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e7683084f9cefaf3748cf526", "postgresql": "e7683084f9cefaf3748cf526", "snowflake": "e7683084f9cefaf3748cf526", @@ -8746,8 +8746,8 @@ "databricks": "9626d393c137a37008bb4b89", "duckdb": "9626d393c137a37008bb4b89", "generic": "9626d393c137a37008bb4b89", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9626d393c137a37008bb4b89", +"motherduck": "9626d393c137a37008bb4b89", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9626d393c137a37008bb4b89", "postgresql": "9626d393c137a37008bb4b89", "snowflake": "9626d393c137a37008bb4b89", @@ -8763,8 +8763,8 @@ "databricks": "669084d1ea704ae5b8ee05c2", "duckdb": "669084d1ea704ae5b8ee05c2", "generic": "669084d1ea704ae5b8ee05c2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "669084d1ea704ae5b8ee05c2", +"motherduck": "669084d1ea704ae5b8ee05c2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "93c1b5b5960e26479baf88d0", "postgresql": "93c1b5b5960e26479baf88d0", "snowflake": "669084d1ea704ae5b8ee05c2", @@ -8780,8 +8780,8 @@ "databricks": "4e47cae1f2f84f5e523dc43f", "duckdb": "4e47cae1f2f84f5e523dc43f", "generic": "4e47cae1f2f84f5e523dc43f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4e47cae1f2f84f5e523dc43f", +"motherduck": "4e47cae1f2f84f5e523dc43f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "4e47cae1f2f84f5e523dc43f", "postgresql": "4e47cae1f2f84f5e523dc43f", "snowflake": "4e47cae1f2f84f5e523dc43f", @@ -8797,8 +8797,8 @@ "databricks": "8fe65454f387031aa6231faf", "duckdb": "8fe65454f387031aa6231faf", "generic": "8fe65454f387031aa6231faf", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8fe65454f387031aa6231faf", +"motherduck": "8fe65454f387031aa6231faf", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8fe65454f387031aa6231faf", "postgresql": "8fe65454f387031aa6231faf", "snowflake": "8fe65454f387031aa6231faf", @@ -8814,8 +8814,8 @@ "databricks": "c9b810639795a7bfab91b98d", "duckdb": "a428302e4848ffe877284c84", "generic": "c9b810639795a7bfab91b98d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c9b810639795a7bfab91b98d", +"motherduck": "a428302e4848ffe877284c84", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a428302e4848ffe877284c84", "postgresql": "a428302e4848ffe877284c84", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -8831,8 +8831,8 @@ "databricks": "c844c96e1e3ffc72d5f90df9", "duckdb": "8a3f094f18fb4047f0febf59", "generic": "c844c96e1e3ffc72d5f90df9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "28eb0d6de9724959ce5ba43e", +"motherduck": "8a3f094f18fb4047f0febf59", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c844c96e1e3ffc72d5f90df9", "postgresql": "c844c96e1e3ffc72d5f90df9", "snowflake": "c844c96e1e3ffc72d5f90df9", @@ -8848,8 +8848,8 @@ "databricks": "e8b6e0bd2ded7b9eec6209e4", "duckdb": "e8b6e0bd2ded7b9eec6209e4", "generic": "e8b6e0bd2ded7b9eec6209e4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e8b6e0bd2ded7b9eec6209e4", +"motherduck": "e8b6e0bd2ded7b9eec6209e4", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e8b6e0bd2ded7b9eec6209e4", "postgresql": "e8b6e0bd2ded7b9eec6209e4", "snowflake": "e8b6e0bd2ded7b9eec6209e4", @@ -8865,8 +8865,8 @@ "databricks": "d41d40c68507f4d02e348833", "duckdb": "d41d40c68507f4d02e348833", "generic": "d41d40c68507f4d02e348833", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d41d40c68507f4d02e348833", +"motherduck": "d41d40c68507f4d02e348833", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d41d40c68507f4d02e348833", "postgresql": "d41d40c68507f4d02e348833", "snowflake": "d41d40c68507f4d02e348833", @@ -8882,8 +8882,8 @@ "databricks": "18db5afe2c1e80d1ee652b54", "duckdb": "18db5afe2c1e80d1ee652b54", "generic": "18db5afe2c1e80d1ee652b54", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "18db5afe2c1e80d1ee652b54", +"motherduck": "18db5afe2c1e80d1ee652b54", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "47d64a5994d42a2b577166ea", "postgresql": "47d64a5994d42a2b577166ea", "snowflake": "18db5afe2c1e80d1ee652b54", @@ -8899,8 +8899,8 @@ "databricks": "29685ad61061d232d5b44f98", "duckdb": "fbb8dada51c532716b2cbf9f", "generic": "29685ad61061d232d5b44f98", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "fbb8dada51c532716b2cbf9f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "fbb8dada51c532716b2cbf9f", "postgresql": "fbb8dada51c532716b2cbf9f", "snowflake": "3d54d99fa15cb697878997d5", @@ -8916,8 +8916,8 @@ "databricks": "d61c83ef8bfdf043fa672cb5", "duckdb": "d61c83ef8bfdf043fa672cb5", "generic": "d61c83ef8bfdf043fa672cb5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d61c83ef8bfdf043fa672cb5", +"motherduck": "d61c83ef8bfdf043fa672cb5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d61c83ef8bfdf043fa672cb5", "postgresql": "d61c83ef8bfdf043fa672cb5", "snowflake": "97a20344d811fb77c7690863", @@ -8933,8 +8933,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1eb7fb4e8cf75773c5573562", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1eb7fb4e8cf75773c5573562", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -8950,8 +8950,8 @@ "databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", "generic": "fb4b6f5101f973d16ea8d1f3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fb4b6f5101f973d16ea8d1f3", +"motherduck": "70fb4728c3adcf6dbe4975d5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", @@ -8967,8 +8967,8 @@ "databricks": "64fc778308b798a730b1d5c9", "duckdb": "64fc778308b798a730b1d5c9", "generic": "64fc778308b798a730b1d5c9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "64fc778308b798a730b1d5c9", +"motherduck": "64fc778308b798a730b1d5c9", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c8ca7a940ad228dd836cad6f", "postgresql": "c8ca7a940ad228dd836cad6f", "snowflake": "64fc778308b798a730b1d5c9", @@ -8984,8 +8984,8 @@ "databricks": "fd3b9424e28fed0134486bc7", "duckdb": "ee8c9d9a91ebec8ca7defc90", "generic": "fd3b9424e28fed0134486bc7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "ee8c9d9a91ebec8ca7defc90", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ee8c9d9a91ebec8ca7defc90", "postgresql": "ee8c9d9a91ebec8ca7defc90", "snowflake": "1d67c9120962e3304f0e1bcc", @@ -9001,8 +9001,8 @@ "databricks": "3a4d8bca5124769db23a43df", "duckdb": "01e3dfc024c37a8d62c7f810", "generic": "3a4d8bca5124769db23a43df", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "01e3dfc024c37a8d62c7f810", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "01e3dfc024c37a8d62c7f810", "postgresql": "01e3dfc024c37a8d62c7f810", "snowflake": "db03b5ccf153c75e43846a6a", @@ -9018,8 +9018,8 @@ "databricks": "791a58207da7e61895ce33f6", "duckdb": "791a58207da7e61895ce33f6", "generic": "791a58207da7e61895ce33f6", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "791a58207da7e61895ce33f6", +"motherduck": "791a58207da7e61895ce33f6", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "791a58207da7e61895ce33f6", "postgresql": "791a58207da7e61895ce33f6", "snowflake": "791a58207da7e61895ce33f6", @@ -9035,8 +9035,8 @@ "databricks": "461923a0d34defd0c726d6e5", "duckdb": "461923a0d34defd0c726d6e5", "generic": "461923a0d34defd0c726d6e5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "461923a0d34defd0c726d6e5", +"motherduck": "461923a0d34defd0c726d6e5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "461923a0d34defd0c726d6e5", "postgresql": "461923a0d34defd0c726d6e5", "snowflake": "461923a0d34defd0c726d6e5", @@ -9052,8 +9052,8 @@ "databricks": "d2c97d221599d038d9875873", "duckdb": "d2c97d221599d038d9875873", "generic": "d2c97d221599d038d9875873", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d2c97d221599d038d9875873", +"motherduck": "d2c97d221599d038d9875873", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "55155766823b6a3a5e882fc2", "postgresql": "55155766823b6a3a5e882fc2", "snowflake": "d2c97d221599d038d9875873", @@ -9069,8 +9069,8 @@ "databricks": "d2c97d221599d038d9875873", "duckdb": "f7fca17b74cc50ae3de8c826", "generic": "d2c97d221599d038d9875873", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d2c97d221599d038d9875873", +"motherduck": "f7fca17b74cc50ae3de8c826", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "55155766823b6a3a5e882fc2", "postgresql": "55155766823b6a3a5e882fc2", "snowflake": "55155766823b6a3a5e882fc2", @@ -9086,8 +9086,8 @@ "databricks": "ef34121d80f6c399a1a456b1", "duckdb": "ef34121d80f6c399a1a456b1", "generic": "ef34121d80f6c399a1a456b1", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ef34121d80f6c399a1a456b1", +"motherduck": "ef34121d80f6c399a1a456b1", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ef34121d80f6c399a1a456b1", "postgresql": "ef34121d80f6c399a1a456b1", "snowflake": "ef34121d80f6c399a1a456b1", @@ -9103,8 +9103,8 @@ "databricks": "79278352c17c9de0f5a28af5", "duckdb": "79278352c17c9de0f5a28af5", "generic": "79278352c17c9de0f5a28af5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "79278352c17c9de0f5a28af5", +"motherduck": "79278352c17c9de0f5a28af5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "79278352c17c9de0f5a28af5", "postgresql": "79278352c17c9de0f5a28af5", "snowflake": "97a20344d811fb77c7690863", @@ -9120,8 +9120,8 @@ "databricks": "b4284f955ab1914d3f556d82", "duckdb": "b4284f955ab1914d3f556d82", "generic": "b4284f955ab1914d3f556d82", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b4284f955ab1914d3f556d82", +"motherduck": "b4284f955ab1914d3f556d82", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "063c794e14d2f13bb89a7a4d", "postgresql": "063c794e14d2f13bb89a7a4d", "snowflake": "b4284f955ab1914d3f556d82", @@ -9137,8 +9137,8 @@ "databricks": "c84c0b1d4d7caa6744130456", "duckdb": "c84c0b1d4d7caa6744130456", "generic": "c84c0b1d4d7caa6744130456", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c84c0b1d4d7caa6744130456", +"motherduck": "c84c0b1d4d7caa6744130456", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "725c73e74f311417bb4f95e8", "postgresql": "725c73e74f311417bb4f95e8", "snowflake": "c84c0b1d4d7caa6744130456", @@ -9154,8 +9154,8 @@ "databricks": "62368fc15cefd5807807b209", "duckdb": "62368fc15cefd5807807b209", "generic": "62368fc15cefd5807807b209", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "62368fc15cefd5807807b209", +"motherduck": "62368fc15cefd5807807b209", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "62368fc15cefd5807807b209", "postgresql": "62368fc15cefd5807807b209", "snowflake": "62368fc15cefd5807807b209", @@ -9171,8 +9171,8 @@ "databricks": "8441d00030df06ad6a5534d3", "duckdb": "8441d00030df06ad6a5534d3", "generic": "8441d00030df06ad6a5534d3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8441d00030df06ad6a5534d3", +"motherduck": "8441d00030df06ad6a5534d3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8441d00030df06ad6a5534d3", "postgresql": "8441d00030df06ad6a5534d3", "snowflake": "97a20344d811fb77c7690863", @@ -9188,8 +9188,8 @@ "databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", "generic": "fb4b6f5101f973d16ea8d1f3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fb4b6f5101f973d16ea8d1f3", +"motherduck": "70fb4728c3adcf6dbe4975d5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", @@ -9205,8 +9205,8 @@ "databricks": "ebc821a95b9286e22c2b26d0", "duckdb": "ebc821a95b9286e22c2b26d0", "generic": "ebc821a95b9286e22c2b26d0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ebc821a95b9286e22c2b26d0", +"motherduck": "ebc821a95b9286e22c2b26d0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ebc821a95b9286e22c2b26d0", "postgresql": "ebc821a95b9286e22c2b26d0", "snowflake": "ebc821a95b9286e22c2b26d0", @@ -9222,8 +9222,8 @@ "databricks": "73151c1398b30e5c5d18f4ce", "duckdb": "73151c1398b30e5c5d18f4ce", "generic": "73151c1398b30e5c5d18f4ce", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "73151c1398b30e5c5d18f4ce", +"motherduck": "73151c1398b30e5c5d18f4ce", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f68b7dbd74ec7c3b9f3066ef", "postgresql": "f68b7dbd74ec7c3b9f3066ef", "snowflake": "73151c1398b30e5c5d18f4ce", @@ -9239,8 +9239,8 @@ "databricks": "4fb572024031cb7631777be1", "duckdb": "792f7d3389f076f50c713946", "generic": "792f7d3389f076f50c713946", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "792f7d3389f076f50c713946", +"motherduck": "792f7d3389f076f50c713946", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "792f7d3389f076f50c713946", "postgresql": "792f7d3389f076f50c713946", "snowflake": "792f7d3389f076f50c713946", @@ -9256,8 +9256,8 @@ "databricks": "236311169ac0526becd627ac", "duckdb": "236311169ac0526becd627ac", "generic": "236311169ac0526becd627ac", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "236311169ac0526becd627ac", +"motherduck": "236311169ac0526becd627ac", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "236311169ac0526becd627ac", "postgresql": "236311169ac0526becd627ac", "snowflake": "236311169ac0526becd627ac", @@ -9273,8 +9273,8 @@ "databricks": "336fd1b7a1f7d8105c29280c", "duckdb": "0baaca265927140414f32b37", "generic": "336fd1b7a1f7d8105c29280c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "336fd1b7a1f7d8105c29280c", +"motherduck": "0baaca265927140414f32b37", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9ea4932505663b43f527adad", "postgresql": "9ea4932505663b43f527adad", "snowflake": "336fd1b7a1f7d8105c29280c", @@ -9290,8 +9290,8 @@ "databricks": "806dd59533345612c07a861b", "duckdb": "806dd59533345612c07a861b", "generic": "806dd59533345612c07a861b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "806dd59533345612c07a861b", +"motherduck": "806dd59533345612c07a861b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "806dd59533345612c07a861b", "postgresql": "806dd59533345612c07a861b", "snowflake": "806dd59533345612c07a861b", @@ -9307,8 +9307,8 @@ "databricks": "2df0f0517ee87dda5683d2bd", "duckdb": "647ac9829e390656c0783aba", "generic": "2df0f0517ee87dda5683d2bd", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2df0f0517ee87dda5683d2bd", +"motherduck": "647ac9829e390656c0783aba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2df0f0517ee87dda5683d2bd", "postgresql": "2df0f0517ee87dda5683d2bd", "snowflake": "8d1f13df6b799b7b301d2e69", @@ -9324,8 +9324,8 @@ "databricks": "5aa51d84ac5e6c7a9306f348", "duckdb": "5aa51d84ac5e6c7a9306f348", "generic": "5aa51d84ac5e6c7a9306f348", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5aa51d84ac5e6c7a9306f348", +"motherduck": "5aa51d84ac5e6c7a9306f348", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5aa51d84ac5e6c7a9306f348", "postgresql": "5aa51d84ac5e6c7a9306f348", "snowflake": "5aa51d84ac5e6c7a9306f348", @@ -9341,8 +9341,8 @@ "databricks": "3fd7037beaaa396764e49e8b", "duckdb": "6247229b9f890f25e7b3f0c5", "generic": "3fd7037beaaa396764e49e8b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3fd7037beaaa396764e49e8b", +"motherduck": "6247229b9f890f25e7b3f0c5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3fd7037beaaa396764e49e8b", "postgresql": "3fd7037beaaa396764e49e8b", "snowflake": "8f60ea843371b982d069e65b", @@ -9358,8 +9358,8 @@ "databricks": "a57535ee64678e69d1acd48a", "duckdb": "077cc38442b3b9eae7bb07d0", "generic": "a57535ee64678e69d1acd48a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "077cc38442b3b9eae7bb07d0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "077cc38442b3b9eae7bb07d0", "postgresql": "077cc38442b3b9eae7bb07d0", "snowflake": "befa13a14d1144835e866a4d", @@ -9375,8 +9375,8 @@ "databricks": "d61c83ef8bfdf043fa672cb5", "duckdb": "d61c83ef8bfdf043fa672cb5", "generic": "d61c83ef8bfdf043fa672cb5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d61c83ef8bfdf043fa672cb5", +"motherduck": "d61c83ef8bfdf043fa672cb5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d61c83ef8bfdf043fa672cb5", "postgresql": "d61c83ef8bfdf043fa672cb5", "snowflake": "97a20344d811fb77c7690863", @@ -9392,8 +9392,8 @@ "databricks": "ff8f9d6dbbdd5b3cf2e7c198", "duckdb": "ff8f9d6dbbdd5b3cf2e7c198", "generic": "ff8f9d6dbbdd5b3cf2e7c198", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ff8f9d6dbbdd5b3cf2e7c198", +"motherduck": "ff8f9d6dbbdd5b3cf2e7c198", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ff8f9d6dbbdd5b3cf2e7c198", "postgresql": "ff8f9d6dbbdd5b3cf2e7c198", "snowflake": "ff8f9d6dbbdd5b3cf2e7c198", @@ -9409,8 +9409,8 @@ "databricks": "4928771fd9e503a30457d160", "duckdb": "4928771fd9e503a30457d160", "generic": "4928771fd9e503a30457d160", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4928771fd9e503a30457d160", +"motherduck": "4928771fd9e503a30457d160", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "50ddfb168c08b09892261e82", "postgresql": "50ddfb168c08b09892261e82", "snowflake": "4928771fd9e503a30457d160", @@ -9426,8 +9426,8 @@ "databricks": "1e75e5227bea0ae294ac7af1", "duckdb": "19bdd92ea0cc5fb028657510", "generic": "b8321619b794db091c9e96a1", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b8321619b794db091c9e96a1", +"motherduck": "19bdd92ea0cc5fb028657510", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "bce26544873d6ec9191c9a86", "postgresql": "bce26544873d6ec9191c9a86", "snowflake": "b8321619b794db091c9e96a1", @@ -9443,8 +9443,8 @@ "databricks": "a0e5b4ce93e9317c3cea1cbf", "duckdb": "a0e5b4ce93e9317c3cea1cbf", "generic": "a0e5b4ce93e9317c3cea1cbf", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a0e5b4ce93e9317c3cea1cbf", +"motherduck": "a0e5b4ce93e9317c3cea1cbf", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a0e5b4ce93e9317c3cea1cbf", "postgresql": "a0e5b4ce93e9317c3cea1cbf", "snowflake": "a0e5b4ce93e9317c3cea1cbf", @@ -9460,8 +9460,8 @@ "databricks": "78de20f17ea5d66780a9f32e", "duckdb": "78de20f17ea5d66780a9f32e", "generic": "78de20f17ea5d66780a9f32e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "78de20f17ea5d66780a9f32e", +"motherduck": "78de20f17ea5d66780a9f32e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "78de20f17ea5d66780a9f32e", "postgresql": "78de20f17ea5d66780a9f32e", "snowflake": "78de20f17ea5d66780a9f32e", @@ -9477,8 +9477,8 @@ "databricks": "40f264d27364684659f815eb", "duckdb": "8bb8eb3ad4ee794b787a664d", "generic": "8bb8eb3ad4ee794b787a664d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8bb8eb3ad4ee794b787a664d", +"motherduck": "8bb8eb3ad4ee794b787a664d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8bb8eb3ad4ee794b787a664d", "postgresql": "8bb8eb3ad4ee794b787a664d", "snowflake": "469cddcff389dac525f4efdf", @@ -9494,8 +9494,8 @@ "databricks": "e46d4be44ab9e500e9190d26", "duckdb": "e46d4be44ab9e500e9190d26", "generic": "e46d4be44ab9e500e9190d26", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e46d4be44ab9e500e9190d26", +"motherduck": "e46d4be44ab9e500e9190d26", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "251aba3fef503ce64556c701", "postgresql": "251aba3fef503ce64556c701", "snowflake": "e46d4be44ab9e500e9190d26", @@ -9511,8 +9511,8 @@ "databricks": "3673b4fbbd8a7fed7120ec2b", "duckdb": "3673b4fbbd8a7fed7120ec2b", "generic": "3673b4fbbd8a7fed7120ec2b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3673b4fbbd8a7fed7120ec2b", +"motherduck": "3673b4fbbd8a7fed7120ec2b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9aff7fcc7d550e1a8d105471", "postgresql": "9aff7fcc7d550e1a8d105471", "snowflake": "3673b4fbbd8a7fed7120ec2b", @@ -9528,8 +9528,8 @@ "databricks": "62368fc15cefd5807807b209", "duckdb": "a3b6b25265be330ec52a3322", "generic": "62368fc15cefd5807807b209", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "62368fc15cefd5807807b209", +"motherduck": "a3b6b25265be330ec52a3322", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "62368fc15cefd5807807b209", "postgresql": "62368fc15cefd5807807b209", "snowflake": "919b7ec6a7b49d8527b784cd", @@ -9545,8 +9545,8 @@ "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fe23594c781e79cbb0c8927a", +"motherduck": "fe23594c781e79cbb0c8927a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8a3f094f18fb4047f0febf59", "postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", @@ -9562,8 +9562,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1eb7fb4e8cf75773c5573562", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1eb7fb4e8cf75773c5573562", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -9579,8 +9579,8 @@ "databricks": "0b8ad171901cc2b7725617da", "duckdb": "0b8ad171901cc2b7725617da", "generic": "0b8ad171901cc2b7725617da", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0b8ad171901cc2b7725617da", +"motherduck": "0b8ad171901cc2b7725617da", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0b8ad171901cc2b7725617da", "postgresql": "0b8ad171901cc2b7725617da", "snowflake": "0b8ad171901cc2b7725617da", @@ -9596,8 +9596,8 @@ "databricks": "97dfbcc6528a197a87add69e", "duckdb": "208eafc9a5aab337867bb773", "generic": "97dfbcc6528a197a87add69e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "208eafc9a5aab337867bb773", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ca3d3fa7d8eb1608bd8752de", "postgresql": "ca3d3fa7d8eb1608bd8752de", "snowflake": "46250b305f6ac5a95e4d3083", @@ -9613,8 +9613,8 @@ "databricks": "c4d601bdc1b1f357b3e9bbdf", "duckdb": "c4d601bdc1b1f357b3e9bbdf", "generic": "c4d601bdc1b1f357b3e9bbdf", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c4d601bdc1b1f357b3e9bbdf", +"motherduck": "c4d601bdc1b1f357b3e9bbdf", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c4d601bdc1b1f357b3e9bbdf", "postgresql": "c4d601bdc1b1f357b3e9bbdf", "snowflake": "c4d601bdc1b1f357b3e9bbdf", @@ -9630,8 +9630,8 @@ "databricks": "669084d1ea704ae5b8ee05c2", "duckdb": "669084d1ea704ae5b8ee05c2", "generic": "669084d1ea704ae5b8ee05c2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "669084d1ea704ae5b8ee05c2", +"motherduck": "669084d1ea704ae5b8ee05c2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "93c1b5b5960e26479baf88d0", "postgresql": "93c1b5b5960e26479baf88d0", "snowflake": "669084d1ea704ae5b8ee05c2", @@ -9647,8 +9647,8 @@ "databricks": "e7c121d9cbaf6e6b91cfdf74", "duckdb": "e7c121d9cbaf6e6b91cfdf74", "generic": "e7c121d9cbaf6e6b91cfdf74", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e7c121d9cbaf6e6b91cfdf74", +"motherduck": "e7c121d9cbaf6e6b91cfdf74", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e7c121d9cbaf6e6b91cfdf74", "postgresql": "e7c121d9cbaf6e6b91cfdf74", "snowflake": "e7c121d9cbaf6e6b91cfdf74", @@ -9664,8 +9664,8 @@ "databricks": "962ea05bab624c1f8c259a65", "duckdb": "962ea05bab624c1f8c259a65", "generic": "962ea05bab624c1f8c259a65", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "962ea05bab624c1f8c259a65", +"motherduck": "962ea05bab624c1f8c259a65", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "962ea05bab624c1f8c259a65", "postgresql": "962ea05bab624c1f8c259a65", "snowflake": "962ea05bab624c1f8c259a65", @@ -9681,8 +9681,8 @@ "databricks": "3e888d249494102b9ba328b2", "duckdb": "3e888d249494102b9ba328b2", "generic": "3e888d249494102b9ba328b2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3e888d249494102b9ba328b2", +"motherduck": "3e888d249494102b9ba328b2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3e888d249494102b9ba328b2", "postgresql": "3e888d249494102b9ba328b2", "snowflake": "3e888d249494102b9ba328b2", @@ -9698,8 +9698,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1eb7fb4e8cf75773c5573562", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1eb7fb4e8cf75773c5573562", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -9715,8 +9715,8 @@ "databricks": "652ff8b21f638eac8fdc46c5", "duckdb": "652ff8b21f638eac8fdc46c5", "generic": "652ff8b21f638eac8fdc46c5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "652ff8b21f638eac8fdc46c5", +"motherduck": "652ff8b21f638eac8fdc46c5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "652ff8b21f638eac8fdc46c5", "postgresql": "652ff8b21f638eac8fdc46c5", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -9732,8 +9732,8 @@ "databricks": "0989b67047ee9c0a1d9f2cf4", "duckdb": "0989b67047ee9c0a1d9f2cf4", "generic": "0989b67047ee9c0a1d9f2cf4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0989b67047ee9c0a1d9f2cf4", +"motherduck": "0989b67047ee9c0a1d9f2cf4", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0989b67047ee9c0a1d9f2cf4", "postgresql": "0989b67047ee9c0a1d9f2cf4", "snowflake": "0989b67047ee9c0a1d9f2cf4", @@ -9749,8 +9749,8 @@ "databricks": "1275b5b0c52f69cf592c004b", "duckdb": "1275b5b0c52f69cf592c004b", "generic": "1275b5b0c52f69cf592c004b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1275b5b0c52f69cf592c004b", +"motherduck": "1275b5b0c52f69cf592c004b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1275b5b0c52f69cf592c004b", "postgresql": "1275b5b0c52f69cf592c004b", "snowflake": "1275b5b0c52f69cf592c004b", @@ -9766,8 +9766,8 @@ "databricks": "3e611643f0beab4953c4e09b", "duckdb": "3e611643f0beab4953c4e09b", "generic": "3e611643f0beab4953c4e09b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3e611643f0beab4953c4e09b", +"motherduck": "3e611643f0beab4953c4e09b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "cae477ce59c72869f37a9a40", "postgresql": "cae477ce59c72869f37a9a40", "snowflake": "3e611643f0beab4953c4e09b", @@ -9783,8 +9783,8 @@ "databricks": "e46d4be44ab9e500e9190d26", "duckdb": "e46d4be44ab9e500e9190d26", "generic": "e46d4be44ab9e500e9190d26", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e46d4be44ab9e500e9190d26", +"motherduck": "e46d4be44ab9e500e9190d26", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "251aba3fef503ce64556c701", "postgresql": "251aba3fef503ce64556c701", "snowflake": "e46d4be44ab9e500e9190d26", @@ -9800,8 +9800,8 @@ "databricks": "70fb4728c3adcf6dbe4975d5", "duckdb": "70fb4728c3adcf6dbe4975d5", "generic": "70fb4728c3adcf6dbe4975d5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "70fb4728c3adcf6dbe4975d5", +"motherduck": "70fb4728c3adcf6dbe4975d5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", @@ -9817,8 +9817,8 @@ "databricks": "e2879cf4770e7f48d2dd704a", "duckdb": "e2879cf4770e7f48d2dd704a", "generic": "e2879cf4770e7f48d2dd704a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e2879cf4770e7f48d2dd704a", +"motherduck": "e2879cf4770e7f48d2dd704a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2d4aea8dec5521e258596f3d", "postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", @@ -9834,8 +9834,8 @@ "databricks": "b6b82efd03def9465e4f97b2", "duckdb": "b6b82efd03def9465e4f97b2", "generic": "b6b82efd03def9465e4f97b2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b6b82efd03def9465e4f97b2", +"motherduck": "b6b82efd03def9465e4f97b2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a3923c3d2c49043629c52bf6", "postgresql": "a3923c3d2c49043629c52bf6", "snowflake": "b6b82efd03def9465e4f97b2", @@ -9851,8 +9851,8 @@ "databricks": "e2879cf4770e7f48d2dd704a", "duckdb": "e2879cf4770e7f48d2dd704a", "generic": "e2879cf4770e7f48d2dd704a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e2879cf4770e7f48d2dd704a", +"motherduck": "e2879cf4770e7f48d2dd704a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2d4aea8dec5521e258596f3d", "postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", @@ -9868,8 +9868,8 @@ "databricks": "80c23e10e486925cf3b33f9d", "duckdb": "80c23e10e486925cf3b33f9d", "generic": "80c23e10e486925cf3b33f9d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "80c23e10e486925cf3b33f9d", +"motherduck": "80c23e10e486925cf3b33f9d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "30466b1eff038acb4463e46d", "postgresql": "30466b1eff038acb4463e46d", "snowflake": "80c23e10e486925cf3b33f9d", @@ -9885,8 +9885,8 @@ "databricks": "60a23b7dcae6c79f4de83068", "duckdb": "60a23b7dcae6c79f4de83068", "generic": "60a23b7dcae6c79f4de83068", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "60a23b7dcae6c79f4de83068", +"motherduck": "60a23b7dcae6c79f4de83068", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "60a23b7dcae6c79f4de83068", "postgresql": "60a23b7dcae6c79f4de83068", "snowflake": "60a23b7dcae6c79f4de83068", @@ -9902,8 +9902,8 @@ "databricks": "f4c43a87a78970bca1f88b09", "duckdb": "f4c43a87a78970bca1f88b09", "generic": "f4c43a87a78970bca1f88b09", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f4c43a87a78970bca1f88b09", +"motherduck": "f4c43a87a78970bca1f88b09", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f4c43a87a78970bca1f88b09", "postgresql": "f4c43a87a78970bca1f88b09", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -9919,8 +9919,8 @@ "databricks": "3f71ab5f3c1c040da96f2cec", "duckdb": "3f71ab5f3c1c040da96f2cec", "generic": "3f71ab5f3c1c040da96f2cec", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3f71ab5f3c1c040da96f2cec", +"motherduck": "3f71ab5f3c1c040da96f2cec", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3f71ab5f3c1c040da96f2cec", "postgresql": "3f71ab5f3c1c040da96f2cec", "snowflake": "3f71ab5f3c1c040da96f2cec", @@ -9936,8 +9936,8 @@ "databricks": "c4346d0257336219f1d26890", "duckdb": "c4346d0257336219f1d26890", "generic": "c4346d0257336219f1d26890", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c4346d0257336219f1d26890", +"motherduck": "c4346d0257336219f1d26890", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "19bdd92ea0cc5fb028657510", "postgresql": "19bdd92ea0cc5fb028657510", "snowflake": "4e24a80b20b9dee0cbbfac17", @@ -9953,8 +9953,8 @@ "databricks": "cabeb00c4bc6c9e3d88a79ed", "duckdb": "bcc4b2c4a8f93d406cf00bc8", "generic": "cabeb00c4bc6c9e3d88a79ed", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "bcc4b2c4a8f93d406cf00bc8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "bcc4b2c4a8f93d406cf00bc8", "postgresql": "bcc4b2c4a8f93d406cf00bc8", "snowflake": "c4046288c6fec9a1344b0faa", @@ -9970,8 +9970,8 @@ "databricks": "d61c83ef8bfdf043fa672cb5", "duckdb": "d61c83ef8bfdf043fa672cb5", "generic": "d61c83ef8bfdf043fa672cb5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d61c83ef8bfdf043fa672cb5", +"motherduck": "d61c83ef8bfdf043fa672cb5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d61c83ef8bfdf043fa672cb5", "postgresql": "d61c83ef8bfdf043fa672cb5", "snowflake": "97a20344d811fb77c7690863", @@ -9987,8 +9987,8 @@ "databricks": "d2c97d221599d038d9875873", "duckdb": "d2c97d221599d038d9875873", "generic": "d2c97d221599d038d9875873", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d2c97d221599d038d9875873", +"motherduck": "d2c97d221599d038d9875873", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "55155766823b6a3a5e882fc2", "postgresql": "55155766823b6a3a5e882fc2", "snowflake": "d2c97d221599d038d9875873", @@ -10004,8 +10004,8 @@ "databricks": "d227b99a82e505d599c2ed8a", "duckdb": "35a9c5c1419bff507cf65ae2", "generic": "d227b99a82e505d599c2ed8a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d227b99a82e505d599c2ed8a", +"motherduck": "35a9c5c1419bff507cf65ae2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "35a9c5c1419bff507cf65ae2", "postgresql": "35a9c5c1419bff507cf65ae2", "snowflake": "d227b99a82e505d599c2ed8a", @@ -10021,8 +10021,8 @@ "databricks": "fa07c8ec6ebf3228010ebd9a", "duckdb": "fa07c8ec6ebf3228010ebd9a", "generic": "fa07c8ec6ebf3228010ebd9a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fa07c8ec6ebf3228010ebd9a", +"motherduck": "fa07c8ec6ebf3228010ebd9a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "fa07c8ec6ebf3228010ebd9a", "postgresql": "fa07c8ec6ebf3228010ebd9a", "snowflake": "fa07c8ec6ebf3228010ebd9a", @@ -10038,8 +10038,8 @@ "databricks": "ab5ad6180652b3b00d1f5b01", "duckdb": "ab5ad6180652b3b00d1f5b01", "generic": "ab5ad6180652b3b00d1f5b01", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ab5ad6180652b3b00d1f5b01", +"motherduck": "ab5ad6180652b3b00d1f5b01", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ab5ad6180652b3b00d1f5b01", "postgresql": "ab5ad6180652b3b00d1f5b01", "snowflake": "226beccc1d430773f4e67b70", @@ -10055,8 +10055,8 @@ "databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", "generic": "fb4b6f5101f973d16ea8d1f3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fb4b6f5101f973d16ea8d1f3", +"motherduck": "70fb4728c3adcf6dbe4975d5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", @@ -10072,8 +10072,8 @@ "databricks": "e46d4be44ab9e500e9190d26", "duckdb": "35e216b6921bf843994f2eea", "generic": "e46d4be44ab9e500e9190d26", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e46d4be44ab9e500e9190d26", +"motherduck": "35e216b6921bf843994f2eea", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "251aba3fef503ce64556c701", "postgresql": "251aba3fef503ce64556c701", "snowflake": "e46d4be44ab9e500e9190d26", @@ -10089,8 +10089,8 @@ "databricks": "c2700ca3573352821e09af03", "duckdb": "c2700ca3573352821e09af03", "generic": "c2700ca3573352821e09af03", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c2700ca3573352821e09af03", +"motherduck": "c2700ca3573352821e09af03", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c2700ca3573352821e09af03", "postgresql": "c2700ca3573352821e09af03", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -10106,8 +10106,8 @@ "databricks": "a428302e4848ffe877284c84", "duckdb": "a428302e4848ffe877284c84", "generic": "a428302e4848ffe877284c84", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a428302e4848ffe877284c84", +"motherduck": "a428302e4848ffe877284c84", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a428302e4848ffe877284c84", "postgresql": "a428302e4848ffe877284c84", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -10123,8 +10123,8 @@ "databricks": "4b27937f44f966ff9f3a7c76", "duckdb": "4b27937f44f966ff9f3a7c76", "generic": "4b27937f44f966ff9f3a7c76", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4b27937f44f966ff9f3a7c76", +"motherduck": "4b27937f44f966ff9f3a7c76", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "4b27937f44f966ff9f3a7c76", "postgresql": "4b27937f44f966ff9f3a7c76", "snowflake": "4b27937f44f966ff9f3a7c76", @@ -10140,8 +10140,8 @@ "databricks": "d63e1b6839b94c8b42376477", "duckdb": "d63e1b6839b94c8b42376477", "generic": "d63e1b6839b94c8b42376477", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d63e1b6839b94c8b42376477", +"motherduck": "d63e1b6839b94c8b42376477", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d63e1b6839b94c8b42376477", "postgresql": "d63e1b6839b94c8b42376477", "snowflake": "d63e1b6839b94c8b42376477", @@ -10157,8 +10157,8 @@ "databricks": "c752fd9fe2ad3b86b8b899da", "duckdb": "c752fd9fe2ad3b86b8b899da", "generic": "c752fd9fe2ad3b86b8b899da", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c752fd9fe2ad3b86b8b899da", +"motherduck": "c752fd9fe2ad3b86b8b899da", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e998cd8f0f74af0526b75ff0", "postgresql": "e998cd8f0f74af0526b75ff0", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -10174,8 +10174,8 @@ "databricks": "1e75e5227bea0ae294ac7af1", "duckdb": "19bdd92ea0cc5fb028657510", "generic": "1e75e5227bea0ae294ac7af1", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "19bdd92ea0cc5fb028657510", +"motherduck": "19bdd92ea0cc5fb028657510", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "19bdd92ea0cc5fb028657510", "postgresql": "19bdd92ea0cc5fb028657510", "snowflake": "8ff614b7306c0edbe36ce00b", @@ -10191,8 +10191,8 @@ "databricks": "57922064a7dc546cbb50e42b", "duckdb": "57922064a7dc546cbb50e42b", "generic": "57922064a7dc546cbb50e42b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "57922064a7dc546cbb50e42b", +"motherduck": "57922064a7dc546cbb50e42b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "57922064a7dc546cbb50e42b", "postgresql": "57922064a7dc546cbb50e42b", "snowflake": "57922064a7dc546cbb50e42b", @@ -10208,8 +10208,8 @@ "databricks": "d4a375fcf7c3190951992469", "duckdb": "d4a375fcf7c3190951992469", "generic": "d4a375fcf7c3190951992469", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d4a375fcf7c3190951992469", +"motherduck": "d4a375fcf7c3190951992469", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d4a375fcf7c3190951992469", "postgresql": "d4a375fcf7c3190951992469", "snowflake": "6306b9e88f7b13dbb6a32c00", @@ -10225,8 +10225,8 @@ "databricks": "47e0816fecdd9e015d7aeb79", "duckdb": "aa7afbd57560be67e5221a3b", "generic": "47e0816fecdd9e015d7aeb79", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "aa7afbd57560be67e5221a3b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "aa7afbd57560be67e5221a3b", "postgresql": "aa7afbd57560be67e5221a3b", "snowflake": "e6f36041b1d9fb8fe3a18cbb", @@ -10242,8 +10242,8 @@ "databricks": "14c32cfb5e5135338d015f42", "duckdb": "14c32cfb5e5135338d015f42", "generic": "14c32cfb5e5135338d015f42", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "14c32cfb5e5135338d015f42", +"motherduck": "14c32cfb5e5135338d015f42", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "14c32cfb5e5135338d015f42", "postgresql": "14c32cfb5e5135338d015f42", "snowflake": "14c32cfb5e5135338d015f42", @@ -10259,8 +10259,8 @@ "databricks": "73d31c6e78cdc3b595b9c4e9", "duckdb": "73d31c6e78cdc3b595b9c4e9", "generic": "73d31c6e78cdc3b595b9c4e9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "73d31c6e78cdc3b595b9c4e9", +"motherduck": "73d31c6e78cdc3b595b9c4e9", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "73d31c6e78cdc3b595b9c4e9", "postgresql": "73d31c6e78cdc3b595b9c4e9", "snowflake": "73d31c6e78cdc3b595b9c4e9", @@ -10276,8 +10276,8 @@ "databricks": "6f763a99e9c69109526b40c0", "duckdb": "6f763a99e9c69109526b40c0", "generic": "6f763a99e9c69109526b40c0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6f763a99e9c69109526b40c0", +"motherduck": "6f763a99e9c69109526b40c0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5226d7e2a62c7650a87d6e84", "postgresql": "5226d7e2a62c7650a87d6e84", "snowflake": "2705f19942a4194a72576911", @@ -10293,8 +10293,8 @@ "databricks": "78b2cf275c640af54feeca31", "duckdb": "78b2cf275c640af54feeca31", "generic": "78b2cf275c640af54feeca31", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "78b2cf275c640af54feeca31", +"motherduck": "78b2cf275c640af54feeca31", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "78b2cf275c640af54feeca31", "postgresql": "78b2cf275c640af54feeca31", "snowflake": "8ff614b7306c0edbe36ce00b", @@ -10310,8 +10310,8 @@ "databricks": "f78e07518606fbc42e0f28fe", "duckdb": "f78e07518606fbc42e0f28fe", "generic": "f78e07518606fbc42e0f28fe", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f78e07518606fbc42e0f28fe", +"motherduck": "f78e07518606fbc42e0f28fe", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "726bc47de786f118c37530f7", "postgresql": "726bc47de786f118c37530f7", "snowflake": "f78e07518606fbc42e0f28fe", @@ -10327,8 +10327,8 @@ "databricks": "11f5a5e0f332f3a87c8ff396", "duckdb": "11f5a5e0f332f3a87c8ff396", "generic": "11f5a5e0f332f3a87c8ff396", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "11f5a5e0f332f3a87c8ff396", +"motherduck": "11f5a5e0f332f3a87c8ff396", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "11f5a5e0f332f3a87c8ff396", "postgresql": "11f5a5e0f332f3a87c8ff396", "snowflake": "11f5a5e0f332f3a87c8ff396", @@ -10344,8 +10344,8 @@ "databricks": "ebc821a95b9286e22c2b26d0", "duckdb": "ebc821a95b9286e22c2b26d0", "generic": "ebc821a95b9286e22c2b26d0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ebc821a95b9286e22c2b26d0", +"motherduck": "ebc821a95b9286e22c2b26d0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ebc821a95b9286e22c2b26d0", "postgresql": "ebc821a95b9286e22c2b26d0", "snowflake": "ebc821a95b9286e22c2b26d0", @@ -10361,8 +10361,8 @@ "databricks": "e95f9763c510e80ddb1422a0", "duckdb": "143fb490be175b5b233c87a4", "generic": "143fb490be175b5b233c87a4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "143fb490be175b5b233c87a4", +"motherduck": "143fb490be175b5b233c87a4", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "143fb490be175b5b233c87a4", "postgresql": "143fb490be175b5b233c87a4", "snowflake": "e6d0f723aa7559449fb75d84", @@ -10378,8 +10378,8 @@ "databricks": "d22967425ad0e5a048596ac6", "duckdb": "d22967425ad0e5a048596ac6", "generic": "d22967425ad0e5a048596ac6", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d22967425ad0e5a048596ac6", +"motherduck": "d22967425ad0e5a048596ac6", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d22967425ad0e5a048596ac6", "postgresql": "d22967425ad0e5a048596ac6", "snowflake": "d22967425ad0e5a048596ac6", @@ -10395,8 +10395,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1eb7fb4e8cf75773c5573562", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1eb7fb4e8cf75773c5573562", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -10412,8 +10412,8 @@ "databricks": "eded69a48ff8476dbf73ff05", "duckdb": "eded69a48ff8476dbf73ff05", "generic": "eded69a48ff8476dbf73ff05", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "eded69a48ff8476dbf73ff05", +"motherduck": "eded69a48ff8476dbf73ff05", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "eded69a48ff8476dbf73ff05", "postgresql": "eded69a48ff8476dbf73ff05", "snowflake": "eded69a48ff8476dbf73ff05", @@ -10429,8 +10429,8 @@ "databricks": "d7bedd3c7663f140f4d10bd4", "duckdb": "d7bedd3c7663f140f4d10bd4", "generic": "d7bedd3c7663f140f4d10bd4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d7bedd3c7663f140f4d10bd4", +"motherduck": "d7bedd3c7663f140f4d10bd4", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d7e01d31db59d1d0507cd75c", "postgresql": "d7e01d31db59d1d0507cd75c", "snowflake": "d7bedd3c7663f140f4d10bd4", @@ -10446,8 +10446,8 @@ "databricks": "d2c97d221599d038d9875873", "duckdb": "d2c97d221599d038d9875873", "generic": "d2c97d221599d038d9875873", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d2c97d221599d038d9875873", +"motherduck": "d2c97d221599d038d9875873", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "55155766823b6a3a5e882fc2", "postgresql": "55155766823b6a3a5e882fc2", "snowflake": "d2c97d221599d038d9875873", @@ -10463,8 +10463,8 @@ "databricks": "c82815abe30b3d099903f838", "duckdb": "c82815abe30b3d099903f838", "generic": "c82815abe30b3d099903f838", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c82815abe30b3d099903f838", +"motherduck": "c82815abe30b3d099903f838", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c82815abe30b3d099903f838", "postgresql": "c82815abe30b3d099903f838", "snowflake": "c82815abe30b3d099903f838", @@ -10480,8 +10480,8 @@ "databricks": "3739567e772c71d3b587ca23", "duckdb": "3739567e772c71d3b587ca23", "generic": "3739567e772c71d3b587ca23", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3739567e772c71d3b587ca23", +"motherduck": "3739567e772c71d3b587ca23", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3739567e772c71d3b587ca23", "postgresql": "3739567e772c71d3b587ca23", "snowflake": "3739567e772c71d3b587ca23", @@ -10497,8 +10497,8 @@ "databricks": "83fdbd1f0a6ee4e72faa4d94", "duckdb": "83fdbd1f0a6ee4e72faa4d94", "generic": "83fdbd1f0a6ee4e72faa4d94", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "83fdbd1f0a6ee4e72faa4d94", +"motherduck": "83fdbd1f0a6ee4e72faa4d94", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "83fdbd1f0a6ee4e72faa4d94", "postgresql": "83fdbd1f0a6ee4e72faa4d94", "snowflake": "83fdbd1f0a6ee4e72faa4d94", @@ -10514,8 +10514,8 @@ "databricks": "af57dd2575fa5a01917ec9cf", "duckdb": "af57dd2575fa5a01917ec9cf", "generic": "af57dd2575fa5a01917ec9cf", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "af57dd2575fa5a01917ec9cf", +"motherduck": "af57dd2575fa5a01917ec9cf", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "af57dd2575fa5a01917ec9cf", "postgresql": "af57dd2575fa5a01917ec9cf", "snowflake": "af57dd2575fa5a01917ec9cf", @@ -10531,8 +10531,8 @@ "databricks": "c50a316d091c821866e4bfba", "duckdb": "c50a316d091c821866e4bfba", "generic": "c50a316d091c821866e4bfba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -10548,8 +10548,8 @@ "databricks": "d3bcf10e00645b12a802c7d0", "duckdb": "c0eec31620dfc2ac0028173d", "generic": "d3bcf10e00645b12a802c7d0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "c0eec31620dfc2ac0028173d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c0eec31620dfc2ac0028173d", "postgresql": "c0eec31620dfc2ac0028173d", "snowflake": "d379c0ee4dad89e570b7329e", @@ -10565,8 +10565,8 @@ "databricks": "39257e40f37cf05c780a4e22", "duckdb": "39257e40f37cf05c780a4e22", "generic": "39257e40f37cf05c780a4e22", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "39257e40f37cf05c780a4e22", +"motherduck": "39257e40f37cf05c780a4e22", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "39257e40f37cf05c780a4e22", "postgresql": "39257e40f37cf05c780a4e22", "snowflake": "39257e40f37cf05c780a4e22", @@ -10582,8 +10582,8 @@ "databricks": "aab0389e2b5470136660bf8a", "duckdb": "aab0389e2b5470136660bf8a", "generic": "aab0389e2b5470136660bf8a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "aab0389e2b5470136660bf8a", +"motherduck": "aab0389e2b5470136660bf8a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f16105cbc61d547e6c08efc8", "postgresql": "f16105cbc61d547e6c08efc8", "snowflake": "97a20344d811fb77c7690863", @@ -10599,8 +10599,8 @@ "databricks": "251aba3fef503ce64556c701", "duckdb": "251aba3fef503ce64556c701", "generic": "251aba3fef503ce64556c701", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "251aba3fef503ce64556c701", +"motherduck": "251aba3fef503ce64556c701", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "251aba3fef503ce64556c701", "postgresql": "251aba3fef503ce64556c701", "snowflake": "c50a316d091c821866e4bfba", @@ -10616,8 +10616,8 @@ "databricks": "aae2b1fe6f2d9807ecde412b", "duckdb": "aae2b1fe6f2d9807ecde412b", "generic": "aae2b1fe6f2d9807ecde412b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "aae2b1fe6f2d9807ecde412b", +"motherduck": "aae2b1fe6f2d9807ecde412b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "aae2b1fe6f2d9807ecde412b", "postgresql": "aae2b1fe6f2d9807ecde412b", "snowflake": "aae2b1fe6f2d9807ecde412b", @@ -10633,8 +10633,8 @@ "databricks": "ebc821a95b9286e22c2b26d0", "duckdb": "ebc821a95b9286e22c2b26d0", "generic": "ebc821a95b9286e22c2b26d0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ebc821a95b9286e22c2b26d0", +"motherduck": "ebc821a95b9286e22c2b26d0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ebc821a95b9286e22c2b26d0", "postgresql": "ebc821a95b9286e22c2b26d0", "snowflake": "ebc821a95b9286e22c2b26d0", @@ -10650,8 +10650,8 @@ "databricks": "251aba3fef503ce64556c701", "duckdb": "251aba3fef503ce64556c701", "generic": "251aba3fef503ce64556c701", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "251aba3fef503ce64556c701", +"motherduck": "251aba3fef503ce64556c701", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "251aba3fef503ce64556c701", "postgresql": "251aba3fef503ce64556c701", "snowflake": "c50a316d091c821866e4bfba", @@ -10667,8 +10667,8 @@ "databricks": "1bdf5908ca249b2331a0427c", "duckdb": "1bdf5908ca249b2331a0427c", "generic": "1bdf5908ca249b2331a0427c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1bdf5908ca249b2331a0427c", +"motherduck": "1bdf5908ca249b2331a0427c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1bdf5908ca249b2331a0427c", "postgresql": "1bdf5908ca249b2331a0427c", "snowflake": "8ff614b7306c0edbe36ce00b", @@ -10684,8 +10684,8 @@ "databricks": "3f71ab5f3c1c040da96f2cec", "duckdb": "3f71ab5f3c1c040da96f2cec", "generic": "3f71ab5f3c1c040da96f2cec", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3f71ab5f3c1c040da96f2cec", +"motherduck": "3f71ab5f3c1c040da96f2cec", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3f71ab5f3c1c040da96f2cec", "postgresql": "3f71ab5f3c1c040da96f2cec", "snowflake": "3f71ab5f3c1c040da96f2cec", @@ -10701,8 +10701,8 @@ "databricks": "53ec6db7b2c81e2ceee285a3", "duckdb": "53ec6db7b2c81e2ceee285a3", "generic": "53ec6db7b2c81e2ceee285a3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "53ec6db7b2c81e2ceee285a3", +"motherduck": "53ec6db7b2c81e2ceee285a3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "53ec6db7b2c81e2ceee285a3", "postgresql": "53ec6db7b2c81e2ceee285a3", "snowflake": "53ec6db7b2c81e2ceee285a3", @@ -10718,8 +10718,8 @@ "databricks": "4b356542044f1267fcfc487a", "duckdb": "5abf4ec799a2ec32fb10da7e", "generic": "4b356542044f1267fcfc487a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4b356542044f1267fcfc487a", +"motherduck": "5abf4ec799a2ec32fb10da7e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a5656d70e208254d1d3e9b4f", "postgresql": "a5656d70e208254d1d3e9b4f", "snowflake": "a5656d70e208254d1d3e9b4f", @@ -10735,8 +10735,8 @@ "databricks": "461923a0d34defd0c726d6e5", "duckdb": "461923a0d34defd0c726d6e5", "generic": "461923a0d34defd0c726d6e5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "461923a0d34defd0c726d6e5", +"motherduck": "461923a0d34defd0c726d6e5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "461923a0d34defd0c726d6e5", "postgresql": "461923a0d34defd0c726d6e5", "snowflake": "461923a0d34defd0c726d6e5", @@ -10752,8 +10752,8 @@ "databricks": "2427f5522575cdc3d6a74e4c", "duckdb": "2427f5522575cdc3d6a74e4c", "generic": "2427f5522575cdc3d6a74e4c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2427f5522575cdc3d6a74e4c", +"motherduck": "2427f5522575cdc3d6a74e4c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2427f5522575cdc3d6a74e4c", "postgresql": "2427f5522575cdc3d6a74e4c", "snowflake": "2427f5522575cdc3d6a74e4c", @@ -10769,8 +10769,8 @@ "databricks": "158229f1c3f1085b80d3190e", "duckdb": "158229f1c3f1085b80d3190e", "generic": "158229f1c3f1085b80d3190e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "158229f1c3f1085b80d3190e", +"motherduck": "158229f1c3f1085b80d3190e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7a1a9517a8cf96477b67c17e", "postgresql": "7a1a9517a8cf96477b67c17e", "snowflake": "158229f1c3f1085b80d3190e", @@ -10786,8 +10786,8 @@ "databricks": "e3bc1b8bad22d8104417b00f", "duckdb": "e3bc1b8bad22d8104417b00f", "generic": "e3bc1b8bad22d8104417b00f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e3bc1b8bad22d8104417b00f", +"motherduck": "e3bc1b8bad22d8104417b00f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b7d6fc051a3c1ea6deb29ff4", "postgresql": "b7d6fc051a3c1ea6deb29ff4", "snowflake": "e3bc1b8bad22d8104417b00f", @@ -10803,8 +10803,8 @@ "databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", "generic": "fb4b6f5101f973d16ea8d1f3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fb4b6f5101f973d16ea8d1f3", +"motherduck": "70fb4728c3adcf6dbe4975d5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", @@ -10820,8 +10820,8 @@ "databricks": "9bb8e9bdc03c6262fc0b887e", "duckdb": "9bb8e9bdc03c6262fc0b887e", "generic": "9bb8e9bdc03c6262fc0b887e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9bb8e9bdc03c6262fc0b887e", +"motherduck": "9bb8e9bdc03c6262fc0b887e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f2ce9dc78f83119cd19850e3", "postgresql": "f2ce9dc78f83119cd19850e3", "snowflake": "9bb8e9bdc03c6262fc0b887e", @@ -10837,8 +10837,8 @@ "databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", "generic": "fb4b6f5101f973d16ea8d1f3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fb4b6f5101f973d16ea8d1f3", +"motherduck": "70fb4728c3adcf6dbe4975d5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", @@ -10854,8 +10854,8 @@ "databricks": "b69a57be16780e0a5e4acadb", "duckdb": "b69a57be16780e0a5e4acadb", "generic": "b69a57be16780e0a5e4acadb", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b69a57be16780e0a5e4acadb", +"motherduck": "b69a57be16780e0a5e4acadb", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a7d1c1767c3d4e900c7d3cb4", "postgresql": "a7d1c1767c3d4e900c7d3cb4", "snowflake": "b69a57be16780e0a5e4acadb", @@ -10871,8 +10871,8 @@ "databricks": "c703e456945a8f06d005c98b", "duckdb": "c703e456945a8f06d005c98b", "generic": "c703e456945a8f06d005c98b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c703e456945a8f06d005c98b", +"motherduck": "c703e456945a8f06d005c98b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c703e456945a8f06d005c98b", "postgresql": "c703e456945a8f06d005c98b", "snowflake": "c703e456945a8f06d005c98b", @@ -10888,8 +10888,8 @@ "databricks": "8371f7fd8d5afa0c7c86482b", "duckdb": "8371f7fd8d5afa0c7c86482b", "generic": "8371f7fd8d5afa0c7c86482b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8371f7fd8d5afa0c7c86482b", +"motherduck": "8371f7fd8d5afa0c7c86482b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8371f7fd8d5afa0c7c86482b", "postgresql": "8371f7fd8d5afa0c7c86482b", "snowflake": "8371f7fd8d5afa0c7c86482b", @@ -10905,8 +10905,8 @@ "databricks": "f4fc387af06ec14d0ed37c37", "duckdb": "f4fc387af06ec14d0ed37c37", "generic": "f4fc387af06ec14d0ed37c37", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f4fc387af06ec14d0ed37c37", +"motherduck": "f4fc387af06ec14d0ed37c37", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "6e561b85b14713ee50331da1", "postgresql": "6e561b85b14713ee50331da1", "snowflake": "f4fc387af06ec14d0ed37c37", @@ -10922,8 +10922,8 @@ "databricks": "97a92f4a47efe38678dac07f", "duckdb": "97a92f4a47efe38678dac07f", "generic": "97a92f4a47efe38678dac07f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "97a92f4a47efe38678dac07f", +"motherduck": "97a92f4a47efe38678dac07f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "97a92f4a47efe38678dac07f", "postgresql": "97a92f4a47efe38678dac07f", "snowflake": "97a92f4a47efe38678dac07f", @@ -10939,8 +10939,8 @@ "databricks": "d61c83ef8bfdf043fa672cb5", "duckdb": "d61c83ef8bfdf043fa672cb5", "generic": "d61c83ef8bfdf043fa672cb5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d61c83ef8bfdf043fa672cb5", +"motherduck": "d61c83ef8bfdf043fa672cb5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d61c83ef8bfdf043fa672cb5", "postgresql": "d61c83ef8bfdf043fa672cb5", "snowflake": "97a20344d811fb77c7690863", @@ -10956,8 +10956,8 @@ "databricks": "c6889f2965db73017be6f6e7", "duckdb": "c6889f2965db73017be6f6e7", "generic": "c6889f2965db73017be6f6e7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c6889f2965db73017be6f6e7", +"motherduck": "c6889f2965db73017be6f6e7", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c6889f2965db73017be6f6e7", "postgresql": "c6889f2965db73017be6f6e7", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -10973,8 +10973,8 @@ "databricks": "279ea7f6f6a3910de04b795b", "duckdb": "279ea7f6f6a3910de04b795b", "generic": "279ea7f6f6a3910de04b795b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "279ea7f6f6a3910de04b795b", +"motherduck": "279ea7f6f6a3910de04b795b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "bc520aebd5872e40030ec7d9", "postgresql": "bc520aebd5872e40030ec7d9", "snowflake": "279ea7f6f6a3910de04b795b", @@ -10990,8 +10990,8 @@ "databricks": "04f0b885283ff5fc129874ad", "duckdb": "04f0b885283ff5fc129874ad", "generic": "04f0b885283ff5fc129874ad", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "04f0b885283ff5fc129874ad", +"motherduck": "04f0b885283ff5fc129874ad", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "04f0b885283ff5fc129874ad", "postgresql": "04f0b885283ff5fc129874ad", "snowflake": "04f0b885283ff5fc129874ad", @@ -11007,8 +11007,8 @@ "databricks": "c6654395c62d21b415299f72", "duckdb": "0712c33a016f7dd6a37ee011", "generic": "c6654395c62d21b415299f72", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c6654395c62d21b415299f72", +"motherduck": "0712c33a016f7dd6a37ee011", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c6654395c62d21b415299f72", "postgresql": "c6654395c62d21b415299f72", "snowflake": "0712c33a016f7dd6a37ee011", @@ -11024,8 +11024,8 @@ "databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", "generic": "fb4b6f5101f973d16ea8d1f3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fb4b6f5101f973d16ea8d1f3", +"motherduck": "70fb4728c3adcf6dbe4975d5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", @@ -11041,8 +11041,8 @@ "databricks": "93c1b5b5960e26479baf88d0", "duckdb": "93c1b5b5960e26479baf88d0", "generic": "93c1b5b5960e26479baf88d0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "93c1b5b5960e26479baf88d0", +"motherduck": "93c1b5b5960e26479baf88d0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "93c1b5b5960e26479baf88d0", "postgresql": "93c1b5b5960e26479baf88d0", "snowflake": "93c1b5b5960e26479baf88d0", @@ -11058,8 +11058,8 @@ "databricks": "6a912271e85e6d85c1238bc0", "duckdb": "6a912271e85e6d85c1238bc0", "generic": "6a912271e85e6d85c1238bc0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6a912271e85e6d85c1238bc0", +"motherduck": "6a912271e85e6d85c1238bc0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "6a912271e85e6d85c1238bc0", "postgresql": "6a912271e85e6d85c1238bc0", "snowflake": "6a912271e85e6d85c1238bc0", @@ -11075,8 +11075,8 @@ "databricks": "5abf4ec799a2ec32fb10da7e", "duckdb": "5abf4ec799a2ec32fb10da7e", "generic": "5abf4ec799a2ec32fb10da7e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5abf4ec799a2ec32fb10da7e", +"motherduck": "5abf4ec799a2ec32fb10da7e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5abf4ec799a2ec32fb10da7e", "postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "a5656d70e208254d1d3e9b4f", @@ -11092,8 +11092,8 @@ "databricks": "eea9a25a6fbf8e049a445513", "duckdb": "eea9a25a6fbf8e049a445513", "generic": "eea9a25a6fbf8e049a445513", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "eea9a25a6fbf8e049a445513", +"motherduck": "eea9a25a6fbf8e049a445513", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "769f762f4c7cffb2990ddacb", "postgresql": "769f762f4c7cffb2990ddacb", "snowflake": "eea9a25a6fbf8e049a445513", @@ -11109,8 +11109,8 @@ "databricks": "304be1d51ad99bfab3750f57", "duckdb": "304be1d51ad99bfab3750f57", "generic": "304be1d51ad99bfab3750f57", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "304be1d51ad99bfab3750f57", +"motherduck": "304be1d51ad99bfab3750f57", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "304be1d51ad99bfab3750f57", "postgresql": "304be1d51ad99bfab3750f57", "snowflake": "304be1d51ad99bfab3750f57", @@ -11126,8 +11126,8 @@ "databricks": "2bfa352725f9d8a91250f75e", "duckdb": "2bfa352725f9d8a91250f75e", "generic": "2bfa352725f9d8a91250f75e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2bfa352725f9d8a91250f75e", +"motherduck": "2bfa352725f9d8a91250f75e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2bfa352725f9d8a91250f75e", "postgresql": "2bfa352725f9d8a91250f75e", "snowflake": "2bfa352725f9d8a91250f75e", @@ -11143,8 +11143,8 @@ "databricks": "ebc821a95b9286e22c2b26d0", "duckdb": "ebc821a95b9286e22c2b26d0", "generic": "ebc821a95b9286e22c2b26d0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ebc821a95b9286e22c2b26d0", +"motherduck": "ebc821a95b9286e22c2b26d0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ebc821a95b9286e22c2b26d0", "postgresql": "ebc821a95b9286e22c2b26d0", "snowflake": "ebc821a95b9286e22c2b26d0", @@ -11160,8 +11160,8 @@ "databricks": "5d0a19a893dbf0c1e6e9f15c", "duckdb": "5d0a19a893dbf0c1e6e9f15c", "generic": "5d0a19a893dbf0c1e6e9f15c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5d0a19a893dbf0c1e6e9f15c", +"motherduck": "5d0a19a893dbf0c1e6e9f15c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5d0a19a893dbf0c1e6e9f15c", "postgresql": "5d0a19a893dbf0c1e6e9f15c", "snowflake": "5d0a19a893dbf0c1e6e9f15c", @@ -11177,8 +11177,8 @@ "databricks": "9bb8e9bdc03c6262fc0b887e", "duckdb": "9bb8e9bdc03c6262fc0b887e", "generic": "9bb8e9bdc03c6262fc0b887e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9bb8e9bdc03c6262fc0b887e", +"motherduck": "9bb8e9bdc03c6262fc0b887e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f2ce9dc78f83119cd19850e3", "postgresql": "f2ce9dc78f83119cd19850e3", "snowflake": "9bb8e9bdc03c6262fc0b887e", @@ -11194,8 +11194,8 @@ "databricks": "15f0b4e032d5524678c61cae", "duckdb": "15f0b4e032d5524678c61cae", "generic": "15f0b4e032d5524678c61cae", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "15f0b4e032d5524678c61cae", +"motherduck": "15f0b4e032d5524678c61cae", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "15f0b4e032d5524678c61cae", "postgresql": "15f0b4e032d5524678c61cae", "snowflake": "15f0b4e032d5524678c61cae", @@ -11211,8 +11211,8 @@ "databricks": "cafb5cff5526dd419baad0ed", "duckdb": "cafb5cff5526dd419baad0ed", "generic": "cafb5cff5526dd419baad0ed", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "cafb5cff5526dd419baad0ed", +"motherduck": "cafb5cff5526dd419baad0ed", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "cafb5cff5526dd419baad0ed", "postgresql": "cafb5cff5526dd419baad0ed", "snowflake": "cafb5cff5526dd419baad0ed", @@ -11228,8 +11228,8 @@ "databricks": "8a3f094f18fb4047f0febf59", "duckdb": "8a3f094f18fb4047f0febf59", "generic": "8a3f094f18fb4047f0febf59", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5c28019dce48a74708c90a76", +"motherduck": "8a3f094f18fb4047f0febf59", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8a3f094f18fb4047f0febf59", "postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "850deea990b52b82b36a9848", @@ -11245,8 +11245,8 @@ "databricks": "bf5a83d0c02f8890941c7ec8", "duckdb": "bf5a83d0c02f8890941c7ec8", "generic": "bf5a83d0c02f8890941c7ec8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "bf5a83d0c02f8890941c7ec8", +"motherduck": "bf5a83d0c02f8890941c7ec8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ef750421d1e193fa61992c79", "postgresql": "ef750421d1e193fa61992c79", "snowflake": "bf5a83d0c02f8890941c7ec8", @@ -11262,8 +11262,8 @@ "databricks": "accf46359aa8c74483b9a197", "duckdb": "4c16382f7ad617ac3cbfa833", "generic": "accf46359aa8c74483b9a197", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "accf46359aa8c74483b9a197", +"motherduck": "4c16382f7ad617ac3cbfa833", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "accf46359aa8c74483b9a197", "postgresql": "accf46359aa8c74483b9a197", "snowflake": "303d6e0feaedc557ea64e414", @@ -11279,8 +11279,8 @@ "databricks": "3f998a9ab68d8b2514840489", "duckdb": "f2ed52f6bf46b4db9343551c", "generic": "3f998a9ab68d8b2514840489", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3f998a9ab68d8b2514840489", +"motherduck": "f2ed52f6bf46b4db9343551c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3f998a9ab68d8b2514840489", "postgresql": "3f998a9ab68d8b2514840489", "snowflake": "f2ed52f6bf46b4db9343551c", @@ -11296,8 +11296,8 @@ "databricks": "c84c0b1d4d7caa6744130456", "duckdb": "c84c0b1d4d7caa6744130456", "generic": "c84c0b1d4d7caa6744130456", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c84c0b1d4d7caa6744130456", +"motherduck": "c84c0b1d4d7caa6744130456", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "725c73e74f311417bb4f95e8", "postgresql": "725c73e74f311417bb4f95e8", "snowflake": "c84c0b1d4d7caa6744130456", @@ -11313,8 +11313,8 @@ "databricks": "aea8a680fa18770e56bf6220", "duckdb": "aea8a680fa18770e56bf6220", "generic": "aea8a680fa18770e56bf6220", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "aea8a680fa18770e56bf6220", +"motherduck": "aea8a680fa18770e56bf6220", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "836641bf4537dec3e767931d", "postgresql": "836641bf4537dec3e767931d", "snowflake": "aea8a680fa18770e56bf6220", @@ -11330,8 +11330,8 @@ "databricks": "19a1a80ffc06169f32694de8", "duckdb": "19a1a80ffc06169f32694de8", "generic": "19a1a80ffc06169f32694de8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "19a1a80ffc06169f32694de8", +"motherduck": "19a1a80ffc06169f32694de8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ab1f5f46e69284778bd66d58", "postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", @@ -11347,8 +11347,8 @@ "databricks": "a428302e4848ffe877284c84", "duckdb": "a428302e4848ffe877284c84", "generic": "a428302e4848ffe877284c84", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a428302e4848ffe877284c84", +"motherduck": "a428302e4848ffe877284c84", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a428302e4848ffe877284c84", "postgresql": "a428302e4848ffe877284c84", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -11364,8 +11364,8 @@ "databricks": "5aa51d84ac5e6c7a9306f348", "duckdb": "5aa51d84ac5e6c7a9306f348", "generic": "5aa51d84ac5e6c7a9306f348", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5aa51d84ac5e6c7a9306f348", +"motherduck": "5aa51d84ac5e6c7a9306f348", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5aa51d84ac5e6c7a9306f348", "postgresql": "5aa51d84ac5e6c7a9306f348", "snowflake": "5aa51d84ac5e6c7a9306f348", @@ -11381,8 +11381,8 @@ "databricks": "70fb4728c3adcf6dbe4975d5", "duckdb": "70fb4728c3adcf6dbe4975d5", "generic": "70fb4728c3adcf6dbe4975d5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "70fb4728c3adcf6dbe4975d5", +"motherduck": "70fb4728c3adcf6dbe4975d5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", @@ -11398,8 +11398,8 @@ "databricks": "dcb00b7053ffe69fe3ce99cd", "duckdb": "c7b697bbfa82dd2821ad64e9", "generic": "c7b697bbfa82dd2821ad64e9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c7b697bbfa82dd2821ad64e9", +"motherduck": "c7b697bbfa82dd2821ad64e9", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c7b697bbfa82dd2821ad64e9", "postgresql": "c7b697bbfa82dd2821ad64e9", "snowflake": "c7b697bbfa82dd2821ad64e9", @@ -11415,8 +11415,8 @@ "databricks": "c0b12bfc586282ce5ea85c45", "duckdb": "c0b12bfc586282ce5ea85c45", "generic": "c0b12bfc586282ce5ea85c45", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c0b12bfc586282ce5ea85c45", +"motherduck": "c0b12bfc586282ce5ea85c45", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c0b12bfc586282ce5ea85c45", "postgresql": "c0b12bfc586282ce5ea85c45", "snowflake": "c0b12bfc586282ce5ea85c45", @@ -11432,8 +11432,8 @@ "databricks": "806bf4b6d83b5dc9948e2054", "duckdb": "806bf4b6d83b5dc9948e2054", "generic": "806bf4b6d83b5dc9948e2054", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "806bf4b6d83b5dc9948e2054", +"motherduck": "806bf4b6d83b5dc9948e2054", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "806bf4b6d83b5dc9948e2054", "postgresql": "806bf4b6d83b5dc9948e2054", "snowflake": "806bf4b6d83b5dc9948e2054", @@ -11449,8 +11449,8 @@ "databricks": "871969531da28b86361e65d2", "duckdb": "871969531da28b86361e65d2", "generic": "871969531da28b86361e65d2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "871969531da28b86361e65d2", +"motherduck": "871969531da28b86361e65d2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "990e37f96d40e397fe74978c", "postgresql": "990e37f96d40e397fe74978c", "snowflake": "871969531da28b86361e65d2", @@ -11466,8 +11466,8 @@ "databricks": "4129901e0f313b38452e22af", "duckdb": "4129901e0f313b38452e22af", "generic": "4129901e0f313b38452e22af", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4129901e0f313b38452e22af", +"motherduck": "4129901e0f313b38452e22af", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "4129901e0f313b38452e22af", "postgresql": "4129901e0f313b38452e22af", "snowflake": "4129901e0f313b38452e22af", @@ -11483,8 +11483,8 @@ "databricks": "0c290c40483b9651ff557830", "duckdb": "0c290c40483b9651ff557830", "generic": "0c290c40483b9651ff557830", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0c290c40483b9651ff557830", +"motherduck": "0c290c40483b9651ff557830", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0c290c40483b9651ff557830", "postgresql": "0c290c40483b9651ff557830", "snowflake": "0c290c40483b9651ff557830", @@ -11500,8 +11500,8 @@ "databricks": "b1603277660bbe23982ff72a", "duckdb": "b0505024b3f15f191c70a470", "generic": "b1603277660bbe23982ff72a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "b0505024b3f15f191c70a470", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "fa1a8787c8f69b9f9be89526", "postgresql": "fa1a8787c8f69b9f9be89526", "snowflake": "50404426183626ce7b686f80", @@ -11517,8 +11517,8 @@ "databricks": "b63494be71c6c8936f6824a6", "duckdb": "b63494be71c6c8936f6824a6", "generic": "b63494be71c6c8936f6824a6", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b63494be71c6c8936f6824a6", +"motherduck": "b63494be71c6c8936f6824a6", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b63494be71c6c8936f6824a6", "postgresql": "b63494be71c6c8936f6824a6", "snowflake": "b63494be71c6c8936f6824a6", @@ -11534,8 +11534,8 @@ "databricks": "016c25f8f0c58947f8da9152", "duckdb": "016c25f8f0c58947f8da9152", "generic": "016c25f8f0c58947f8da9152", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "016c25f8f0c58947f8da9152", +"motherduck": "016c25f8f0c58947f8da9152", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "016c25f8f0c58947f8da9152", "postgresql": "016c25f8f0c58947f8da9152", "snowflake": "016c25f8f0c58947f8da9152", @@ -11551,8 +11551,8 @@ "databricks": "e02779be57c0fcfa69ee7746", "duckdb": "e02779be57c0fcfa69ee7746", "generic": "e02779be57c0fcfa69ee7746", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e02779be57c0fcfa69ee7746", +"motherduck": "e02779be57c0fcfa69ee7746", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e02779be57c0fcfa69ee7746", "postgresql": "e02779be57c0fcfa69ee7746", "snowflake": "e02779be57c0fcfa69ee7746", @@ -11568,8 +11568,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "ace891b6844cb59efad42031", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "ace891b6844cb59efad42031", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -11585,8 +11585,8 @@ "databricks": "b7507b03866507106687a313", "duckdb": "b7507b03866507106687a313", "generic": "b7507b03866507106687a313", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b7507b03866507106687a313", +"motherduck": "b7507b03866507106687a313", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b7507b03866507106687a313", "postgresql": "b7507b03866507106687a313", "snowflake": "b7507b03866507106687a313", @@ -11602,8 +11602,8 @@ "databricks": "fdc9a2fc03f4b67d24982d0a", "duckdb": "fdc9a2fc03f4b67d24982d0a", "generic": "fdc9a2fc03f4b67d24982d0a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fdc9a2fc03f4b67d24982d0a", +"motherduck": "fdc9a2fc03f4b67d24982d0a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "576eb7ad8e7eca2db50ec386", "postgresql": "576eb7ad8e7eca2db50ec386", "snowflake": "fdc9a2fc03f4b67d24982d0a", @@ -11619,8 +11619,8 @@ "databricks": "81a45386abae5f6be0a027d0", "duckdb": "5c8b98f516cedefc237fa721", "generic": "81a45386abae5f6be0a027d0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "81a45386abae5f6be0a027d0", +"motherduck": "5c8b98f516cedefc237fa721", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5c8b98f516cedefc237fa721", "postgresql": "5c8b98f516cedefc237fa721", "snowflake": "5c8b98f516cedefc237fa721", @@ -11636,8 +11636,8 @@ "databricks": "8d26a44bbe0d29bdb1509369", "duckdb": "27d7e27586eab431e308413f", "generic": "8d26a44bbe0d29bdb1509369", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8d26a44bbe0d29bdb1509369", +"motherduck": "27d7e27586eab431e308413f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8d26a44bbe0d29bdb1509369", "postgresql": "8d26a44bbe0d29bdb1509369", "snowflake": "8d26a44bbe0d29bdb1509369", @@ -11653,8 +11653,8 @@ "databricks": "3f71ab5f3c1c040da96f2cec", "duckdb": "3f71ab5f3c1c040da96f2cec", "generic": "3f71ab5f3c1c040da96f2cec", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3f71ab5f3c1c040da96f2cec", +"motherduck": "3f71ab5f3c1c040da96f2cec", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3f71ab5f3c1c040da96f2cec", "postgresql": "3f71ab5f3c1c040da96f2cec", "snowflake": "3f71ab5f3c1c040da96f2cec", @@ -11670,8 +11670,8 @@ "databricks": "ab5ad6180652b3b00d1f5b01", "duckdb": "ab5ad6180652b3b00d1f5b01", "generic": "ab5ad6180652b3b00d1f5b01", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ab5ad6180652b3b00d1f5b01", +"motherduck": "ab5ad6180652b3b00d1f5b01", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ab5ad6180652b3b00d1f5b01", "postgresql": "ab5ad6180652b3b00d1f5b01", "snowflake": "f43dac38f34a1c0618a3f812", @@ -11687,8 +11687,8 @@ "databricks": "4662a1758442ce44b830269f", "duckdb": "4662a1758442ce44b830269f", "generic": "4662a1758442ce44b830269f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4662a1758442ce44b830269f", +"motherduck": "4662a1758442ce44b830269f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "4662a1758442ce44b830269f", "postgresql": "4662a1758442ce44b830269f", "snowflake": "4662a1758442ce44b830269f", @@ -11704,8 +11704,8 @@ "databricks": "34af912a178427fc6e8bdee3", "duckdb": "34af912a178427fc6e8bdee3", "generic": "34af912a178427fc6e8bdee3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "34af912a178427fc6e8bdee3", +"motherduck": "34af912a178427fc6e8bdee3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "34af912a178427fc6e8bdee3", "postgresql": "34af912a178427fc6e8bdee3", "snowflake": "34af912a178427fc6e8bdee3", @@ -11721,8 +11721,8 @@ "databricks": "29e896ae6f2844ec3744aad6", "duckdb": "27d7e27586eab431e308413f", "generic": "29e896ae6f2844ec3744aad6", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "29e896ae6f2844ec3744aad6", +"motherduck": "27d7e27586eab431e308413f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "29e896ae6f2844ec3744aad6", "postgresql": "29e896ae6f2844ec3744aad6", "snowflake": "29e896ae6f2844ec3744aad6", @@ -11738,8 +11738,8 @@ "databricks": "e8605878c6fffc044556a858", "duckdb": "e8605878c6fffc044556a858", "generic": "e8605878c6fffc044556a858", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e8605878c6fffc044556a858", +"motherduck": "e8605878c6fffc044556a858", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e8605878c6fffc044556a858", "postgresql": "e8605878c6fffc044556a858", "snowflake": "e8605878c6fffc044556a858", @@ -11755,8 +11755,8 @@ "databricks": "b561884132dbe7e42b719110", "duckdb": "b561884132dbe7e42b719110", "generic": "b561884132dbe7e42b719110", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b561884132dbe7e42b719110", +"motherduck": "b561884132dbe7e42b719110", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5aa51d84ac5e6c7a9306f348", "postgresql": "5aa51d84ac5e6c7a9306f348", "snowflake": "b561884132dbe7e42b719110", @@ -11772,8 +11772,8 @@ "databricks": "c6106f30f23f392e17eadef4", "duckdb": "c6106f30f23f392e17eadef4", "generic": "c6106f30f23f392e17eadef4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c6106f30f23f392e17eadef4", +"motherduck": "c6106f30f23f392e17eadef4", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f43dac38f34a1c0618a3f812", "postgresql": "f43dac38f34a1c0618a3f812", "snowflake": "c6106f30f23f392e17eadef4", @@ -11789,8 +11789,8 @@ "databricks": "e7aec8198ab628519324444a", "duckdb": "e7aec8198ab628519324444a", "generic": "e7aec8198ab628519324444a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e7aec8198ab628519324444a", +"motherduck": "e7aec8198ab628519324444a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e7aec8198ab628519324444a", "postgresql": "e7aec8198ab628519324444a", "snowflake": "e7aec8198ab628519324444a", @@ -11806,8 +11806,8 @@ "databricks": "881106a4a0d2de0ff7c56373", "duckdb": "881106a4a0d2de0ff7c56373", "generic": "881106a4a0d2de0ff7c56373", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "881106a4a0d2de0ff7c56373", +"motherduck": "881106a4a0d2de0ff7c56373", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "cf167ec395fc843602e3b1e0", "postgresql": "cf167ec395fc843602e3b1e0", "snowflake": "881106a4a0d2de0ff7c56373", @@ -11823,8 +11823,8 @@ "databricks": "2c6b371b030464f525066893", "duckdb": "2c6b371b030464f525066893", "generic": "2c6b371b030464f525066893", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2c6b371b030464f525066893", +"motherduck": "2c6b371b030464f525066893", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3feb479faeec777c2ade8afd", "postgresql": "3feb479faeec777c2ade8afd", "snowflake": "2c6b371b030464f525066893", @@ -11840,8 +11840,8 @@ "databricks": "e3d9a113277b9c69432b32d1", "duckdb": "e3d9a113277b9c69432b32d1", "generic": "e3d9a113277b9c69432b32d1", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e3d9a113277b9c69432b32d1", +"motherduck": "e3d9a113277b9c69432b32d1", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e3d9a113277b9c69432b32d1", "postgresql": "e3d9a113277b9c69432b32d1", "snowflake": "e3d9a113277b9c69432b32d1", @@ -11857,8 +11857,8 @@ "databricks": "ba0da7ef018b990fcc83cba1", "duckdb": "ba0da7ef018b990fcc83cba1", "generic": "ba0da7ef018b990fcc83cba1", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ba0da7ef018b990fcc83cba1", +"motherduck": "ba0da7ef018b990fcc83cba1", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ba0da7ef018b990fcc83cba1", "postgresql": "ba0da7ef018b990fcc83cba1", "snowflake": "ba0da7ef018b990fcc83cba1", @@ -11874,8 +11874,8 @@ "databricks": "e57ac1541fb51b362cd5b470", "duckdb": "396b2ce32efb5365b393c230", "generic": "e57ac1541fb51b362cd5b470", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e57ac1541fb51b362cd5b470", +"motherduck": "396b2ce32efb5365b393c230", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "396b2ce32efb5365b393c230", "postgresql": "396b2ce32efb5365b393c230", "snowflake": "4b356542044f1267fcfc487a", @@ -11891,8 +11891,8 @@ "databricks": "de89632e277dbc8c97115419", "duckdb": "5abf4ec799a2ec32fb10da7e", "generic": "de89632e277dbc8c97115419", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "de89632e277dbc8c97115419", +"motherduck": "5abf4ec799a2ec32fb10da7e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5abf4ec799a2ec32fb10da7e", "postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "a5656d70e208254d1d3e9b4f", @@ -11908,8 +11908,8 @@ "databricks": "319a9fbb02ed503eb114164b", "duckdb": "e060b5a5a8e9774b580d58d3", "generic": "e060b5a5a8e9774b580d58d3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e060b5a5a8e9774b580d58d3", +"motherduck": "e060b5a5a8e9774b580d58d3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e060b5a5a8e9774b580d58d3", "postgresql": "e060b5a5a8e9774b580d58d3", "snowflake": "e060b5a5a8e9774b580d58d3", @@ -11925,8 +11925,8 @@ "databricks": "67d251c042daee0bb2bf7351", "duckdb": "67d251c042daee0bb2bf7351", "generic": "67d251c042daee0bb2bf7351", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "67d251c042daee0bb2bf7351", +"motherduck": "67d251c042daee0bb2bf7351", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "67d251c042daee0bb2bf7351", "postgresql": "67d251c042daee0bb2bf7351", "snowflake": "67d251c042daee0bb2bf7351", @@ -11942,8 +11942,8 @@ "databricks": "558da38ec4d9d5d6c72f60cf", "duckdb": "919b7ec6a7b49d8527b784cd", "generic": "558da38ec4d9d5d6c72f60cf", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "558da38ec4d9d5d6c72f60cf", +"motherduck": "919b7ec6a7b49d8527b784cd", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "558da38ec4d9d5d6c72f60cf", "postgresql": "558da38ec4d9d5d6c72f60cf", "snowflake": "919b7ec6a7b49d8527b784cd", @@ -11959,8 +11959,8 @@ "databricks": "66ee8490513ef7b131cb4824", "duckdb": "66ee8490513ef7b131cb4824", "generic": "66ee8490513ef7b131cb4824", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "66ee8490513ef7b131cb4824", +"motherduck": "66ee8490513ef7b131cb4824", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "66ee8490513ef7b131cb4824", "postgresql": "66ee8490513ef7b131cb4824", "snowflake": "66ee8490513ef7b131cb4824", @@ -11976,8 +11976,8 @@ "databricks": "8f552eefca3955a73672f686", "duckdb": "8f552eefca3955a73672f686", "generic": "8f552eefca3955a73672f686", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8f552eefca3955a73672f686", +"motherduck": "8f552eefca3955a73672f686", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8f552eefca3955a73672f686", "postgresql": "8f552eefca3955a73672f686", "snowflake": "8f552eefca3955a73672f686", @@ -11993,8 +11993,8 @@ "databricks": "e2879cf4770e7f48d2dd704a", "duckdb": "e2879cf4770e7f48d2dd704a", "generic": "e2879cf4770e7f48d2dd704a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e2879cf4770e7f48d2dd704a", +"motherduck": "e2879cf4770e7f48d2dd704a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2d4aea8dec5521e258596f3d", "postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", @@ -12010,8 +12010,8 @@ "databricks": "5abf4ec799a2ec32fb10da7e", "duckdb": "5abf4ec799a2ec32fb10da7e", "generic": "5abf4ec799a2ec32fb10da7e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5abf4ec799a2ec32fb10da7e", +"motherduck": "5abf4ec799a2ec32fb10da7e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5abf4ec799a2ec32fb10da7e", "postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "a5656d70e208254d1d3e9b4f", @@ -12027,8 +12027,8 @@ "databricks": "19a1a80ffc06169f32694de8", "duckdb": "19a1a80ffc06169f32694de8", "generic": "19a1a80ffc06169f32694de8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "19a1a80ffc06169f32694de8", +"motherduck": "19a1a80ffc06169f32694de8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ab1f5f46e69284778bd66d58", "postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", @@ -12044,8 +12044,8 @@ "databricks": "853d4786e4edb4f90284765b", "duckdb": "f4c37736a388a1d793d4de1a", "generic": "853d4786e4edb4f90284765b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "f4c37736a388a1d793d4de1a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f4c37736a388a1d793d4de1a", "postgresql": "f4c37736a388a1d793d4de1a", "snowflake": "a640dde3e800eb6ea27813e3", @@ -12061,8 +12061,8 @@ "databricks": "e57ac1541fb51b362cd5b470", "duckdb": "e57ac1541fb51b362cd5b470", "generic": "e57ac1541fb51b362cd5b470", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e57ac1541fb51b362cd5b470", +"motherduck": "e57ac1541fb51b362cd5b470", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e57ac1541fb51b362cd5b470", "postgresql": "e57ac1541fb51b362cd5b470", "snowflake": "4b356542044f1267fcfc487a", @@ -12078,8 +12078,8 @@ "databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", "generic": "fb4b6f5101f973d16ea8d1f3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fb4b6f5101f973d16ea8d1f3", +"motherduck": "70fb4728c3adcf6dbe4975d5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", @@ -12095,8 +12095,8 @@ "databricks": "aab0389e2b5470136660bf8a", "duckdb": "aab0389e2b5470136660bf8a", "generic": "aab0389e2b5470136660bf8a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "aab0389e2b5470136660bf8a", +"motherduck": "aab0389e2b5470136660bf8a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f16105cbc61d547e6c08efc8", "postgresql": "f16105cbc61d547e6c08efc8", "snowflake": "97a20344d811fb77c7690863", @@ -12112,8 +12112,8 @@ "databricks": "19a1a80ffc06169f32694de8", "duckdb": "19a1a80ffc06169f32694de8", "generic": "19a1a80ffc06169f32694de8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "19a1a80ffc06169f32694de8", +"motherduck": "19a1a80ffc06169f32694de8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "19a1a80ffc06169f32694de8", "postgresql": "19a1a80ffc06169f32694de8", "snowflake": "19a1a80ffc06169f32694de8", @@ -12129,8 +12129,8 @@ "databricks": "776e4bf23c8ef10ffaceade2", "duckdb": "776e4bf23c8ef10ffaceade2", "generic": "776e4bf23c8ef10ffaceade2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "776e4bf23c8ef10ffaceade2", +"motherduck": "776e4bf23c8ef10ffaceade2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5d06b10843e3e6487fb9906a", "postgresql": "5d06b10843e3e6487fb9906a", "snowflake": "776e4bf23c8ef10ffaceade2", @@ -12146,8 +12146,8 @@ "databricks": "c50a316d091c821866e4bfba", "duckdb": "c50a316d091c821866e4bfba", "generic": "c50a316d091c821866e4bfba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -12163,8 +12163,8 @@ "databricks": "a3acd7ba35e81a800f967bd3", "duckdb": "42d4a23d9a52e79d0c3458d3", "generic": "a3acd7ba35e81a800f967bd3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a3acd7ba35e81a800f967bd3", +"motherduck": "42d4a23d9a52e79d0c3458d3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "42d4a23d9a52e79d0c3458d3", "postgresql": "42d4a23d9a52e79d0c3458d3", "snowflake": "a3acd7ba35e81a800f967bd3", @@ -12180,8 +12180,8 @@ "databricks": "95050a5e73c2bff179a785b7", "duckdb": "95050a5e73c2bff179a785b7", "generic": "95050a5e73c2bff179a785b7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "95050a5e73c2bff179a785b7", +"motherduck": "95050a5e73c2bff179a785b7", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e197cb69fc5e5754a25d4038", "postgresql": "e197cb69fc5e5754a25d4038", "snowflake": "95050a5e73c2bff179a785b7", @@ -12197,8 +12197,8 @@ "databricks": "0703bf71a54942bdaa28406e", "duckdb": "0703bf71a54942bdaa28406e", "generic": "0703bf71a54942bdaa28406e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0703bf71a54942bdaa28406e", +"motherduck": "0703bf71a54942bdaa28406e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0703bf71a54942bdaa28406e", "postgresql": "0703bf71a54942bdaa28406e", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -12214,8 +12214,8 @@ "databricks": "3a4d8bca5124769db23a43df", "duckdb": "01e3dfc024c37a8d62c7f810", "generic": "3a4d8bca5124769db23a43df", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "01e3dfc024c37a8d62c7f810", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "01e3dfc024c37a8d62c7f810", "postgresql": "01e3dfc024c37a8d62c7f810", "snowflake": "db03b5ccf153c75e43846a6a", @@ -12231,8 +12231,8 @@ "databricks": "aab0389e2b5470136660bf8a", "duckdb": "aab0389e2b5470136660bf8a", "generic": "aab0389e2b5470136660bf8a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "aab0389e2b5470136660bf8a", +"motherduck": "aab0389e2b5470136660bf8a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f16105cbc61d547e6c08efc8", "postgresql": "f16105cbc61d547e6c08efc8", "snowflake": "97a20344d811fb77c7690863", @@ -12248,8 +12248,8 @@ "databricks": "2bfa352725f9d8a91250f75e", "duckdb": "2bfa352725f9d8a91250f75e", "generic": "2bfa352725f9d8a91250f75e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2bfa352725f9d8a91250f75e", +"motherduck": "2bfa352725f9d8a91250f75e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2bfa352725f9d8a91250f75e", "postgresql": "2bfa352725f9d8a91250f75e", "snowflake": "2bfa352725f9d8a91250f75e", @@ -12265,8 +12265,8 @@ "databricks": "50a940812c6ed2e6dd842e74", "duckdb": "50a940812c6ed2e6dd842e74", "generic": "50a940812c6ed2e6dd842e74", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "50a940812c6ed2e6dd842e74", +"motherduck": "50a940812c6ed2e6dd842e74", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "50a940812c6ed2e6dd842e74", "postgresql": "50a940812c6ed2e6dd842e74", "snowflake": "50a940812c6ed2e6dd842e74", @@ -12282,8 +12282,8 @@ "databricks": "e57ac1541fb51b362cd5b470", "duckdb": "e57ac1541fb51b362cd5b470", "generic": "e57ac1541fb51b362cd5b470", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e57ac1541fb51b362cd5b470", +"motherduck": "e57ac1541fb51b362cd5b470", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e57ac1541fb51b362cd5b470", "postgresql": "e57ac1541fb51b362cd5b470", "snowflake": "4b356542044f1267fcfc487a", @@ -12299,8 +12299,8 @@ "databricks": "279ea7f6f6a3910de04b795b", "duckdb": "279ea7f6f6a3910de04b795b", "generic": "279ea7f6f6a3910de04b795b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "279ea7f6f6a3910de04b795b", +"motherduck": "279ea7f6f6a3910de04b795b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "bc520aebd5872e40030ec7d9", "postgresql": "bc520aebd5872e40030ec7d9", "snowflake": "279ea7f6f6a3910de04b795b", @@ -12316,8 +12316,8 @@ "databricks": "461923a0d34defd0c726d6e5", "duckdb": "461923a0d34defd0c726d6e5", "generic": "461923a0d34defd0c726d6e5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "461923a0d34defd0c726d6e5", +"motherduck": "461923a0d34defd0c726d6e5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "461923a0d34defd0c726d6e5", "postgresql": "461923a0d34defd0c726d6e5", "snowflake": "461923a0d34defd0c726d6e5", @@ -12333,8 +12333,8 @@ "databricks": "e57ac1541fb51b362cd5b470", "duckdb": "84fca9ac893777959cfd715c", "generic": "e57ac1541fb51b362cd5b470", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e57ac1541fb51b362cd5b470", +"motherduck": "84fca9ac893777959cfd715c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e57ac1541fb51b362cd5b470", "postgresql": "e57ac1541fb51b362cd5b470", "snowflake": "84fca9ac893777959cfd715c", @@ -12350,8 +12350,8 @@ "databricks": "e57ac1541fb51b362cd5b470", "duckdb": "e57ac1541fb51b362cd5b470", "generic": "e57ac1541fb51b362cd5b470", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e57ac1541fb51b362cd5b470", +"motherduck": "e57ac1541fb51b362cd5b470", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e57ac1541fb51b362cd5b470", "postgresql": "e57ac1541fb51b362cd5b470", "snowflake": "4b356542044f1267fcfc487a", @@ -12367,8 +12367,8 @@ "databricks": "75a99436882f66b2fdc219e5", "duckdb": "2cc8493833d5ac6046bfbfc2", "generic": "2cc8493833d5ac6046bfbfc2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2cc8493833d5ac6046bfbfc2", +"motherduck": "2cc8493833d5ac6046bfbfc2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2cc8493833d5ac6046bfbfc2", "postgresql": "2cc8493833d5ac6046bfbfc2", "snowflake": "70fb65041d155a676d8443d4", @@ -12384,8 +12384,8 @@ "databricks": "f80980deec760e5b50e05376", "duckdb": "8305d75697b54f509bbc0f53", "generic": "80723d96ed6ed21034faa9bd", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "8305d75697b54f509bbc0f53", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8305d75697b54f509bbc0f53", "postgresql": "8305d75697b54f509bbc0f53", "snowflake": "240e7de80b095b23fdb1fd92", @@ -12401,8 +12401,8 @@ "databricks": "d41d40c68507f4d02e348833", "duckdb": "d41d40c68507f4d02e348833", "generic": "d41d40c68507f4d02e348833", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d41d40c68507f4d02e348833", +"motherduck": "d41d40c68507f4d02e348833", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d41d40c68507f4d02e348833", "postgresql": "d41d40c68507f4d02e348833", "snowflake": "d41d40c68507f4d02e348833", @@ -12418,8 +12418,8 @@ "databricks": "8441d00030df06ad6a5534d3", "duckdb": "2c44dc35fd1ef8701101802e", "generic": "8441d00030df06ad6a5534d3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8441d00030df06ad6a5534d3", +"motherduck": "2c44dc35fd1ef8701101802e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8441d00030df06ad6a5534d3", "postgresql": "8441d00030df06ad6a5534d3", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -12435,8 +12435,8 @@ "databricks": "599b3293ed620803ce2401b5", "duckdb": "599b3293ed620803ce2401b5", "generic": "599b3293ed620803ce2401b5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "599b3293ed620803ce2401b5", +"motherduck": "599b3293ed620803ce2401b5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "363a6b7f05b0dac69e1e6547", "postgresql": "363a6b7f05b0dac69e1e6547", "snowflake": "599b3293ed620803ce2401b5", @@ -12452,8 +12452,8 @@ "databricks": "669084d1ea704ae5b8ee05c2", "duckdb": "669084d1ea704ae5b8ee05c2", "generic": "669084d1ea704ae5b8ee05c2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "669084d1ea704ae5b8ee05c2", +"motherduck": "669084d1ea704ae5b8ee05c2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "669084d1ea704ae5b8ee05c2", "postgresql": "669084d1ea704ae5b8ee05c2", "snowflake": "669084d1ea704ae5b8ee05c2", @@ -12469,8 +12469,8 @@ "databricks": "9e4a50a8aab648a9737f7d0c", "duckdb": "9e4a50a8aab648a9737f7d0c", "generic": "9e4a50a8aab648a9737f7d0c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9e4a50a8aab648a9737f7d0c", +"motherduck": "9e4a50a8aab648a9737f7d0c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "36d29258901a4d8c296baa68", "postgresql": "36d29258901a4d8c296baa68", "snowflake": "9e4a50a8aab648a9737f7d0c", @@ -12486,8 +12486,8 @@ "databricks": "ebc821a95b9286e22c2b26d0", "duckdb": "ebc821a95b9286e22c2b26d0", "generic": "ebc821a95b9286e22c2b26d0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ebc821a95b9286e22c2b26d0", +"motherduck": "ebc821a95b9286e22c2b26d0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ebc821a95b9286e22c2b26d0", "postgresql": "ebc821a95b9286e22c2b26d0", "snowflake": "ebc821a95b9286e22c2b26d0", @@ -12503,8 +12503,8 @@ "databricks": "606d813c3f1dfcf4921045c8", "duckdb": "9c4e957c3856f1159283313a", "generic": "8731ee5ed03816bf6b4a8464", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b9f1a78bcda3bc968346cc10", +"motherduck": "9c4e957c3856f1159283313a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9c4e957c3856f1159283313a", "postgresql": "9c4e957c3856f1159283313a", "snowflake": "cd2eb5ae8b54388fde5d5737", @@ -12520,8 +12520,8 @@ "databricks": "70fb4728c3adcf6dbe4975d5", "duckdb": "70fb4728c3adcf6dbe4975d5", "generic": "70fb4728c3adcf6dbe4975d5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "70fb4728c3adcf6dbe4975d5", +"motherduck": "70fb4728c3adcf6dbe4975d5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", @@ -12537,8 +12537,8 @@ "databricks": "19a1a80ffc06169f32694de8", "duckdb": "19a1a80ffc06169f32694de8", "generic": "19a1a80ffc06169f32694de8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "19a1a80ffc06169f32694de8", +"motherduck": "19a1a80ffc06169f32694de8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ab1f5f46e69284778bd66d58", "postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", @@ -12554,8 +12554,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1eb7fb4e8cf75773c5573562", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1eb7fb4e8cf75773c5573562", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -12571,8 +12571,8 @@ "databricks": "c77c5b64930f0a65094fd575", "duckdb": "0ceb777141e874f9277f0f53", "generic": "18cb3eafdbb91b315055426b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "18cb3eafdbb91b315055426b", +"motherduck": "0ceb777141e874f9277f0f53", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "18cb3eafdbb91b315055426b", "postgresql": "18cb3eafdbb91b315055426b", "snowflake": "d5bfcc48faca639b47d37309", @@ -12588,8 +12588,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1eb7fb4e8cf75773c5573562", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1eb7fb4e8cf75773c5573562", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -12605,8 +12605,8 @@ "databricks": "398f4aeaabc39054d76e214e", "duckdb": "7a79790250ede37cbc407ce6", "generic": "398f4aeaabc39054d76e214e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "398f4aeaabc39054d76e214e", +"motherduck": "7a79790250ede37cbc407ce6", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "398f4aeaabc39054d76e214e", "postgresql": "398f4aeaabc39054d76e214e", "snowflake": "398f4aeaabc39054d76e214e", @@ -12622,8 +12622,8 @@ "databricks": "e6ffdece2977be55da73b59a", "duckdb": "c50a316d091c821866e4bfba", "generic": "e6ffdece2977be55da73b59a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e6ffdece2977be55da73b59a", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -12639,8 +12639,8 @@ "databricks": "d227b99a82e505d599c2ed8a", "duckdb": "35a9c5c1419bff507cf65ae2", "generic": "d227b99a82e505d599c2ed8a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d227b99a82e505d599c2ed8a", +"motherduck": "35a9c5c1419bff507cf65ae2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "35a9c5c1419bff507cf65ae2", "postgresql": "35a9c5c1419bff507cf65ae2", "snowflake": "d227b99a82e505d599c2ed8a", @@ -12656,8 +12656,8 @@ "databricks": "b981394a3f4d961a44e73868", "duckdb": "4ed568ea1adb2c8a7141be20", "generic": "b981394a3f4d961a44e73868", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "4ed568ea1adb2c8a7141be20", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "4ed568ea1adb2c8a7141be20", "postgresql": "4ed568ea1adb2c8a7141be20", "snowflake": "b63ef04215d2dd65ccf32905", @@ -12673,8 +12673,8 @@ "databricks": "c50a316d091c821866e4bfba", "duckdb": "c50a316d091c821866e4bfba", "generic": "c50a316d091c821866e4bfba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -12690,8 +12690,8 @@ "databricks": "79278352c17c9de0f5a28af5", "duckdb": "79278352c17c9de0f5a28af5", "generic": "79278352c17c9de0f5a28af5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "79278352c17c9de0f5a28af5", +"motherduck": "79278352c17c9de0f5a28af5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "79278352c17c9de0f5a28af5", "postgresql": "79278352c17c9de0f5a28af5", "snowflake": "97a20344d811fb77c7690863", @@ -12707,8 +12707,8 @@ "databricks": "f774d5e8feadbf11edaa26b1", "duckdb": "f46cf4b4e131aa122b5b33d6", "generic": "f774d5e8feadbf11edaa26b1", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f774d5e8feadbf11edaa26b1", +"motherduck": "f46cf4b4e131aa122b5b33d6", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f46cf4b4e131aa122b5b33d6", "postgresql": "f46cf4b4e131aa122b5b33d6", "snowflake": "f46cf4b4e131aa122b5b33d6", @@ -12724,8 +12724,8 @@ "databricks": "4172129e8d84a0da597bc612", "duckdb": "0da0fad3107f7a26744c9411", "generic": "4172129e8d84a0da597bc612", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "0da0fad3107f7a26744c9411", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0da0fad3107f7a26744c9411", "postgresql": "0da0fad3107f7a26744c9411", "snowflake": "e21aafb74f6e5c5e181abd5e", @@ -12741,8 +12741,8 @@ "databricks": "5c528c7b4329aeb692ec6e94", "duckdb": "5c528c7b4329aeb692ec6e94", "generic": "5c528c7b4329aeb692ec6e94", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5c528c7b4329aeb692ec6e94", +"motherduck": "5c528c7b4329aeb692ec6e94", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5c528c7b4329aeb692ec6e94", "postgresql": "5c528c7b4329aeb692ec6e94", "snowflake": "5c528c7b4329aeb692ec6e94", @@ -12758,8 +12758,8 @@ "databricks": "b66ef401bfc62ea5f67697f0", "duckdb": "b66ef401bfc62ea5f67697f0", "generic": "b66ef401bfc62ea5f67697f0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b66ef401bfc62ea5f67697f0", +"motherduck": "b66ef401bfc62ea5f67697f0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b66ef401bfc62ea5f67697f0", "postgresql": "b66ef401bfc62ea5f67697f0", "snowflake": "b66ef401bfc62ea5f67697f0", @@ -12775,8 +12775,8 @@ "databricks": "41beda08d4df864cb9dd8402", "duckdb": "8a72102ccfafa4befa09244b", "generic": "41beda08d4df864cb9dd8402", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "41beda08d4df864cb9dd8402", +"motherduck": "8a72102ccfafa4befa09244b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "41beda08d4df864cb9dd8402", "postgresql": "41beda08d4df864cb9dd8402", "snowflake": "bae18d740565e7e0e416c8f3", @@ -12792,8 +12792,8 @@ "databricks": "c4346d0257336219f1d26890", "duckdb": "c4346d0257336219f1d26890", "generic": "c4346d0257336219f1d26890", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c4346d0257336219f1d26890", +"motherduck": "c4346d0257336219f1d26890", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "19bdd92ea0cc5fb028657510", "postgresql": "19bdd92ea0cc5fb028657510", "snowflake": "4e24a80b20b9dee0cbbfac17", @@ -12809,8 +12809,8 @@ "databricks": "e57ac1541fb51b362cd5b470", "duckdb": "e57ac1541fb51b362cd5b470", "generic": "e57ac1541fb51b362cd5b470", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e57ac1541fb51b362cd5b470", +"motherduck": "e57ac1541fb51b362cd5b470", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e57ac1541fb51b362cd5b470", "postgresql": "e57ac1541fb51b362cd5b470", "snowflake": "4b356542044f1267fcfc487a", @@ -12826,8 +12826,8 @@ "databricks": "b137c998bdaa2555d912e8fa", "duckdb": "27f973a6b2da27a79f5c0074", "generic": "b137c998bdaa2555d912e8fa", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b137c998bdaa2555d912e8fa", +"motherduck": "27f973a6b2da27a79f5c0074", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b137c998bdaa2555d912e8fa", "postgresql": "b137c998bdaa2555d912e8fa", "snowflake": "b137c998bdaa2555d912e8fa", @@ -12843,8 +12843,8 @@ "databricks": "ab5ad6180652b3b00d1f5b01", "duckdb": "ab5ad6180652b3b00d1f5b01", "generic": "ab5ad6180652b3b00d1f5b01", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ab5ad6180652b3b00d1f5b01", +"motherduck": "ab5ad6180652b3b00d1f5b01", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ab5ad6180652b3b00d1f5b01", "postgresql": "ab5ad6180652b3b00d1f5b01", "snowflake": "f43dac38f34a1c0618a3f812", @@ -12860,8 +12860,8 @@ "databricks": "336fd1b7a1f7d8105c29280c", "duckdb": "0baaca265927140414f32b37", "generic": "336fd1b7a1f7d8105c29280c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "336fd1b7a1f7d8105c29280c", +"motherduck": "0baaca265927140414f32b37", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9ea4932505663b43f527adad", "postgresql": "9ea4932505663b43f527adad", "snowflake": "336fd1b7a1f7d8105c29280c", @@ -12877,8 +12877,8 @@ "databricks": "0960c90223e1eca15361360f", "duckdb": "0960c90223e1eca15361360f", "generic": "0960c90223e1eca15361360f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0960c90223e1eca15361360f", +"motherduck": "0960c90223e1eca15361360f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b72367ce0764c938c620a44c", "postgresql": "b72367ce0764c938c620a44c", "snowflake": "0960c90223e1eca15361360f", @@ -12894,8 +12894,8 @@ "databricks": "b46dfa976d5ba04f95bea32b", "duckdb": "b46dfa976d5ba04f95bea32b", "generic": "b46dfa976d5ba04f95bea32b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b46dfa976d5ba04f95bea32b", +"motherduck": "b46dfa976d5ba04f95bea32b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "34dcdfe3e3afe6140e1353e0", "postgresql": "34dcdfe3e3afe6140e1353e0", "snowflake": "b46dfa976d5ba04f95bea32b", @@ -12911,8 +12911,8 @@ "databricks": "b66ef401bfc62ea5f67697f0", "duckdb": "b66ef401bfc62ea5f67697f0", "generic": "b66ef401bfc62ea5f67697f0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b66ef401bfc62ea5f67697f0", +"motherduck": "b66ef401bfc62ea5f67697f0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b66ef401bfc62ea5f67697f0", "postgresql": "b66ef401bfc62ea5f67697f0", "snowflake": "b66ef401bfc62ea5f67697f0", @@ -12928,8 +12928,8 @@ "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fe23594c781e79cbb0c8927a", +"motherduck": "fe23594c781e79cbb0c8927a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8a3f094f18fb4047f0febf59", "postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", @@ -12945,8 +12945,8 @@ "databricks": "d90a78129f7050a540984f6f", "duckdb": "66f7ca02161349d637fa7d4e", "generic": "66f7ca02161349d637fa7d4e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "66f7ca02161349d637fa7d4e", +"motherduck": "66f7ca02161349d637fa7d4e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "66f7ca02161349d637fa7d4e", "postgresql": "66f7ca02161349d637fa7d4e", "snowflake": "66f7ca02161349d637fa7d4e", @@ -12962,8 +12962,8 @@ "databricks": "8c977bea8b7e2befaacba9a7", "duckdb": "20949d5f5c24adb922f1df86", "generic": "8c977bea8b7e2befaacba9a7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8c977bea8b7e2befaacba9a7", +"motherduck": "20949d5f5c24adb922f1df86", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8c977bea8b7e2befaacba9a7", "postgresql": "8c977bea8b7e2befaacba9a7", "snowflake": "c990ce1e11ec35da455f1547", @@ -12979,8 +12979,8 @@ "databricks": "26e466cea9d3c66c226e9bbb", "duckdb": "26e466cea9d3c66c226e9bbb", "generic": "26e466cea9d3c66c226e9bbb", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "26e466cea9d3c66c226e9bbb", +"motherduck": "26e466cea9d3c66c226e9bbb", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a1fa261c560b019889bf4835", "postgresql": "a1fa261c560b019889bf4835", "snowflake": "26e466cea9d3c66c226e9bbb", @@ -12996,8 +12996,8 @@ "databricks": "ed874ecba919fbd97b7f6780", "duckdb": "31efc7b307bffec917380e8e", "generic": "ed874ecba919fbd97b7f6780", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ed874ecba919fbd97b7f6780", +"motherduck": "31efc7b307bffec917380e8e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "31efc7b307bffec917380e8e", "postgresql": "31efc7b307bffec917380e8e", "snowflake": "42d4a23d9a52e79d0c3458d3", @@ -13013,8 +13013,8 @@ "databricks": "c557f7ce51bc180332354184", "duckdb": "2f9ac8bd6831b78665301f10", "generic": "c557f7ce51bc180332354184", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c557f7ce51bc180332354184", +"motherduck": "2f9ac8bd6831b78665301f10", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2f9ac8bd6831b78665301f10", "postgresql": "2f9ac8bd6831b78665301f10", "snowflake": "2f9ac8bd6831b78665301f10", @@ -13030,8 +13030,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1eb7fb4e8cf75773c5573562", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1eb7fb4e8cf75773c5573562", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -13047,8 +13047,8 @@ "databricks": "7102b9514dfef58f8673a6bf", "duckdb": "7102b9514dfef58f8673a6bf", "generic": "7102b9514dfef58f8673a6bf", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "7102b9514dfef58f8673a6bf", +"motherduck": "7102b9514dfef58f8673a6bf", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7102b9514dfef58f8673a6bf", "postgresql": "7102b9514dfef58f8673a6bf", "snowflake": "7102b9514dfef58f8673a6bf", @@ -13064,8 +13064,8 @@ "databricks": "6d7ab232e65fe8d85c7169ae", "duckdb": "6d7ab232e65fe8d85c7169ae", "generic": "6d7ab232e65fe8d85c7169ae", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6d7ab232e65fe8d85c7169ae", +"motherduck": "6d7ab232e65fe8d85c7169ae", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a6659d96bbfa5da75e69e22d", "postgresql": "a6659d96bbfa5da75e69e22d", "snowflake": "6d7ab232e65fe8d85c7169ae", @@ -13081,8 +13081,8 @@ "databricks": "e7683084f9cefaf3748cf526", "duckdb": "e7683084f9cefaf3748cf526", "generic": "e7683084f9cefaf3748cf526", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e7683084f9cefaf3748cf526", +"motherduck": "e7683084f9cefaf3748cf526", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e7683084f9cefaf3748cf526", "postgresql": "e7683084f9cefaf3748cf526", "snowflake": "e7683084f9cefaf3748cf526", @@ -13098,8 +13098,8 @@ "databricks": "2bfa352725f9d8a91250f75e", "duckdb": "2bfa352725f9d8a91250f75e", "generic": "2bfa352725f9d8a91250f75e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2bfa352725f9d8a91250f75e", +"motherduck": "2bfa352725f9d8a91250f75e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2bfa352725f9d8a91250f75e", "postgresql": "2bfa352725f9d8a91250f75e", "snowflake": "2bfa352725f9d8a91250f75e", @@ -13115,8 +13115,8 @@ "databricks": "50a940812c6ed2e6dd842e74", "duckdb": "50a940812c6ed2e6dd842e74", "generic": "50a940812c6ed2e6dd842e74", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "50a940812c6ed2e6dd842e74", +"motherduck": "50a940812c6ed2e6dd842e74", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "50a940812c6ed2e6dd842e74", "postgresql": "50a940812c6ed2e6dd842e74", "snowflake": "50a940812c6ed2e6dd842e74", @@ -13132,8 +13132,8 @@ "databricks": "e1c74d96ac578f9323bf1bbd", "duckdb": "e1c74d96ac578f9323bf1bbd", "generic": "e1c74d96ac578f9323bf1bbd", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e1c74d96ac578f9323bf1bbd", +"motherduck": "e1c74d96ac578f9323bf1bbd", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e1c74d96ac578f9323bf1bbd", "postgresql": "e1c74d96ac578f9323bf1bbd", "snowflake": "e1c74d96ac578f9323bf1bbd", @@ -13149,8 +13149,8 @@ "databricks": "d6e37cd37419bfb26e09b080", "duckdb": "d6e37cd37419bfb26e09b080", "generic": "d6e37cd37419bfb26e09b080", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d6e37cd37419bfb26e09b080", +"motherduck": "d6e37cd37419bfb26e09b080", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d6e37cd37419bfb26e09b080", "postgresql": "d6e37cd37419bfb26e09b080", "snowflake": "d6e37cd37419bfb26e09b080", @@ -13166,8 +13166,8 @@ "databricks": "de89632e277dbc8c97115419", "duckdb": "5abf4ec799a2ec32fb10da7e", "generic": "de89632e277dbc8c97115419", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "de89632e277dbc8c97115419", +"motherduck": "5abf4ec799a2ec32fb10da7e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5abf4ec799a2ec32fb10da7e", "postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", @@ -13183,8 +13183,8 @@ "databricks": "45809055304b61252670382f", "duckdb": "7a79790250ede37cbc407ce6", "generic": "45809055304b61252670382f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "45809055304b61252670382f", +"motherduck": "7a79790250ede37cbc407ce6", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "45809055304b61252670382f", "postgresql": "45809055304b61252670382f", "snowflake": "45809055304b61252670382f", @@ -13200,8 +13200,8 @@ "databricks": "dcb00b7053ffe69fe3ce99cd", "duckdb": "c7b697bbfa82dd2821ad64e9", "generic": "c7b697bbfa82dd2821ad64e9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c7b697bbfa82dd2821ad64e9", +"motherduck": "c7b697bbfa82dd2821ad64e9", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c7b697bbfa82dd2821ad64e9", "postgresql": "c7b697bbfa82dd2821ad64e9", "snowflake": "c7b697bbfa82dd2821ad64e9", @@ -13217,8 +13217,8 @@ "databricks": "66acfa34471e60551c71ca8a", "duckdb": "66acfa34471e60551c71ca8a", "generic": "66acfa34471e60551c71ca8a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "66acfa34471e60551c71ca8a", +"motherduck": "66acfa34471e60551c71ca8a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "66acfa34471e60551c71ca8a", "postgresql": "66acfa34471e60551c71ca8a", "snowflake": "6306b9e88f7b13dbb6a32c00", @@ -13234,8 +13234,8 @@ "databricks": "bca4e355ae48af6503cb15b3", "duckdb": "793afe64018cd40f2abf8cd7", "generic": "793afe64018cd40f2abf8cd7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "793afe64018cd40f2abf8cd7", +"motherduck": "793afe64018cd40f2abf8cd7", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "793afe64018cd40f2abf8cd7", "postgresql": "793afe64018cd40f2abf8cd7", "snowflake": "c0351067a884fbee21c680b3", @@ -13251,8 +13251,8 @@ "databricks": "0c40d2d16c130d68a7c6c41d", "duckdb": "0c40d2d16c130d68a7c6c41d", "generic": "0c40d2d16c130d68a7c6c41d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0c40d2d16c130d68a7c6c41d", +"motherduck": "0c40d2d16c130d68a7c6c41d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0c40d2d16c130d68a7c6c41d", "postgresql": "0c40d2d16c130d68a7c6c41d", "snowflake": "0c40d2d16c130d68a7c6c41d", @@ -13268,8 +13268,8 @@ "databricks": "18db5afe2c1e80d1ee652b54", "duckdb": "18db5afe2c1e80d1ee652b54", "generic": "18db5afe2c1e80d1ee652b54", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "18db5afe2c1e80d1ee652b54", +"motherduck": "18db5afe2c1e80d1ee652b54", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "47d64a5994d42a2b577166ea", "postgresql": "47d64a5994d42a2b577166ea", "snowflake": "18db5afe2c1e80d1ee652b54", @@ -13285,8 +13285,8 @@ "databricks": "f73576af0cd50cbe498a0092", "duckdb": "f73576af0cd50cbe498a0092", "generic": "f73576af0cd50cbe498a0092", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f73576af0cd50cbe498a0092", +"motherduck": "f73576af0cd50cbe498a0092", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f73576af0cd50cbe498a0092", "postgresql": "f73576af0cd50cbe498a0092", "snowflake": "f73576af0cd50cbe498a0092", @@ -13302,8 +13302,8 @@ "databricks": "3f71ab5f3c1c040da96f2cec", "duckdb": "3f71ab5f3c1c040da96f2cec", "generic": "3f71ab5f3c1c040da96f2cec", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3f71ab5f3c1c040da96f2cec", +"motherduck": "3f71ab5f3c1c040da96f2cec", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3f71ab5f3c1c040da96f2cec", "postgresql": "3f71ab5f3c1c040da96f2cec", "snowflake": "3f71ab5f3c1c040da96f2cec", @@ -13319,8 +13319,8 @@ "databricks": "83fdbd1f0a6ee4e72faa4d94", "duckdb": "83fdbd1f0a6ee4e72faa4d94", "generic": "83fdbd1f0a6ee4e72faa4d94", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "83fdbd1f0a6ee4e72faa4d94", +"motherduck": "83fdbd1f0a6ee4e72faa4d94", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "83fdbd1f0a6ee4e72faa4d94", "postgresql": "83fdbd1f0a6ee4e72faa4d94", "snowflake": "83fdbd1f0a6ee4e72faa4d94", @@ -13336,8 +13336,8 @@ "databricks": "e3bc1b8bad22d8104417b00f", "duckdb": "355e032cc5c0f24695836b3d", "generic": "355e032cc5c0f24695836b3d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "355e032cc5c0f24695836b3d", +"motherduck": "355e032cc5c0f24695836b3d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b0b09c4a6f7570ece56f59ce", "postgresql": "b0b09c4a6f7570ece56f59ce", "snowflake": "355e032cc5c0f24695836b3d", @@ -13353,8 +13353,8 @@ "databricks": "a40042061a75dff39acf27ea", "duckdb": "a40042061a75dff39acf27ea", "generic": "a40042061a75dff39acf27ea", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a40042061a75dff39acf27ea", +"motherduck": "a40042061a75dff39acf27ea", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a40042061a75dff39acf27ea", "postgresql": "a40042061a75dff39acf27ea", "snowflake": "a40042061a75dff39acf27ea", @@ -13370,8 +13370,8 @@ "databricks": "e46d4be44ab9e500e9190d26", "duckdb": "e46d4be44ab9e500e9190d26", "generic": "e46d4be44ab9e500e9190d26", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e46d4be44ab9e500e9190d26", +"motherduck": "e46d4be44ab9e500e9190d26", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "251aba3fef503ce64556c701", "postgresql": "251aba3fef503ce64556c701", "snowflake": "e46d4be44ab9e500e9190d26", @@ -13387,8 +13387,8 @@ "databricks": "d0c95c7a10940244afe0eb6c", "duckdb": "d0c95c7a10940244afe0eb6c", "generic": "d0c95c7a10940244afe0eb6c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d0c95c7a10940244afe0eb6c", +"motherduck": "d0c95c7a10940244afe0eb6c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d0c95c7a10940244afe0eb6c", "postgresql": "d0c95c7a10940244afe0eb6c", "snowflake": "d0c95c7a10940244afe0eb6c", @@ -13404,8 +13404,8 @@ "databricks": "5c28019dce48a74708c90a76", "duckdb": "5c28019dce48a74708c90a76", "generic": "5c28019dce48a74708c90a76", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5c28019dce48a74708c90a76", +"motherduck": "5c28019dce48a74708c90a76", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5c28019dce48a74708c90a76", "postgresql": "5c28019dce48a74708c90a76", "snowflake": "a5656d70e208254d1d3e9b4f", @@ -13421,8 +13421,8 @@ "databricks": "2e4ac90e0dc84f220169a428", "duckdb": "2e4ac90e0dc84f220169a428", "generic": "2e4ac90e0dc84f220169a428", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2e4ac90e0dc84f220169a428", +"motherduck": "2e4ac90e0dc84f220169a428", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2e4ac90e0dc84f220169a428", "postgresql": "2e4ac90e0dc84f220169a428", "snowflake": "2e4ac90e0dc84f220169a428", @@ -13438,8 +13438,8 @@ "databricks": "f94b458b28b4e92e314740bf", "duckdb": "f94b458b28b4e92e314740bf", "generic": "f94b458b28b4e92e314740bf", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f94b458b28b4e92e314740bf", +"motherduck": "f94b458b28b4e92e314740bf", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f94b458b28b4e92e314740bf", "postgresql": "f94b458b28b4e92e314740bf", "snowflake": "f94b458b28b4e92e314740bf", @@ -13455,8 +13455,8 @@ "databricks": "2705f19942a4194a72576911", "duckdb": "2705f19942a4194a72576911", "generic": "2705f19942a4194a72576911", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2705f19942a4194a72576911", +"motherduck": "2705f19942a4194a72576911", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5226d7e2a62c7650a87d6e84", "postgresql": "5226d7e2a62c7650a87d6e84", "snowflake": "2705f19942a4194a72576911", @@ -13472,8 +13472,8 @@ "databricks": "42a76d5ff05d0f91911ff368", "duckdb": "42a76d5ff05d0f91911ff368", "generic": "42a76d5ff05d0f91911ff368", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "42a76d5ff05d0f91911ff368", +"motherduck": "42a76d5ff05d0f91911ff368", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "42a76d5ff05d0f91911ff368", "postgresql": "42a76d5ff05d0f91911ff368", "snowflake": "42a76d5ff05d0f91911ff368", @@ -13489,8 +13489,8 @@ "databricks": "de89632e277dbc8c97115419", "duckdb": "de89632e277dbc8c97115419", "generic": "de89632e277dbc8c97115419", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "de89632e277dbc8c97115419", +"motherduck": "de89632e277dbc8c97115419", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5abf4ec799a2ec32fb10da7e", "postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", @@ -13506,8 +13506,8 @@ "databricks": "f94b458b28b4e92e314740bf", "duckdb": "f94b458b28b4e92e314740bf", "generic": "f94b458b28b4e92e314740bf", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f94b458b28b4e92e314740bf", +"motherduck": "f94b458b28b4e92e314740bf", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f94b458b28b4e92e314740bf", "postgresql": "f94b458b28b4e92e314740bf", "snowflake": "f94b458b28b4e92e314740bf", @@ -13523,8 +13523,8 @@ "databricks": "19a1a80ffc06169f32694de8", "duckdb": "19a1a80ffc06169f32694de8", "generic": "19a1a80ffc06169f32694de8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "19a1a80ffc06169f32694de8", +"motherduck": "19a1a80ffc06169f32694de8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ab1f5f46e69284778bd66d58", "postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", @@ -13540,8 +13540,8 @@ "databricks": "7c11e0be7630905d93bd9ce1", "duckdb": "7c11e0be7630905d93bd9ce1", "generic": "7c11e0be7630905d93bd9ce1", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "7c11e0be7630905d93bd9ce1", +"motherduck": "7c11e0be7630905d93bd9ce1", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7c11e0be7630905d93bd9ce1", "postgresql": "7c11e0be7630905d93bd9ce1", "snowflake": "7c11e0be7630905d93bd9ce1", @@ -13557,8 +13557,8 @@ "databricks": "ebc821a95b9286e22c2b26d0", "duckdb": "ebc821a95b9286e22c2b26d0", "generic": "ebc821a95b9286e22c2b26d0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ebc821a95b9286e22c2b26d0", +"motherduck": "ebc821a95b9286e22c2b26d0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ebc821a95b9286e22c2b26d0", "postgresql": "ebc821a95b9286e22c2b26d0", "snowflake": "ebc821a95b9286e22c2b26d0", @@ -13574,8 +13574,8 @@ "databricks": "181c436b3522e70edf3adf58", "duckdb": "181c436b3522e70edf3adf58", "generic": "181c436b3522e70edf3adf58", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "181c436b3522e70edf3adf58", +"motherduck": "181c436b3522e70edf3adf58", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7560dc4d0678d613b6142654", "postgresql": "7560dc4d0678d613b6142654", "snowflake": "181c436b3522e70edf3adf58", @@ -13591,8 +13591,8 @@ "databricks": "2bd744f24899a7b211c526d9", "duckdb": "2bd744f24899a7b211c526d9", "generic": "2bd744f24899a7b211c526d9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2bd744f24899a7b211c526d9", +"motherduck": "2bd744f24899a7b211c526d9", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2bd744f24899a7b211c526d9", "postgresql": "2bd744f24899a7b211c526d9", "snowflake": "2bd744f24899a7b211c526d9", @@ -13608,8 +13608,8 @@ "databricks": "b60a7259ca26ea325ba090a1", "duckdb": "b60a7259ca26ea325ba090a1", "generic": "b60a7259ca26ea325ba090a1", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b60a7259ca26ea325ba090a1", +"motherduck": "b60a7259ca26ea325ba090a1", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b60a7259ca26ea325ba090a1", "postgresql": "b60a7259ca26ea325ba090a1", "snowflake": "b60a7259ca26ea325ba090a1", @@ -13625,8 +13625,8 @@ "databricks": "79278352c17c9de0f5a28af5", "duckdb": "79278352c17c9de0f5a28af5", "generic": "79278352c17c9de0f5a28af5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "79278352c17c9de0f5a28af5", +"motherduck": "79278352c17c9de0f5a28af5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "79278352c17c9de0f5a28af5", "postgresql": "79278352c17c9de0f5a28af5", "snowflake": "97a20344d811fb77c7690863", @@ -13642,8 +13642,8 @@ "databricks": "285056becb5d467811be7cd6", "duckdb": "0712c33a016f7dd6a37ee011", "generic": "285056becb5d467811be7cd6", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "285056becb5d467811be7cd6", +"motherduck": "0712c33a016f7dd6a37ee011", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "285056becb5d467811be7cd6", "postgresql": "285056becb5d467811be7cd6", "snowflake": "0712c33a016f7dd6a37ee011", @@ -13659,8 +13659,8 @@ "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fe23594c781e79cbb0c8927a", +"motherduck": "fe23594c781e79cbb0c8927a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8a3f094f18fb4047f0febf59", "postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", @@ -13676,8 +13676,8 @@ "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fe23594c781e79cbb0c8927a", +"motherduck": "fe23594c781e79cbb0c8927a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8a3f094f18fb4047f0febf59", "postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", @@ -13693,8 +13693,8 @@ "databricks": "62368fc15cefd5807807b209", "duckdb": "a3b6b25265be330ec52a3322", "generic": "62368fc15cefd5807807b209", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "62368fc15cefd5807807b209", +"motherduck": "a3b6b25265be330ec52a3322", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "62368fc15cefd5807807b209", "postgresql": "62368fc15cefd5807807b209", "snowflake": "919b7ec6a7b49d8527b784cd", @@ -13710,8 +13710,8 @@ "databricks": "4d0f0db1a550ed1e0da1a043", "duckdb": "4d0f0db1a550ed1e0da1a043", "generic": "4d0f0db1a550ed1e0da1a043", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4d0f0db1a550ed1e0da1a043", +"motherduck": "4d0f0db1a550ed1e0da1a043", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e47320d1030c166a05193bc4", "postgresql": "e47320d1030c166a05193bc4", "snowflake": "4d0f0db1a550ed1e0da1a043", @@ -13727,8 +13727,8 @@ "databricks": "fa78cfa46a78783d069e2fc5", "duckdb": "6f4372a332076048e56d5076", "generic": "fa78cfa46a78783d069e2fc5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fa78cfa46a78783d069e2fc5", +"motherduck": "6f4372a332076048e56d5076", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "6f4372a332076048e56d5076", "postgresql": "6f4372a332076048e56d5076", "snowflake": "daeeb0f00b3ee4211b8da00f", @@ -13744,8 +13744,8 @@ "databricks": "62368fc15cefd5807807b209", "duckdb": "62368fc15cefd5807807b209", "generic": "62368fc15cefd5807807b209", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "62368fc15cefd5807807b209", +"motherduck": "62368fc15cefd5807807b209", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "62368fc15cefd5807807b209", "postgresql": "62368fc15cefd5807807b209", "snowflake": "62368fc15cefd5807807b209", @@ -13761,8 +13761,8 @@ "databricks": "e8b6e0bd2ded7b9eec6209e4", "duckdb": "e8b6e0bd2ded7b9eec6209e4", "generic": "e8b6e0bd2ded7b9eec6209e4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e8b6e0bd2ded7b9eec6209e4", +"motherduck": "e8b6e0bd2ded7b9eec6209e4", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e8b6e0bd2ded7b9eec6209e4", "postgresql": "e8b6e0bd2ded7b9eec6209e4", "snowflake": "e8b6e0bd2ded7b9eec6209e4", @@ -13778,8 +13778,8 @@ "databricks": "2e8f6dfec4597fb9373329e7", "duckdb": "087a9417cb511ff7bcfd6ccb", "generic": "2e8f6dfec4597fb9373329e7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2e8f6dfec4597fb9373329e7", +"motherduck": "087a9417cb511ff7bcfd6ccb", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "383f8922f4b6f167d384037d", "postgresql": "383f8922f4b6f167d384037d", "snowflake": "383f8922f4b6f167d384037d", @@ -13795,8 +13795,8 @@ "databricks": "e08a957777bd278f921ca0ee", "duckdb": "e08a957777bd278f921ca0ee", "generic": "e08a957777bd278f921ca0ee", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e08a957777bd278f921ca0ee", +"motherduck": "e08a957777bd278f921ca0ee", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e08a957777bd278f921ca0ee", "postgresql": "e08a957777bd278f921ca0ee", "snowflake": "e08a957777bd278f921ca0ee", @@ -13812,8 +13812,8 @@ "databricks": "d79c3ac70093cb4cb531e71c", "duckdb": "d79c3ac70093cb4cb531e71c", "generic": "d79c3ac70093cb4cb531e71c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d79c3ac70093cb4cb531e71c", +"motherduck": "d79c3ac70093cb4cb531e71c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d79c3ac70093cb4cb531e71c", "postgresql": "d79c3ac70093cb4cb531e71c", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -13829,8 +13829,8 @@ "databricks": "688179272da645629077e980", "duckdb": "688179272da645629077e980", "generic": "688179272da645629077e980", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "688179272da645629077e980", +"motherduck": "688179272da645629077e980", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "688179272da645629077e980", "postgresql": "688179272da645629077e980", "snowflake": "b210f8eb26c37b422f786c6b", @@ -13846,8 +13846,8 @@ "databricks": "e08a957777bd278f921ca0ee", "duckdb": "e08a957777bd278f921ca0ee", "generic": "e08a957777bd278f921ca0ee", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e08a957777bd278f921ca0ee", +"motherduck": "e08a957777bd278f921ca0ee", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e08a957777bd278f921ca0ee", "postgresql": "e08a957777bd278f921ca0ee", "snowflake": "e08a957777bd278f921ca0ee", @@ -13863,8 +13863,8 @@ "databricks": "3533f91995bca270f191503d", "duckdb": "3533f91995bca270f191503d", "generic": "3533f91995bca270f191503d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3533f91995bca270f191503d", +"motherduck": "3533f91995bca270f191503d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3533f91995bca270f191503d", "postgresql": "3533f91995bca270f191503d", "snowflake": "3533f91995bca270f191503d", @@ -13880,8 +13880,8 @@ "databricks": "c2700ca3573352821e09af03", "duckdb": "c2700ca3573352821e09af03", "generic": "c2700ca3573352821e09af03", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c2700ca3573352821e09af03", +"motherduck": "c2700ca3573352821e09af03", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c2700ca3573352821e09af03", "postgresql": "c2700ca3573352821e09af03", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -13897,8 +13897,8 @@ "databricks": "eea9a25a6fbf8e049a445513", "duckdb": "eea9a25a6fbf8e049a445513", "generic": "eea9a25a6fbf8e049a445513", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "eea9a25a6fbf8e049a445513", +"motherduck": "eea9a25a6fbf8e049a445513", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "769f762f4c7cffb2990ddacb", "postgresql": "769f762f4c7cffb2990ddacb", "snowflake": "eea9a25a6fbf8e049a445513", @@ -13914,8 +13914,8 @@ "databricks": "251aba3fef503ce64556c701", "duckdb": "251aba3fef503ce64556c701", "generic": "251aba3fef503ce64556c701", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "251aba3fef503ce64556c701", +"motherduck": "251aba3fef503ce64556c701", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b7d6fc051a3c1ea6deb29ff4", "postgresql": "b7d6fc051a3c1ea6deb29ff4", "snowflake": "251aba3fef503ce64556c701", @@ -13931,8 +13931,8 @@ "databricks": "1a804f317a3e9a0016925b04", "duckdb": "110327dc45eac0e8d54a96dc", "generic": "110327dc45eac0e8d54a96dc", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "110327dc45eac0e8d54a96dc", +"motherduck": "110327dc45eac0e8d54a96dc", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "110327dc45eac0e8d54a96dc", "postgresql": "110327dc45eac0e8d54a96dc", "snowflake": "110327dc45eac0e8d54a96dc", @@ -13948,8 +13948,8 @@ "databricks": "d6e37cd37419bfb26e09b080", "duckdb": "d6e37cd37419bfb26e09b080", "generic": "d6e37cd37419bfb26e09b080", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d6e37cd37419bfb26e09b080", +"motherduck": "d6e37cd37419bfb26e09b080", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d6e37cd37419bfb26e09b080", "postgresql": "d6e37cd37419bfb26e09b080", "snowflake": "d6e37cd37419bfb26e09b080", @@ -13965,8 +13965,8 @@ "databricks": "e46d4be44ab9e500e9190d26", "duckdb": "e46d4be44ab9e500e9190d26", "generic": "e46d4be44ab9e500e9190d26", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e46d4be44ab9e500e9190d26", +"motherduck": "e46d4be44ab9e500e9190d26", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "251aba3fef503ce64556c701", "postgresql": "251aba3fef503ce64556c701", "snowflake": "e46d4be44ab9e500e9190d26", @@ -13982,8 +13982,8 @@ "databricks": "0703bf71a54942bdaa28406e", "duckdb": "0703bf71a54942bdaa28406e", "generic": "0703bf71a54942bdaa28406e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0703bf71a54942bdaa28406e", +"motherduck": "0703bf71a54942bdaa28406e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0703bf71a54942bdaa28406e", "postgresql": "0703bf71a54942bdaa28406e", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -13999,8 +13999,8 @@ "databricks": "319a9fbb02ed503eb114164b", "duckdb": "e060b5a5a8e9774b580d58d3", "generic": "e060b5a5a8e9774b580d58d3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e060b5a5a8e9774b580d58d3", +"motherduck": "e060b5a5a8e9774b580d58d3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e060b5a5a8e9774b580d58d3", "postgresql": "e060b5a5a8e9774b580d58d3", "snowflake": "e060b5a5a8e9774b580d58d3", @@ -14016,8 +14016,8 @@ "databricks": "c6654395c62d21b415299f72", "duckdb": "0712c33a016f7dd6a37ee011", "generic": "c6654395c62d21b415299f72", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c6654395c62d21b415299f72", +"motherduck": "0712c33a016f7dd6a37ee011", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c6654395c62d21b415299f72", "postgresql": "c6654395c62d21b415299f72", "snowflake": "0712c33a016f7dd6a37ee011", @@ -14033,8 +14033,8 @@ "databricks": "71ad5cceb4fd8e86446cf461", "duckdb": "b0fba0f56f0cfe71621226b5", "generic": "71ad5cceb4fd8e86446cf461", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "71ad5cceb4fd8e86446cf461", +"motherduck": "b0fba0f56f0cfe71621226b5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "71ad5cceb4fd8e86446cf461", "postgresql": "71ad5cceb4fd8e86446cf461", "snowflake": "ffdce71ffba2fc524a0e9de5", @@ -14050,8 +14050,8 @@ "databricks": "d227b99a82e505d599c2ed8a", "duckdb": "35a9c5c1419bff507cf65ae2", "generic": "d227b99a82e505d599c2ed8a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d227b99a82e505d599c2ed8a", +"motherduck": "35a9c5c1419bff507cf65ae2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "35a9c5c1419bff507cf65ae2", "postgresql": "35a9c5c1419bff507cf65ae2", "snowflake": "d227b99a82e505d599c2ed8a", @@ -14067,8 +14067,8 @@ "databricks": "c2700ca3573352821e09af03", "duckdb": "c2700ca3573352821e09af03", "generic": "c2700ca3573352821e09af03", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c2700ca3573352821e09af03", +"motherduck": "c2700ca3573352821e09af03", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c2700ca3573352821e09af03", "postgresql": "c2700ca3573352821e09af03", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -14084,8 +14084,8 @@ "databricks": "3915f3f73328f0838497b2d5", "duckdb": "3915f3f73328f0838497b2d5", "generic": "3915f3f73328f0838497b2d5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3915f3f73328f0838497b2d5", +"motherduck": "3915f3f73328f0838497b2d5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3915f3f73328f0838497b2d5", "postgresql": "3915f3f73328f0838497b2d5", "snowflake": "f43dac38f34a1c0618a3f812", @@ -14101,8 +14101,8 @@ "databricks": "0703bf71a54942bdaa28406e", "duckdb": "0703bf71a54942bdaa28406e", "generic": "0703bf71a54942bdaa28406e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0703bf71a54942bdaa28406e", +"motherduck": "0703bf71a54942bdaa28406e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0703bf71a54942bdaa28406e", "postgresql": "0703bf71a54942bdaa28406e", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -14118,8 +14118,8 @@ "databricks": "b55b02912e30e27dcb40ad93", "duckdb": "b55b02912e30e27dcb40ad93", "generic": "b55b02912e30e27dcb40ad93", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b55b02912e30e27dcb40ad93", +"motherduck": "b55b02912e30e27dcb40ad93", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b55b02912e30e27dcb40ad93", "postgresql": "b55b02912e30e27dcb40ad93", "snowflake": "b55b02912e30e27dcb40ad93", @@ -14135,8 +14135,8 @@ "databricks": "3688fe25b8bdff7eb938e6f9", "duckdb": "e8cd7a4caa15ec2b6f990a1f", "generic": "1757eadf86c2a8956b520868", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "e8cd7a4caa15ec2b6f990a1f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e8cd7a4caa15ec2b6f990a1f", "postgresql": "e8cd7a4caa15ec2b6f990a1f", "snowflake": "fe491937bc4ec7b061f2be43", @@ -14152,8 +14152,8 @@ "databricks": "c6106f30f23f392e17eadef4", "duckdb": "c6106f30f23f392e17eadef4", "generic": "c6106f30f23f392e17eadef4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c6106f30f23f392e17eadef4", +"motherduck": "c6106f30f23f392e17eadef4", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f43dac38f34a1c0618a3f812", "postgresql": "f43dac38f34a1c0618a3f812", "snowflake": "c6106f30f23f392e17eadef4", @@ -14169,8 +14169,8 @@ "databricks": "eb9c0f675dd300eb82500b21", "duckdb": "eb9c0f675dd300eb82500b21", "generic": "eb9c0f675dd300eb82500b21", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "eb9c0f675dd300eb82500b21", +"motherduck": "eb9c0f675dd300eb82500b21", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "eb9c0f675dd300eb82500b21", "postgresql": "eb9c0f675dd300eb82500b21", "snowflake": "4e24a80b20b9dee0cbbfac17", @@ -14186,8 +14186,8 @@ "databricks": "3eb44072dafa3bef75c97b6a", "duckdb": "3eb44072dafa3bef75c97b6a", "generic": "3eb44072dafa3bef75c97b6a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3eb44072dafa3bef75c97b6a", +"motherduck": "3eb44072dafa3bef75c97b6a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3eb44072dafa3bef75c97b6a", "postgresql": "3eb44072dafa3bef75c97b6a", "snowflake": "3eb44072dafa3bef75c97b6a", @@ -14203,8 +14203,8 @@ "databricks": "461923a0d34defd0c726d6e5", "duckdb": "461923a0d34defd0c726d6e5", "generic": "461923a0d34defd0c726d6e5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "461923a0d34defd0c726d6e5", +"motherduck": "461923a0d34defd0c726d6e5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "461923a0d34defd0c726d6e5", "postgresql": "461923a0d34defd0c726d6e5", "snowflake": "461923a0d34defd0c726d6e5", @@ -14220,8 +14220,8 @@ "databricks": "3f998a9ab68d8b2514840489", "duckdb": "f2ed52f6bf46b4db9343551c", "generic": "3f998a9ab68d8b2514840489", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3f998a9ab68d8b2514840489", +"motherduck": "f2ed52f6bf46b4db9343551c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3f998a9ab68d8b2514840489", "postgresql": "3f998a9ab68d8b2514840489", "snowflake": "f2ed52f6bf46b4db9343551c", @@ -14237,8 +14237,8 @@ "databricks": "0960c90223e1eca15361360f", "duckdb": "0960c90223e1eca15361360f", "generic": "0960c90223e1eca15361360f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0960c90223e1eca15361360f", +"motherduck": "0960c90223e1eca15361360f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b72367ce0764c938c620a44c", "postgresql": "b72367ce0764c938c620a44c", "snowflake": "0960c90223e1eca15361360f", @@ -14254,8 +14254,8 @@ "databricks": "e2879cf4770e7f48d2dd704a", "duckdb": "e2879cf4770e7f48d2dd704a", "generic": "e2879cf4770e7f48d2dd704a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e2879cf4770e7f48d2dd704a", +"motherduck": "e2879cf4770e7f48d2dd704a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2d4aea8dec5521e258596f3d", "postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", @@ -14271,8 +14271,8 @@ "databricks": "11d8d18c76077fddaf1b264f", "duckdb": "11d8d18c76077fddaf1b264f", "generic": "11d8d18c76077fddaf1b264f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "11d8d18c76077fddaf1b264f", +"motherduck": "11d8d18c76077fddaf1b264f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "11d8d18c76077fddaf1b264f", "postgresql": "11d8d18c76077fddaf1b264f", "snowflake": "11d8d18c76077fddaf1b264f", @@ -14288,8 +14288,8 @@ "databricks": "0b9d9879e289ea9a6a3e3e70", "duckdb": "0b9d9879e289ea9a6a3e3e70", "generic": "0b9d9879e289ea9a6a3e3e70", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0b9d9879e289ea9a6a3e3e70", +"motherduck": "0b9d9879e289ea9a6a3e3e70", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0b9d9879e289ea9a6a3e3e70", "postgresql": "0b9d9879e289ea9a6a3e3e70", "snowflake": "0b9d9879e289ea9a6a3e3e70", @@ -14305,8 +14305,8 @@ "databricks": "8e484ef176389314b9fd318e", "duckdb": "8e484ef176389314b9fd318e", "generic": "8e484ef176389314b9fd318e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8e484ef176389314b9fd318e", +"motherduck": "8e484ef176389314b9fd318e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8e484ef176389314b9fd318e", "postgresql": "8e484ef176389314b9fd318e", "snowflake": "8e484ef176389314b9fd318e", @@ -14322,8 +14322,8 @@ "databricks": "41a317d0625259b54aa1dc94", "duckdb": "41a317d0625259b54aa1dc94", "generic": "41a317d0625259b54aa1dc94", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "41a317d0625259b54aa1dc94", +"motherduck": "41a317d0625259b54aa1dc94", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "41a317d0625259b54aa1dc94", "postgresql": "41a317d0625259b54aa1dc94", "snowflake": "41a317d0625259b54aa1dc94", @@ -14339,8 +14339,8 @@ "databricks": "72d1587b51c517b53f52a2d8", "duckdb": "f01435c3d6e460ffd86479dc", "generic": "f01435c3d6e460ffd86479dc", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f01435c3d6e460ffd86479dc", +"motherduck": "f01435c3d6e460ffd86479dc", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f01435c3d6e460ffd86479dc", "postgresql": "f01435c3d6e460ffd86479dc", "snowflake": "143dfed5733eda8a7f6ed075", @@ -14356,8 +14356,8 @@ "databricks": "5d25d5e13f81700741695ed4", "duckdb": "5d25d5e13f81700741695ed4", "generic": "5d25d5e13f81700741695ed4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5d25d5e13f81700741695ed4", +"motherduck": "5d25d5e13f81700741695ed4", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "62131fb5b065f5de2e74a0f1", "postgresql": "62131fb5b065f5de2e74a0f1", "snowflake": "5d25d5e13f81700741695ed4", @@ -14373,8 +14373,8 @@ "databricks": "ea6e350a4a788066ff386c50", "duckdb": "3e2cff9b165a6eaf1c043756", "generic": "ea6e350a4a788066ff386c50", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ea6e350a4a788066ff386c50", +"motherduck": "3e2cff9b165a6eaf1c043756", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ea6e350a4a788066ff386c50", "postgresql": "ea6e350a4a788066ff386c50", "snowflake": "2a7a3265ae4026b435644d0e", @@ -14390,8 +14390,8 @@ "databricks": "adbb06204fd66d8071c3dbc6", "duckdb": "29739921db4d47abc1ac18c7", "generic": "29739921db4d47abc1ac18c7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "29739921db4d47abc1ac18c7", +"motherduck": "29739921db4d47abc1ac18c7", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "29739921db4d47abc1ac18c7", "postgresql": "29739921db4d47abc1ac18c7", "snowflake": "216a7f0ead009a087eea4e1c", @@ -14407,8 +14407,8 @@ "databricks": "3f71ab5f3c1c040da96f2cec", "duckdb": "3f71ab5f3c1c040da96f2cec", "generic": "3f71ab5f3c1c040da96f2cec", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3f71ab5f3c1c040da96f2cec", +"motherduck": "3f71ab5f3c1c040da96f2cec", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3f71ab5f3c1c040da96f2cec", "postgresql": "3f71ab5f3c1c040da96f2cec", "snowflake": "3f71ab5f3c1c040da96f2cec", @@ -14424,8 +14424,8 @@ "databricks": "f15c56475abfc73488abe63b", "duckdb": "f15c56475abfc73488abe63b", "generic": "f15c56475abfc73488abe63b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f15c56475abfc73488abe63b", +"motherduck": "f15c56475abfc73488abe63b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f15c56475abfc73488abe63b", "postgresql": "f15c56475abfc73488abe63b", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -14441,8 +14441,8 @@ "databricks": "669084d1ea704ae5b8ee05c2", "duckdb": "669084d1ea704ae5b8ee05c2", "generic": "669084d1ea704ae5b8ee05c2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "669084d1ea704ae5b8ee05c2", +"motherduck": "669084d1ea704ae5b8ee05c2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "93c1b5b5960e26479baf88d0", "postgresql": "93c1b5b5960e26479baf88d0", "snowflake": "669084d1ea704ae5b8ee05c2", @@ -14458,8 +14458,8 @@ "databricks": "38b2ec94bf02be40007ce525", "duckdb": "38b2ec94bf02be40007ce525", "generic": "38b2ec94bf02be40007ce525", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "38b2ec94bf02be40007ce525", +"motherduck": "38b2ec94bf02be40007ce525", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "850deea990b52b82b36a9848", "postgresql": "850deea990b52b82b36a9848", "snowflake": "38b2ec94bf02be40007ce525", @@ -14475,8 +14475,8 @@ "databricks": "61058ba40f23266ededb040f", "duckdb": "53ea41ce10b9fa682ddf305d", "generic": "a1176e5228f8200c23e7a0a7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "53ea41ce10b9fa682ddf305d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a09c2bcd944462f73dd87c2d", "postgresql": "a09c2bcd944462f73dd87c2d", "snowflake": "9553636ef19ebb7706dabb5f", @@ -14492,8 +14492,8 @@ "databricks": "66078c43faec49916ed11546", "duckdb": "66078c43faec49916ed11546", "generic": "66078c43faec49916ed11546", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "66078c43faec49916ed11546", +"motherduck": "66078c43faec49916ed11546", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "48a21c33337c0a03f7f0c746", "postgresql": "48a21c33337c0a03f7f0c746", "snowflake": "66078c43faec49916ed11546", @@ -14509,8 +14509,8 @@ "databricks": "cea3ac691106cc77e45d9414", "duckdb": "cea3ac691106cc77e45d9414", "generic": "cea3ac691106cc77e45d9414", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "cea3ac691106cc77e45d9414", +"motherduck": "cea3ac691106cc77e45d9414", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "cea3ac691106cc77e45d9414", "postgresql": "cea3ac691106cc77e45d9414", "snowflake": "cea3ac691106cc77e45d9414", @@ -14526,8 +14526,8 @@ "databricks": "aae2b1fe6f2d9807ecde412b", "duckdb": "aae2b1fe6f2d9807ecde412b", "generic": "aae2b1fe6f2d9807ecde412b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "aae2b1fe6f2d9807ecde412b", +"motherduck": "aae2b1fe6f2d9807ecde412b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "aae2b1fe6f2d9807ecde412b", "postgresql": "aae2b1fe6f2d9807ecde412b", "snowflake": "aae2b1fe6f2d9807ecde412b", @@ -14543,8 +14543,8 @@ "databricks": "937a574f550c49d90b6065cc", "duckdb": "937a574f550c49d90b6065cc", "generic": "937a574f550c49d90b6065cc", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "937a574f550c49d90b6065cc", +"motherduck": "937a574f550c49d90b6065cc", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "937a574f550c49d90b6065cc", "postgresql": "937a574f550c49d90b6065cc", "snowflake": "937a574f550c49d90b6065cc", @@ -14560,8 +14560,8 @@ "databricks": "ce156bc49d99761b01d1c550", "duckdb": "ce156bc49d99761b01d1c550", "generic": "ce156bc49d99761b01d1c550", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ce156bc49d99761b01d1c550", +"motherduck": "ce156bc49d99761b01d1c550", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ce156bc49d99761b01d1c550", "postgresql": "ce156bc49d99761b01d1c550", "snowflake": "ce156bc49d99761b01d1c550", @@ -14577,8 +14577,8 @@ "databricks": "2cc8cf0f153f72622d15048c", "duckdb": "2cc8cf0f153f72622d15048c", "generic": "2cc8cf0f153f72622d15048c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2cc8cf0f153f72622d15048c", +"motherduck": "2cc8cf0f153f72622d15048c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2cc8cf0f153f72622d15048c", "postgresql": "2cc8cf0f153f72622d15048c", "snowflake": "2cc8cf0f153f72622d15048c", @@ -14594,8 +14594,8 @@ "databricks": "ba72bc162859c1861cc127e7", "duckdb": "ba72bc162859c1861cc127e7", "generic": "ba72bc162859c1861cc127e7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ba72bc162859c1861cc127e7", +"motherduck": "ba72bc162859c1861cc127e7", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ba72bc162859c1861cc127e7", "postgresql": "ba72bc162859c1861cc127e7", "snowflake": "ba72bc162859c1861cc127e7", @@ -14611,8 +14611,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1eb7fb4e8cf75773c5573562", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1eb7fb4e8cf75773c5573562", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -14628,8 +14628,8 @@ "databricks": "c2a3a3c3789aeea50e8c4f35", "duckdb": "c2a3a3c3789aeea50e8c4f35", "generic": "c2a3a3c3789aeea50e8c4f35", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c2a3a3c3789aeea50e8c4f35", +"motherduck": "c2a3a3c3789aeea50e8c4f35", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c2a3a3c3789aeea50e8c4f35", "postgresql": "c2a3a3c3789aeea50e8c4f35", "snowflake": "c2a3a3c3789aeea50e8c4f35", @@ -14645,8 +14645,8 @@ "databricks": "054898738769701df3abbf2e", "duckdb": "16c4b7f3bdc7c21a00a78838", "generic": "054898738769701df3abbf2e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "054898738769701df3abbf2e", +"motherduck": "16c4b7f3bdc7c21a00a78838", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "16c4b7f3bdc7c21a00a78838", "postgresql": "16c4b7f3bdc7c21a00a78838", "snowflake": "054898738769701df3abbf2e", @@ -14662,8 +14662,8 @@ "databricks": "e3bc1b8bad22d8104417b00f", "duckdb": "b0b09c4a6f7570ece56f59ce", "generic": "b0b09c4a6f7570ece56f59ce", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "355e032cc5c0f24695836b3d", +"motherduck": "b0b09c4a6f7570ece56f59ce", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "111af35848fe500ec2ae54b2", "postgresql": "111af35848fe500ec2ae54b2", "snowflake": "355e032cc5c0f24695836b3d", @@ -14679,8 +14679,8 @@ "databricks": "e8a5e051bb567983c365b3fd", "duckdb": "e8a5e051bb567983c365b3fd", "generic": "e8a5e051bb567983c365b3fd", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e8a5e051bb567983c365b3fd", +"motherduck": "e8a5e051bb567983c365b3fd", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e8a5e051bb567983c365b3fd", "postgresql": "e8a5e051bb567983c365b3fd", "snowflake": "b210f8eb26c37b422f786c6b", @@ -14696,8 +14696,8 @@ "databricks": "207b6dc5f2a2547137393792", "duckdb": "50563fc19d4ed8ceb4f5e8e4", "generic": "50563fc19d4ed8ceb4f5e8e4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "50563fc19d4ed8ceb4f5e8e4", +"motherduck": "50563fc19d4ed8ceb4f5e8e4", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8770742a4fe26efe6bd47a00", "postgresql": "8770742a4fe26efe6bd47a00", "snowflake": "a4cd3eca76c356ecd633fe7b", @@ -14713,8 +14713,8 @@ "databricks": "19a1a80ffc06169f32694de8", "duckdb": "19a1a80ffc06169f32694de8", "generic": "19a1a80ffc06169f32694de8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "19a1a80ffc06169f32694de8", +"motherduck": "19a1a80ffc06169f32694de8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ab1f5f46e69284778bd66d58", "postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", @@ -14730,8 +14730,8 @@ "databricks": "3f71ab5f3c1c040da96f2cec", "duckdb": "3f71ab5f3c1c040da96f2cec", "generic": "3f71ab5f3c1c040da96f2cec", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3f71ab5f3c1c040da96f2cec", +"motherduck": "3f71ab5f3c1c040da96f2cec", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3f71ab5f3c1c040da96f2cec", "postgresql": "3f71ab5f3c1c040da96f2cec", "snowflake": "3f71ab5f3c1c040da96f2cec", @@ -14747,8 +14747,8 @@ "databricks": "77ffdb80029bd6e2cca768ad", "duckdb": "77ffdb80029bd6e2cca768ad", "generic": "77ffdb80029bd6e2cca768ad", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "77ffdb80029bd6e2cca768ad", +"motherduck": "77ffdb80029bd6e2cca768ad", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "77ffdb80029bd6e2cca768ad", "postgresql": "77ffdb80029bd6e2cca768ad", "snowflake": "77ffdb80029bd6e2cca768ad", @@ -14764,8 +14764,8 @@ "databricks": "c37de6584758b2433c289aee", "duckdb": "c37de6584758b2433c289aee", "generic": "c37de6584758b2433c289aee", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c37de6584758b2433c289aee", +"motherduck": "c37de6584758b2433c289aee", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c37de6584758b2433c289aee", "postgresql": "c37de6584758b2433c289aee", "snowflake": "c37de6584758b2433c289aee", @@ -14781,8 +14781,8 @@ "databricks": "555111267818cbd3d0b07105", "duckdb": "555111267818cbd3d0b07105", "generic": "555111267818cbd3d0b07105", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "555111267818cbd3d0b07105", +"motherduck": "555111267818cbd3d0b07105", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "555111267818cbd3d0b07105", "postgresql": "555111267818cbd3d0b07105", "snowflake": "555111267818cbd3d0b07105", @@ -14798,8 +14798,8 @@ "databricks": "2bfa352725f9d8a91250f75e", "duckdb": "2bfa352725f9d8a91250f75e", "generic": "2bfa352725f9d8a91250f75e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2bfa352725f9d8a91250f75e", +"motherduck": "2bfa352725f9d8a91250f75e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2bfa352725f9d8a91250f75e", "postgresql": "2bfa352725f9d8a91250f75e", "snowflake": "2bfa352725f9d8a91250f75e", @@ -14815,8 +14815,8 @@ "databricks": "3c0719ca5c83e0a20d0b0b42", "duckdb": "42d4a23d9a52e79d0c3458d3", "generic": "3c0719ca5c83e0a20d0b0b42", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3c0719ca5c83e0a20d0b0b42", +"motherduck": "42d4a23d9a52e79d0c3458d3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "42d4a23d9a52e79d0c3458d3", "postgresql": "42d4a23d9a52e79d0c3458d3", "snowflake": "a3acd7ba35e81a800f967bd3", @@ -14832,8 +14832,8 @@ "databricks": "aae2b1fe6f2d9807ecde412b", "duckdb": "aae2b1fe6f2d9807ecde412b", "generic": "aae2b1fe6f2d9807ecde412b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "aae2b1fe6f2d9807ecde412b", +"motherduck": "aae2b1fe6f2d9807ecde412b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "aae2b1fe6f2d9807ecde412b", "postgresql": "aae2b1fe6f2d9807ecde412b", "snowflake": "aae2b1fe6f2d9807ecde412b", @@ -14849,8 +14849,8 @@ "databricks": "2bfa352725f9d8a91250f75e", "duckdb": "2bfa352725f9d8a91250f75e", "generic": "2bfa352725f9d8a91250f75e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2bfa352725f9d8a91250f75e", +"motherduck": "2bfa352725f9d8a91250f75e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2bfa352725f9d8a91250f75e", "postgresql": "2bfa352725f9d8a91250f75e", "snowflake": "2bfa352725f9d8a91250f75e", @@ -14866,8 +14866,8 @@ "databricks": "558da38ec4d9d5d6c72f60cf", "duckdb": "919b7ec6a7b49d8527b784cd", "generic": "558da38ec4d9d5d6c72f60cf", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "558da38ec4d9d5d6c72f60cf", +"motherduck": "919b7ec6a7b49d8527b784cd", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "558da38ec4d9d5d6c72f60cf", "postgresql": "558da38ec4d9d5d6c72f60cf", "snowflake": "919b7ec6a7b49d8527b784cd", @@ -14883,8 +14883,8 @@ "databricks": "c9b810639795a7bfab91b98d", "duckdb": "a428302e4848ffe877284c84", "generic": "c9b810639795a7bfab91b98d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c9b810639795a7bfab91b98d", +"motherduck": "a428302e4848ffe877284c84", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a428302e4848ffe877284c84", "postgresql": "a428302e4848ffe877284c84", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -14900,8 +14900,8 @@ "databricks": "5fd024c2193505b62de9c162", "duckdb": "5fd024c2193505b62de9c162", "generic": "5fd024c2193505b62de9c162", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5fd024c2193505b62de9c162", +"motherduck": "5fd024c2193505b62de9c162", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5fd024c2193505b62de9c162", "postgresql": "5fd024c2193505b62de9c162", "snowflake": "5fd024c2193505b62de9c162", @@ -14917,8 +14917,8 @@ "databricks": "4b356542044f1267fcfc487a", "duckdb": "4b356542044f1267fcfc487a", "generic": "4b356542044f1267fcfc487a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4b356542044f1267fcfc487a", +"motherduck": "4b356542044f1267fcfc487a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a5656d70e208254d1d3e9b4f", "postgresql": "a5656d70e208254d1d3e9b4f", "snowflake": "4b356542044f1267fcfc487a", @@ -14934,8 +14934,8 @@ "databricks": "ebc821a95b9286e22c2b26d0", "duckdb": "ebc821a95b9286e22c2b26d0", "generic": "ebc821a95b9286e22c2b26d0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ebc821a95b9286e22c2b26d0", +"motherduck": "ebc821a95b9286e22c2b26d0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ebc821a95b9286e22c2b26d0", "postgresql": "ebc821a95b9286e22c2b26d0", "snowflake": "ebc821a95b9286e22c2b26d0", @@ -14951,8 +14951,8 @@ "databricks": "6bd27be6a47ef34ec1defbad", "duckdb": "b0791f9f270e764eeeb6fb8e", "generic": "c7c38fdeaeb7858a44976f1d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c7c38fdeaeb7858a44976f1d", +"motherduck": "b0791f9f270e764eeeb6fb8e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "4d7604d41e3585ec187dec82", "postgresql": "4d7604d41e3585ec187dec82", "snowflake": "c7c38fdeaeb7858a44976f1d", @@ -14968,8 +14968,8 @@ "databricks": "66bffd006fc7638db82257d2", "duckdb": "66bffd006fc7638db82257d2", "generic": "66bffd006fc7638db82257d2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "66bffd006fc7638db82257d2", +"motherduck": "66bffd006fc7638db82257d2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "66bffd006fc7638db82257d2", "postgresql": "66bffd006fc7638db82257d2", "snowflake": "66bffd006fc7638db82257d2", @@ -14985,8 +14985,8 @@ "databricks": "f43dac38f34a1c0618a3f812", "duckdb": "f43dac38f34a1c0618a3f812", "generic": "f43dac38f34a1c0618a3f812", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f43dac38f34a1c0618a3f812", +"motherduck": "f43dac38f34a1c0618a3f812", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f43dac38f34a1c0618a3f812", "postgresql": "f43dac38f34a1c0618a3f812", "snowflake": "f43dac38f34a1c0618a3f812", @@ -15002,8 +15002,8 @@ "databricks": "5d25d5e13f81700741695ed4", "duckdb": "5d25d5e13f81700741695ed4", "generic": "5d25d5e13f81700741695ed4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5d25d5e13f81700741695ed4", +"motherduck": "5d25d5e13f81700741695ed4", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "62131fb5b065f5de2e74a0f1", "postgresql": "62131fb5b065f5de2e74a0f1", "snowflake": "5d25d5e13f81700741695ed4", @@ -15019,8 +15019,8 @@ "databricks": "52928fec41d9097d054cb63d", "duckdb": "52928fec41d9097d054cb63d", "generic": "52928fec41d9097d054cb63d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "52928fec41d9097d054cb63d", +"motherduck": "52928fec41d9097d054cb63d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "52928fec41d9097d054cb63d", "postgresql": "52928fec41d9097d054cb63d", "snowflake": "52928fec41d9097d054cb63d", @@ -15036,8 +15036,8 @@ "databricks": "b561884132dbe7e42b719110", "duckdb": "b561884132dbe7e42b719110", "generic": "b561884132dbe7e42b719110", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b561884132dbe7e42b719110", +"motherduck": "b561884132dbe7e42b719110", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5aa51d84ac5e6c7a9306f348", "postgresql": "5aa51d84ac5e6c7a9306f348", "snowflake": "b561884132dbe7e42b719110", @@ -15053,8 +15053,8 @@ "databricks": "c6654395c62d21b415299f72", "duckdb": "0712c33a016f7dd6a37ee011", "generic": "c6654395c62d21b415299f72", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c6654395c62d21b415299f72", +"motherduck": "0712c33a016f7dd6a37ee011", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c6654395c62d21b415299f72", "postgresql": "c6654395c62d21b415299f72", "snowflake": "0712c33a016f7dd6a37ee011", @@ -15070,8 +15070,8 @@ "databricks": "2ffee1a4ca7dc4a711fc0d27", "duckdb": "2ffee1a4ca7dc4a711fc0d27", "generic": "2ffee1a4ca7dc4a711fc0d27", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2ffee1a4ca7dc4a711fc0d27", +"motherduck": "2ffee1a4ca7dc4a711fc0d27", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2ffee1a4ca7dc4a711fc0d27", "postgresql": "2ffee1a4ca7dc4a711fc0d27", "snowflake": "2ffee1a4ca7dc4a711fc0d27", @@ -15087,8 +15087,8 @@ "databricks": "c6889f2965db73017be6f6e7", "duckdb": "c6889f2965db73017be6f6e7", "generic": "c6889f2965db73017be6f6e7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c6889f2965db73017be6f6e7", +"motherduck": "c6889f2965db73017be6f6e7", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c6889f2965db73017be6f6e7", "postgresql": "c6889f2965db73017be6f6e7", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -15104,8 +15104,8 @@ "databricks": "de89632e277dbc8c97115419", "duckdb": "5abf4ec799a2ec32fb10da7e", "generic": "de89632e277dbc8c97115419", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "de89632e277dbc8c97115419", +"motherduck": "5abf4ec799a2ec32fb10da7e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5abf4ec799a2ec32fb10da7e", "postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", @@ -15121,8 +15121,8 @@ "databricks": "d61c83ef8bfdf043fa672cb5", "duckdb": "d61c83ef8bfdf043fa672cb5", "generic": "d61c83ef8bfdf043fa672cb5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d61c83ef8bfdf043fa672cb5", +"motherduck": "d61c83ef8bfdf043fa672cb5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d61c83ef8bfdf043fa672cb5", "postgresql": "d61c83ef8bfdf043fa672cb5", "snowflake": "97a20344d811fb77c7690863", @@ -15138,8 +15138,8 @@ "databricks": "9c2cff8be7225cf79883ca65", "duckdb": "9c2cff8be7225cf79883ca65", "generic": "9c2cff8be7225cf79883ca65", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9c2cff8be7225cf79883ca65", +"motherduck": "9c2cff8be7225cf79883ca65", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9c2cff8be7225cf79883ca65", "postgresql": "9c2cff8be7225cf79883ca65", "snowflake": "9c2cff8be7225cf79883ca65", @@ -15155,8 +15155,8 @@ "databricks": "ab62f43260f32e2cf62666fc", "duckdb": "0baaca265927140414f32b37", "generic": "ab62f43260f32e2cf62666fc", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ab62f43260f32e2cf62666fc", +"motherduck": "0baaca265927140414f32b37", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9ea4932505663b43f527adad", "postgresql": "9ea4932505663b43f527adad", "snowflake": "ab62f43260f32e2cf62666fc", @@ -15172,8 +15172,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1eb7fb4e8cf75773c5573562", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1eb7fb4e8cf75773c5573562", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -15189,8 +15189,8 @@ "databricks": "97e5335218efead1a4226b8d", "duckdb": "97e5335218efead1a4226b8d", "generic": "97e5335218efead1a4226b8d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "97e5335218efead1a4226b8d", +"motherduck": "97e5335218efead1a4226b8d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "97e5335218efead1a4226b8d", "postgresql": "97e5335218efead1a4226b8d", "snowflake": "97e5335218efead1a4226b8d", @@ -15206,8 +15206,8 @@ "databricks": "ee47e1424cb01deb0cf51760", "duckdb": "ee47e1424cb01deb0cf51760", "generic": "ee47e1424cb01deb0cf51760", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ee47e1424cb01deb0cf51760", +"motherduck": "ee47e1424cb01deb0cf51760", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3c1521654088dd54d0dc9ead", "postgresql": "3c1521654088dd54d0dc9ead", "snowflake": "ee47e1424cb01deb0cf51760", @@ -15223,8 +15223,8 @@ "databricks": "2d4aea8dec5521e258596f3d", "duckdb": "2d4aea8dec5521e258596f3d", "generic": "2d4aea8dec5521e258596f3d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2d4aea8dec5521e258596f3d", +"motherduck": "2d4aea8dec5521e258596f3d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f15c56475abfc73488abe63b", "postgresql": "f15c56475abfc73488abe63b", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -15240,8 +15240,8 @@ "databricks": "8917b70db141a02ac2e2bb1e", "duckdb": "8917b70db141a02ac2e2bb1e", "generic": "8917b70db141a02ac2e2bb1e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8917b70db141a02ac2e2bb1e", +"motherduck": "8917b70db141a02ac2e2bb1e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8917b70db141a02ac2e2bb1e", "postgresql": "8917b70db141a02ac2e2bb1e", "snowflake": "8917b70db141a02ac2e2bb1e", @@ -15257,8 +15257,8 @@ "databricks": "ef480cafe4d260b70ce5214c", "duckdb": "ef480cafe4d260b70ce5214c", "generic": "ef480cafe4d260b70ce5214c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ef480cafe4d260b70ce5214c", +"motherduck": "ef480cafe4d260b70ce5214c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ef480cafe4d260b70ce5214c", "postgresql": "ef480cafe4d260b70ce5214c", "snowflake": "ef480cafe4d260b70ce5214c", @@ -15274,8 +15274,8 @@ "databricks": "8075663faec01d3abb4fc153", "duckdb": "19bdd92ea0cc5fb028657510", "generic": "8075663faec01d3abb4fc153", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8075663faec01d3abb4fc153", +"motherduck": "19bdd92ea0cc5fb028657510", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8075663faec01d3abb4fc153", "postgresql": "8075663faec01d3abb4fc153", "snowflake": "8ff614b7306c0edbe36ce00b", @@ -15291,8 +15291,8 @@ "databricks": "ee47e1424cb01deb0cf51760", "duckdb": "ee47e1424cb01deb0cf51760", "generic": "ee47e1424cb01deb0cf51760", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ee47e1424cb01deb0cf51760", +"motherduck": "ee47e1424cb01deb0cf51760", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3c1521654088dd54d0dc9ead", "postgresql": "3c1521654088dd54d0dc9ead", "snowflake": "ee47e1424cb01deb0cf51760", @@ -15308,8 +15308,8 @@ "databricks": "aab0389e2b5470136660bf8a", "duckdb": "aab0389e2b5470136660bf8a", "generic": "aab0389e2b5470136660bf8a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "aab0389e2b5470136660bf8a", +"motherduck": "aab0389e2b5470136660bf8a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "aab0389e2b5470136660bf8a", "postgresql": "aab0389e2b5470136660bf8a", "snowflake": "97a20344d811fb77c7690863", @@ -15325,8 +15325,8 @@ "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fe23594c781e79cbb0c8927a", +"motherduck": "fe23594c781e79cbb0c8927a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8a3f094f18fb4047f0febf59", "postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", @@ -15342,8 +15342,8 @@ "databricks": "e3bc1b8bad22d8104417b00f", "duckdb": "61d08c878cc506dd91ca4586", "generic": "61d08c878cc506dd91ca4586", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c100cdbd268164cf084295be", +"motherduck": "61d08c878cc506dd91ca4586", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "111af35848fe500ec2ae54b2", "postgresql": "111af35848fe500ec2ae54b2", "snowflake": "c100cdbd268164cf084295be", @@ -15359,8 +15359,8 @@ "databricks": "41ae0d23941c87be23d59e1f", "duckdb": "755657fbcced1de4551fb370", "generic": "41ae0d23941c87be23d59e1f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "41ae0d23941c87be23d59e1f", +"motherduck": "755657fbcced1de4551fb370", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "41ae0d23941c87be23d59e1f", "postgresql": "41ae0d23941c87be23d59e1f", "snowflake": "41ae0d23941c87be23d59e1f", @@ -15376,8 +15376,8 @@ "databricks": "3f71ab5f3c1c040da96f2cec", "duckdb": "3f71ab5f3c1c040da96f2cec", "generic": "3f71ab5f3c1c040da96f2cec", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3f71ab5f3c1c040da96f2cec", +"motherduck": "3f71ab5f3c1c040da96f2cec", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3f71ab5f3c1c040da96f2cec", "postgresql": "3f71ab5f3c1c040da96f2cec", "snowflake": "3f71ab5f3c1c040da96f2cec", @@ -15393,8 +15393,8 @@ "databricks": "251aba3fef503ce64556c701", "duckdb": "251aba3fef503ce64556c701", "generic": "251aba3fef503ce64556c701", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "251aba3fef503ce64556c701", +"motherduck": "251aba3fef503ce64556c701", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "251aba3fef503ce64556c701", "postgresql": "251aba3fef503ce64556c701", "snowflake": "c50a316d091c821866e4bfba", @@ -15410,8 +15410,8 @@ "databricks": "79278352c17c9de0f5a28af5", "duckdb": "79278352c17c9de0f5a28af5", "generic": "79278352c17c9de0f5a28af5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "79278352c17c9de0f5a28af5", +"motherduck": "79278352c17c9de0f5a28af5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "79278352c17c9de0f5a28af5", "postgresql": "79278352c17c9de0f5a28af5", "snowflake": "97a20344d811fb77c7690863", @@ -15427,8 +15427,8 @@ "databricks": "c82815abe30b3d099903f838", "duckdb": "c82815abe30b3d099903f838", "generic": "c82815abe30b3d099903f838", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c82815abe30b3d099903f838", +"motherduck": "c82815abe30b3d099903f838", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c82815abe30b3d099903f838", "postgresql": "c82815abe30b3d099903f838", "snowflake": "c82815abe30b3d099903f838", @@ -15444,8 +15444,8 @@ "databricks": "d6e37cd37419bfb26e09b080", "duckdb": "d6e37cd37419bfb26e09b080", "generic": "d6e37cd37419bfb26e09b080", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d6e37cd37419bfb26e09b080", +"motherduck": "d6e37cd37419bfb26e09b080", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d6e37cd37419bfb26e09b080", "postgresql": "d6e37cd37419bfb26e09b080", "snowflake": "d6e37cd37419bfb26e09b080", @@ -15461,8 +15461,8 @@ "databricks": "5c28019dce48a74708c90a76", "duckdb": "5c28019dce48a74708c90a76", "generic": "5c28019dce48a74708c90a76", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5c28019dce48a74708c90a76", +"motherduck": "5c28019dce48a74708c90a76", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5c28019dce48a74708c90a76", "postgresql": "5c28019dce48a74708c90a76", "snowflake": "a5656d70e208254d1d3e9b4f", @@ -15478,8 +15478,8 @@ "databricks": "f4c43a87a78970bca1f88b09", "duckdb": "f4c43a87a78970bca1f88b09", "generic": "f4c43a87a78970bca1f88b09", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f4c43a87a78970bca1f88b09", +"motherduck": "f4c43a87a78970bca1f88b09", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f4c43a87a78970bca1f88b09", "postgresql": "f4c43a87a78970bca1f88b09", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -15495,8 +15495,8 @@ "databricks": "669084d1ea704ae5b8ee05c2", "duckdb": "669084d1ea704ae5b8ee05c2", "generic": "669084d1ea704ae5b8ee05c2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "669084d1ea704ae5b8ee05c2", +"motherduck": "669084d1ea704ae5b8ee05c2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "93c1b5b5960e26479baf88d0", "postgresql": "93c1b5b5960e26479baf88d0", "snowflake": "669084d1ea704ae5b8ee05c2", @@ -15512,8 +15512,8 @@ "databricks": "9070740b5c7f7fb5d091ad05", "duckdb": "9070740b5c7f7fb5d091ad05", "generic": "9070740b5c7f7fb5d091ad05", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9070740b5c7f7fb5d091ad05", +"motherduck": "9070740b5c7f7fb5d091ad05", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9070740b5c7f7fb5d091ad05", "postgresql": "9070740b5c7f7fb5d091ad05", "snowflake": "9070740b5c7f7fb5d091ad05", @@ -15529,8 +15529,8 @@ "databricks": "19a1a80ffc06169f32694de8", "duckdb": "19a1a80ffc06169f32694de8", "generic": "19a1a80ffc06169f32694de8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "19a1a80ffc06169f32694de8", +"motherduck": "19a1a80ffc06169f32694de8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ab1f5f46e69284778bd66d58", "postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", @@ -15546,8 +15546,8 @@ "databricks": "2bfa352725f9d8a91250f75e", "duckdb": "2bfa352725f9d8a91250f75e", "generic": "2bfa352725f9d8a91250f75e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2bfa352725f9d8a91250f75e", +"motherduck": "2bfa352725f9d8a91250f75e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2bfa352725f9d8a91250f75e", "postgresql": "2bfa352725f9d8a91250f75e", "snowflake": "2bfa352725f9d8a91250f75e", @@ -15563,8 +15563,8 @@ "databricks": "bf34c743a59d7c3f212cf17b", "duckdb": "c0eec31620dfc2ac0028173d", "generic": "bf34c743a59d7c3f212cf17b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "bf34c743a59d7c3f212cf17b", +"motherduck": "c0eec31620dfc2ac0028173d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "759df854baf67e256ec19adb", "postgresql": "759df854baf67e256ec19adb", "snowflake": "354dee69d4f0c63cf79046ac", @@ -15580,8 +15580,8 @@ "databricks": "6cffd1617edd6454241ee732", "duckdb": "6cffd1617edd6454241ee732", "generic": "6cffd1617edd6454241ee732", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6cffd1617edd6454241ee732", +"motherduck": "6cffd1617edd6454241ee732", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "6cffd1617edd6454241ee732", "postgresql": "6cffd1617edd6454241ee732", "snowflake": "6cffd1617edd6454241ee732", @@ -15597,8 +15597,8 @@ "databricks": "e3bc1b8bad22d8104417b00f", "duckdb": "111af35848fe500ec2ae54b2", "generic": "111af35848fe500ec2ae54b2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e3bc1b8bad22d8104417b00f", +"motherduck": "111af35848fe500ec2ae54b2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "111af35848fe500ec2ae54b2", "postgresql": "111af35848fe500ec2ae54b2", "snowflake": "e3bc1b8bad22d8104417b00f", @@ -15614,8 +15614,8 @@ "databricks": "e4f0244beaa01667e80482b6", "duckdb": "a766fea06fc070b352976b0a", "generic": "a766fea06fc070b352976b0a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a766fea06fc070b352976b0a", +"motherduck": "a766fea06fc070b352976b0a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a766fea06fc070b352976b0a", "postgresql": "a766fea06fc070b352976b0a", "snowflake": "a766fea06fc070b352976b0a", @@ -15631,8 +15631,8 @@ "databricks": "6a912271e85e6d85c1238bc0", "duckdb": "6a912271e85e6d85c1238bc0", "generic": "6a912271e85e6d85c1238bc0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6a912271e85e6d85c1238bc0", +"motherduck": "6a912271e85e6d85c1238bc0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "6a912271e85e6d85c1238bc0", "postgresql": "6a912271e85e6d85c1238bc0", "snowflake": "6a912271e85e6d85c1238bc0", @@ -15648,8 +15648,8 @@ "databricks": "e3bc1b8bad22d8104417b00f", "duckdb": "3efe8bb57c98fcbe74096b3c", "generic": "3efe8bb57c98fcbe74096b3c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1d8b945fd793ecdcacb90c21", +"motherduck": "3efe8bb57c98fcbe74096b3c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "111af35848fe500ec2ae54b2", "postgresql": "111af35848fe500ec2ae54b2", "snowflake": "1d8b945fd793ecdcacb90c21", @@ -15665,8 +15665,8 @@ "databricks": "e6ffdece2977be55da73b59a", "duckdb": "c50a316d091c821866e4bfba", "generic": "e6ffdece2977be55da73b59a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e6ffdece2977be55da73b59a", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -15682,8 +15682,8 @@ "databricks": "1e75e5227bea0ae294ac7af1", "duckdb": "19bdd92ea0cc5fb028657510", "generic": "b8321619b794db091c9e96a1", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b8321619b794db091c9e96a1", +"motherduck": "19bdd92ea0cc5fb028657510", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "bce26544873d6ec9191c9a86", "postgresql": "bce26544873d6ec9191c9a86", "snowflake": "b8321619b794db091c9e96a1", @@ -15699,8 +15699,8 @@ "databricks": "e6ffdece2977be55da73b59a", "duckdb": "c50a316d091c821866e4bfba", "generic": "e6ffdece2977be55da73b59a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e6ffdece2977be55da73b59a", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -15716,8 +15716,8 @@ "databricks": "8a3f094f18fb4047f0febf59", "duckdb": "8a3f094f18fb4047f0febf59", "generic": "8a3f094f18fb4047f0febf59", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5c28019dce48a74708c90a76", +"motherduck": "8a3f094f18fb4047f0febf59", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8a3f094f18fb4047f0febf59", "postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "850deea990b52b82b36a9848", @@ -15733,8 +15733,8 @@ "databricks": "2bfa352725f9d8a91250f75e", "duckdb": "2bfa352725f9d8a91250f75e", "generic": "2bfa352725f9d8a91250f75e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2bfa352725f9d8a91250f75e", +"motherduck": "2bfa352725f9d8a91250f75e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2bfa352725f9d8a91250f75e", "postgresql": "2bfa352725f9d8a91250f75e", "snowflake": "2bfa352725f9d8a91250f75e", @@ -15750,8 +15750,8 @@ "databricks": "07c9e779f7ae9c58f4f0c902", "duckdb": "07c9e779f7ae9c58f4f0c902", "generic": "07c9e779f7ae9c58f4f0c902", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "07c9e779f7ae9c58f4f0c902", +"motherduck": "07c9e779f7ae9c58f4f0c902", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "07c9e779f7ae9c58f4f0c902", "postgresql": "07c9e779f7ae9c58f4f0c902", "snowflake": "07c9e779f7ae9c58f4f0c902", @@ -15767,8 +15767,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1eb7fb4e8cf75773c5573562", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1eb7fb4e8cf75773c5573562", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -15784,8 +15784,8 @@ "databricks": "5c28019dce48a74708c90a76", "duckdb": "5c28019dce48a74708c90a76", "generic": "5c28019dce48a74708c90a76", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5c28019dce48a74708c90a76", +"motherduck": "5c28019dce48a74708c90a76", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5c28019dce48a74708c90a76", "postgresql": "5c28019dce48a74708c90a76", "snowflake": "a5656d70e208254d1d3e9b4f", @@ -15801,8 +15801,8 @@ "databricks": "ebc821a95b9286e22c2b26d0", "duckdb": "ebc821a95b9286e22c2b26d0", "generic": "ebc821a95b9286e22c2b26d0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ebc821a95b9286e22c2b26d0", +"motherduck": "ebc821a95b9286e22c2b26d0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ebc821a95b9286e22c2b26d0", "postgresql": "ebc821a95b9286e22c2b26d0", "snowflake": "ebc821a95b9286e22c2b26d0", @@ -15818,8 +15818,8 @@ "databricks": "e248573edaaaa875cb926c45", "duckdb": "e248573edaaaa875cb926c45", "generic": "e248573edaaaa875cb926c45", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e248573edaaaa875cb926c45", +"motherduck": "e248573edaaaa875cb926c45", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e248573edaaaa875cb926c45", "postgresql": "e248573edaaaa875cb926c45", "snowflake": "e248573edaaaa875cb926c45", @@ -15835,8 +15835,8 @@ "databricks": "0b8ad171901cc2b7725617da", "duckdb": "0b8ad171901cc2b7725617da", "generic": "0b8ad171901cc2b7725617da", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0b8ad171901cc2b7725617da", +"motherduck": "0b8ad171901cc2b7725617da", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0b8ad171901cc2b7725617da", "postgresql": "0b8ad171901cc2b7725617da", "snowflake": "0b8ad171901cc2b7725617da", @@ -15852,8 +15852,8 @@ "databricks": "d44c023c720f385769f2ab81", "duckdb": "d44c023c720f385769f2ab81", "generic": "d44c023c720f385769f2ab81", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d44c023c720f385769f2ab81", +"motherduck": "d44c023c720f385769f2ab81", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d44c023c720f385769f2ab81", "postgresql": "d44c023c720f385769f2ab81", "snowflake": "d44c023c720f385769f2ab81", @@ -15869,8 +15869,8 @@ "databricks": "8a3f094f18fb4047f0febf59", "duckdb": "8a3f094f18fb4047f0febf59", "generic": "8a3f094f18fb4047f0febf59", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5c28019dce48a74708c90a76", +"motherduck": "8a3f094f18fb4047f0febf59", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8a3f094f18fb4047f0febf59", "postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "850deea990b52b82b36a9848", @@ -15886,8 +15886,8 @@ "databricks": "5abf4ec799a2ec32fb10da7e", "duckdb": "5abf4ec799a2ec32fb10da7e", "generic": "5abf4ec799a2ec32fb10da7e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5abf4ec799a2ec32fb10da7e", +"motherduck": "5abf4ec799a2ec32fb10da7e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5abf4ec799a2ec32fb10da7e", "postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "a5656d70e208254d1d3e9b4f", @@ -15903,8 +15903,8 @@ "databricks": "c5c5fbb0257c93153cb84430", "duckdb": "c5c5fbb0257c93153cb84430", "generic": "c5c5fbb0257c93153cb84430", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c5c5fbb0257c93153cb84430", +"motherduck": "c5c5fbb0257c93153cb84430", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c5c5fbb0257c93153cb84430", "postgresql": "c5c5fbb0257c93153cb84430", "snowflake": "c5c5fbb0257c93153cb84430", @@ -15920,8 +15920,8 @@ "databricks": "d6e37cd37419bfb26e09b080", "duckdb": "d6e37cd37419bfb26e09b080", "generic": "d6e37cd37419bfb26e09b080", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d6e37cd37419bfb26e09b080", +"motherduck": "d6e37cd37419bfb26e09b080", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d6e37cd37419bfb26e09b080", "postgresql": "d6e37cd37419bfb26e09b080", "snowflake": "d6e37cd37419bfb26e09b080", @@ -15937,8 +15937,8 @@ "databricks": "d34f84c1b71b9c9c0c20abb0", "duckdb": "d34f84c1b71b9c9c0c20abb0", "generic": "d34f84c1b71b9c9c0c20abb0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d34f84c1b71b9c9c0c20abb0", +"motherduck": "d34f84c1b71b9c9c0c20abb0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d34f84c1b71b9c9c0c20abb0", "postgresql": "d34f84c1b71b9c9c0c20abb0", "snowflake": "d34f84c1b71b9c9c0c20abb0", @@ -15954,8 +15954,8 @@ "databricks": "e6ffdece2977be55da73b59a", "duckdb": "c50a316d091c821866e4bfba", "generic": "e6ffdece2977be55da73b59a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e6ffdece2977be55da73b59a", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -15971,8 +15971,8 @@ "databricks": "bbb2563edda71d0b69ef0855", "duckdb": "8a3f094f18fb4047f0febf59", "generic": "bbb2563edda71d0b69ef0855", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "51d717b08676179ee1ccefe4", +"motherduck": "8a3f094f18fb4047f0febf59", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "bbb2563edda71d0b69ef0855", "postgresql": "bbb2563edda71d0b69ef0855", "snowflake": "bbb2563edda71d0b69ef0855", @@ -15988,8 +15988,8 @@ "databricks": "79278352c17c9de0f5a28af5", "duckdb": "79278352c17c9de0f5a28af5", "generic": "79278352c17c9de0f5a28af5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "79278352c17c9de0f5a28af5", +"motherduck": "79278352c17c9de0f5a28af5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "79278352c17c9de0f5a28af5", "postgresql": "79278352c17c9de0f5a28af5", "snowflake": "97a20344d811fb77c7690863", @@ -16005,8 +16005,8 @@ "databricks": "c1eef7499ef79562fa125447", "duckdb": "c1eef7499ef79562fa125447", "generic": "c1eef7499ef79562fa125447", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c1eef7499ef79562fa125447", +"motherduck": "c1eef7499ef79562fa125447", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c1eef7499ef79562fa125447", "postgresql": "c1eef7499ef79562fa125447", "snowflake": "c1eef7499ef79562fa125447", @@ -16022,8 +16022,8 @@ "databricks": "b210f8eb26c37b422f786c6b", "duckdb": "b210f8eb26c37b422f786c6b", "generic": "b210f8eb26c37b422f786c6b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b210f8eb26c37b422f786c6b", +"motherduck": "b210f8eb26c37b422f786c6b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b210f8eb26c37b422f786c6b", "postgresql": "b210f8eb26c37b422f786c6b", "snowflake": "b210f8eb26c37b422f786c6b", @@ -16039,8 +16039,8 @@ "databricks": "2d4aea8dec5521e258596f3d", "duckdb": "2d4aea8dec5521e258596f3d", "generic": "2d4aea8dec5521e258596f3d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2d4aea8dec5521e258596f3d", +"motherduck": "2d4aea8dec5521e258596f3d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f15c56475abfc73488abe63b", "postgresql": "f15c56475abfc73488abe63b", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -16056,8 +16056,8 @@ "databricks": "ba98b19402ac6379a4172de9", "duckdb": "0712c33a016f7dd6a37ee011", "generic": "ba98b19402ac6379a4172de9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ba98b19402ac6379a4172de9", +"motherduck": "0712c33a016f7dd6a37ee011", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3aa01d5ccf0d0b7e5c540297", "postgresql": "3aa01d5ccf0d0b7e5c540297", "snowflake": "ba98b19402ac6379a4172de9", @@ -16073,8 +16073,8 @@ "databricks": "e46d4be44ab9e500e9190d26", "duckdb": "e46d4be44ab9e500e9190d26", "generic": "e46d4be44ab9e500e9190d26", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e46d4be44ab9e500e9190d26", +"motherduck": "e46d4be44ab9e500e9190d26", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "251aba3fef503ce64556c701", "postgresql": "251aba3fef503ce64556c701", "snowflake": "e46d4be44ab9e500e9190d26", @@ -16090,8 +16090,8 @@ "databricks": "9572f907cb1c76265b3b608f", "duckdb": "6c58c6a62958e64e6a26bf9c", "generic": "9572f907cb1c76265b3b608f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9572f907cb1c76265b3b608f", +"motherduck": "6c58c6a62958e64e6a26bf9c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9572f907cb1c76265b3b608f", "postgresql": "9572f907cb1c76265b3b608f", "snowflake": "6c58c6a62958e64e6a26bf9c", @@ -16107,8 +16107,8 @@ "databricks": "2bfa352725f9d8a91250f75e", "duckdb": "2bfa352725f9d8a91250f75e", "generic": "2bfa352725f9d8a91250f75e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2bfa352725f9d8a91250f75e", +"motherduck": "2bfa352725f9d8a91250f75e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2bfa352725f9d8a91250f75e", "postgresql": "2bfa352725f9d8a91250f75e", "snowflake": "2bfa352725f9d8a91250f75e", @@ -16124,8 +16124,8 @@ "databricks": "42a76d5ff05d0f91911ff368", "duckdb": "42a76d5ff05d0f91911ff368", "generic": "42a76d5ff05d0f91911ff368", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "42a76d5ff05d0f91911ff368", +"motherduck": "42a76d5ff05d0f91911ff368", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "42a76d5ff05d0f91911ff368", "postgresql": "42a76d5ff05d0f91911ff368", "snowflake": "42a76d5ff05d0f91911ff368", @@ -16141,8 +16141,8 @@ "databricks": "251aba3fef503ce64556c701", "duckdb": "251aba3fef503ce64556c701", "generic": "251aba3fef503ce64556c701", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "251aba3fef503ce64556c701", +"motherduck": "251aba3fef503ce64556c701", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "251aba3fef503ce64556c701", "postgresql": "251aba3fef503ce64556c701", "snowflake": "c50a316d091c821866e4bfba", @@ -16158,8 +16158,8 @@ "databricks": "5d25d5e13f81700741695ed4", "duckdb": "5d25d5e13f81700741695ed4", "generic": "5d25d5e13f81700741695ed4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5d25d5e13f81700741695ed4", +"motherduck": "5d25d5e13f81700741695ed4", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5d25d5e13f81700741695ed4", "postgresql": "5d25d5e13f81700741695ed4", "snowflake": "5d25d5e13f81700741695ed4", @@ -16175,8 +16175,8 @@ "databricks": "1e75e5227bea0ae294ac7af1", "duckdb": "19bdd92ea0cc5fb028657510", "generic": "1e75e5227bea0ae294ac7af1", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "19bdd92ea0cc5fb028657510", +"motherduck": "19bdd92ea0cc5fb028657510", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "19bdd92ea0cc5fb028657510", "postgresql": "19bdd92ea0cc5fb028657510", "snowflake": "8ff614b7306c0edbe36ce00b", @@ -16192,8 +16192,8 @@ "databricks": "268b9b7c7862c9de6569286a", "duckdb": "268b9b7c7862c9de6569286a", "generic": "268b9b7c7862c9de6569286a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "268b9b7c7862c9de6569286a", +"motherduck": "268b9b7c7862c9de6569286a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "268b9b7c7862c9de6569286a", "postgresql": "268b9b7c7862c9de6569286a", "snowflake": "268b9b7c7862c9de6569286a", @@ -16209,8 +16209,8 @@ "databricks": "da33de41a707d0e067e415fd", "duckdb": "da33de41a707d0e067e415fd", "generic": "da33de41a707d0e067e415fd", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "da33de41a707d0e067e415fd", +"motherduck": "da33de41a707d0e067e415fd", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "da33de41a707d0e067e415fd", "postgresql": "da33de41a707d0e067e415fd", "snowflake": "da33de41a707d0e067e415fd", @@ -16226,8 +16226,8 @@ "databricks": "7c8df74df226e73b459bfdba", "duckdb": "7c8df74df226e73b459bfdba", "generic": "7c8df74df226e73b459bfdba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "7c8df74df226e73b459bfdba", +"motherduck": "7c8df74df226e73b459bfdba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7c8df74df226e73b459bfdba", "postgresql": "7c8df74df226e73b459bfdba", "snowflake": "7c8df74df226e73b459bfdba", @@ -16243,8 +16243,8 @@ "databricks": "ebb346128c80cb653e9694c8", "duckdb": "ebb346128c80cb653e9694c8", "generic": "ebb346128c80cb653e9694c8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ebb346128c80cb653e9694c8", +"motherduck": "ebb346128c80cb653e9694c8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ebb346128c80cb653e9694c8", "postgresql": "ebb346128c80cb653e9694c8", "snowflake": "ebb346128c80cb653e9694c8", @@ -16260,8 +16260,8 @@ "databricks": "00714a615c9e7a090fb122c5", "duckdb": "9f4b51f000ffbe30f6b7d826", "generic": "00714a615c9e7a090fb122c5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "00714a615c9e7a090fb122c5", +"motherduck": "9f4b51f000ffbe30f6b7d826", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "30d01707c0b130f64a4d8414", "postgresql": "30d01707c0b130f64a4d8414", "snowflake": "bacfe825b19ef52bc792ab22", @@ -16277,8 +16277,8 @@ "databricks": "f85dfff687194a44f32eeb15", "duckdb": "f85dfff687194a44f32eeb15", "generic": "f85dfff687194a44f32eeb15", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f85dfff687194a44f32eeb15", +"motherduck": "f85dfff687194a44f32eeb15", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f85dfff687194a44f32eeb15", "postgresql": "f85dfff687194a44f32eeb15", "snowflake": "f85dfff687194a44f32eeb15", @@ -16294,8 +16294,8 @@ "databricks": "24b677b214ecdacdb007642e", "duckdb": "24b677b214ecdacdb007642e", "generic": "24b677b214ecdacdb007642e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "24b677b214ecdacdb007642e", +"motherduck": "24b677b214ecdacdb007642e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "24b677b214ecdacdb007642e", "postgresql": "24b677b214ecdacdb007642e", "snowflake": "24b677b214ecdacdb007642e", @@ -16311,8 +16311,8 @@ "databricks": "4c2128e6078118fd0d3856fa", "duckdb": "4c2128e6078118fd0d3856fa", "generic": "4c2128e6078118fd0d3856fa", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4c2128e6078118fd0d3856fa", +"motherduck": "4c2128e6078118fd0d3856fa", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "4c2128e6078118fd0d3856fa", "postgresql": "4c2128e6078118fd0d3856fa", "snowflake": "4c2128e6078118fd0d3856fa", @@ -16328,8 +16328,8 @@ "databricks": "2795066c9a71fa3e3780c73b", "duckdb": "29525edc1ad146bad4372f59", "generic": "2795066c9a71fa3e3780c73b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "29525edc1ad146bad4372f59", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "29525edc1ad146bad4372f59", "postgresql": "29525edc1ad146bad4372f59", "snowflake": "b3b752c645651b849e23d39a", @@ -16345,8 +16345,8 @@ "databricks": "b561884132dbe7e42b719110", "duckdb": "b561884132dbe7e42b719110", "generic": "b561884132dbe7e42b719110", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b561884132dbe7e42b719110", +"motherduck": "b561884132dbe7e42b719110", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5aa51d84ac5e6c7a9306f348", "postgresql": "5aa51d84ac5e6c7a9306f348", "snowflake": "b561884132dbe7e42b719110", @@ -16362,8 +16362,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1eb7fb4e8cf75773c5573562", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1eb7fb4e8cf75773c5573562", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -16379,8 +16379,8 @@ "databricks": "e7b8de8bd235305d79e2b6b1", "duckdb": "e7b8de8bd235305d79e2b6b1", "generic": "e7b8de8bd235305d79e2b6b1", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e7b8de8bd235305d79e2b6b1", +"motherduck": "e7b8de8bd235305d79e2b6b1", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e7b8de8bd235305d79e2b6b1", "postgresql": "e7b8de8bd235305d79e2b6b1", "snowflake": "e7b8de8bd235305d79e2b6b1", @@ -16396,8 +16396,8 @@ "databricks": "9aff7fcc7d550e1a8d105471", "duckdb": "9aff7fcc7d550e1a8d105471", "generic": "9aff7fcc7d550e1a8d105471", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9aff7fcc7d550e1a8d105471", +"motherduck": "9aff7fcc7d550e1a8d105471", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9aff7fcc7d550e1a8d105471", "postgresql": "9aff7fcc7d550e1a8d105471", "snowflake": "9aff7fcc7d550e1a8d105471", @@ -16413,8 +16413,8 @@ "databricks": "242bf50062e95bc8534ca887", "duckdb": "242bf50062e95bc8534ca887", "generic": "242bf50062e95bc8534ca887", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "242bf50062e95bc8534ca887", +"motherduck": "242bf50062e95bc8534ca887", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3f86c2b62e6298676c7001fd", "postgresql": "3f86c2b62e6298676c7001fd", "snowflake": "242bf50062e95bc8534ca887", @@ -16430,8 +16430,8 @@ "databricks": "5d25d5e13f81700741695ed4", "duckdb": "5d25d5e13f81700741695ed4", "generic": "5d25d5e13f81700741695ed4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5d25d5e13f81700741695ed4", +"motherduck": "5d25d5e13f81700741695ed4", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "62131fb5b065f5de2e74a0f1", "postgresql": "62131fb5b065f5de2e74a0f1", "snowflake": "5d25d5e13f81700741695ed4", @@ -16447,8 +16447,8 @@ "databricks": "75c45736873ac5c99a62bc20", "duckdb": "13f1401e7cd410600a4c9485", "generic": "75c45736873ac5c99a62bc20", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "75c45736873ac5c99a62bc20", +"motherduck": "13f1401e7cd410600a4c9485", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "13f1401e7cd410600a4c9485", "postgresql": "13f1401e7cd410600a4c9485", "snowflake": "9b5f44707b5f4764b82741be", @@ -16464,8 +16464,8 @@ "databricks": "8e2423c700938b33a6fea361", "duckdb": "bf0a539a5f35525b6b4067c5", "generic": "8e2423c700938b33a6fea361", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "64442499eea1962913b864ba", +"motherduck": "bf0a539a5f35525b6b4067c5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "fac7e3f22dda1f3edd9dee28", "postgresql": "fac7e3f22dda1f3edd9dee28", "snowflake": "fe64d523696db51fa15d432e", @@ -16481,8 +16481,8 @@ "databricks": "15be07c71f6871ae037caae3", "duckdb": "15be07c71f6871ae037caae3", "generic": "15be07c71f6871ae037caae3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "15be07c71f6871ae037caae3", +"motherduck": "15be07c71f6871ae037caae3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "15be07c71f6871ae037caae3", "postgresql": "15be07c71f6871ae037caae3", "snowflake": "15be07c71f6871ae037caae3", @@ -16498,8 +16498,8 @@ "databricks": "b210f8eb26c37b422f786c6b", "duckdb": "b210f8eb26c37b422f786c6b", "generic": "b210f8eb26c37b422f786c6b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b210f8eb26c37b422f786c6b", +"motherduck": "b210f8eb26c37b422f786c6b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b210f8eb26c37b422f786c6b", "postgresql": "b210f8eb26c37b422f786c6b", "snowflake": "b210f8eb26c37b422f786c6b", @@ -16515,8 +16515,8 @@ "databricks": "3d23f811a03db519b598c815", "duckdb": "3d23f811a03db519b598c815", "generic": "3d23f811a03db519b598c815", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3d23f811a03db519b598c815", +"motherduck": "3d23f811a03db519b598c815", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3d23f811a03db519b598c815", "postgresql": "3d23f811a03db519b598c815", "snowflake": "3d23f811a03db519b598c815", @@ -16532,8 +16532,8 @@ "databricks": "fa55698f69d154f89214f351", "duckdb": "fa55698f69d154f89214f351", "generic": "fa55698f69d154f89214f351", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fa55698f69d154f89214f351", +"motherduck": "fa55698f69d154f89214f351", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "fa55698f69d154f89214f351", "postgresql": "fa55698f69d154f89214f351", "snowflake": "fa55698f69d154f89214f351", @@ -16549,8 +16549,8 @@ "databricks": "3009be83c84cdfc92bbb075e", "duckdb": "3009be83c84cdfc92bbb075e", "generic": "3009be83c84cdfc92bbb075e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3009be83c84cdfc92bbb075e", +"motherduck": "3009be83c84cdfc92bbb075e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3009be83c84cdfc92bbb075e", "postgresql": "3009be83c84cdfc92bbb075e", "snowflake": "3009be83c84cdfc92bbb075e", @@ -16566,8 +16566,8 @@ "databricks": "5d25d5e13f81700741695ed4", "duckdb": "5d25d5e13f81700741695ed4", "generic": "5d25d5e13f81700741695ed4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5d25d5e13f81700741695ed4", +"motherduck": "5d25d5e13f81700741695ed4", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "62131fb5b065f5de2e74a0f1", "postgresql": "62131fb5b065f5de2e74a0f1", "snowflake": "5d25d5e13f81700741695ed4", @@ -16583,8 +16583,8 @@ "databricks": "9d6807d797be08f8376a5a19", "duckdb": "9d6807d797be08f8376a5a19", "generic": "9d6807d797be08f8376a5a19", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9d6807d797be08f8376a5a19", +"motherduck": "9d6807d797be08f8376a5a19", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9d6807d797be08f8376a5a19", "postgresql": "9d6807d797be08f8376a5a19", "snowflake": "9d6807d797be08f8376a5a19", @@ -16600,8 +16600,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1eb7fb4e8cf75773c5573562", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1eb7fb4e8cf75773c5573562", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -16617,8 +16617,8 @@ "databricks": "d61c83ef8bfdf043fa672cb5", "duckdb": "d61c83ef8bfdf043fa672cb5", "generic": "d61c83ef8bfdf043fa672cb5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d61c83ef8bfdf043fa672cb5", +"motherduck": "d61c83ef8bfdf043fa672cb5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d61c83ef8bfdf043fa672cb5", "postgresql": "d61c83ef8bfdf043fa672cb5", "snowflake": "97a20344d811fb77c7690863", @@ -16634,8 +16634,8 @@ "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fe23594c781e79cbb0c8927a", +"motherduck": "fe23594c781e79cbb0c8927a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8a3f094f18fb4047f0febf59", "postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", @@ -16651,8 +16651,8 @@ "databricks": "100c78a43fb3cbda50787a10", "duckdb": "100c78a43fb3cbda50787a10", "generic": "100c78a43fb3cbda50787a10", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "100c78a43fb3cbda50787a10", +"motherduck": "100c78a43fb3cbda50787a10", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "100c78a43fb3cbda50787a10", "postgresql": "100c78a43fb3cbda50787a10", "snowflake": "100c78a43fb3cbda50787a10", @@ -16668,8 +16668,8 @@ "databricks": "aab0389e2b5470136660bf8a", "duckdb": "aab0389e2b5470136660bf8a", "generic": "aab0389e2b5470136660bf8a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "aab0389e2b5470136660bf8a", +"motherduck": "aab0389e2b5470136660bf8a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f16105cbc61d547e6c08efc8", "postgresql": "f16105cbc61d547e6c08efc8", "snowflake": "97a20344d811fb77c7690863", @@ -16685,8 +16685,8 @@ "databricks": "624eb95d280fa0653ec73f7f", "duckdb": "624eb95d280fa0653ec73f7f", "generic": "624eb95d280fa0653ec73f7f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "624eb95d280fa0653ec73f7f", +"motherduck": "624eb95d280fa0653ec73f7f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9dd376ce83bd3444e52fdb88", "postgresql": "9dd376ce83bd3444e52fdb88", "snowflake": "624eb95d280fa0653ec73f7f", @@ -16702,8 +16702,8 @@ "databricks": "6b04f574865e71733e09ba1d", "duckdb": "6b04f574865e71733e09ba1d", "generic": "6b04f574865e71733e09ba1d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6b04f574865e71733e09ba1d", +"motherduck": "6b04f574865e71733e09ba1d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e4d1b966e983269b60041cc7", "postgresql": "e4d1b966e983269b60041cc7", "snowflake": "6b04f574865e71733e09ba1d", @@ -16719,8 +16719,8 @@ "databricks": "8a3f094f18fb4047f0febf59", "duckdb": "8a3f094f18fb4047f0febf59", "generic": "8a3f094f18fb4047f0febf59", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5c28019dce48a74708c90a76", +"motherduck": "8a3f094f18fb4047f0febf59", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8a3f094f18fb4047f0febf59", "postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "850deea990b52b82b36a9848", @@ -16736,8 +16736,8 @@ "databricks": "5aa51d84ac5e6c7a9306f348", "duckdb": "5aa51d84ac5e6c7a9306f348", "generic": "5aa51d84ac5e6c7a9306f348", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5aa51d84ac5e6c7a9306f348", +"motherduck": "5aa51d84ac5e6c7a9306f348", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5aa51d84ac5e6c7a9306f348", "postgresql": "5aa51d84ac5e6c7a9306f348", "snowflake": "5aa51d84ac5e6c7a9306f348", @@ -16753,8 +16753,8 @@ "databricks": "07f8200cee37aa0f6990da3d", "duckdb": "07f8200cee37aa0f6990da3d", "generic": "07f8200cee37aa0f6990da3d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "07f8200cee37aa0f6990da3d", +"motherduck": "07f8200cee37aa0f6990da3d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "07f8200cee37aa0f6990da3d", "postgresql": "07f8200cee37aa0f6990da3d", "snowflake": "07f8200cee37aa0f6990da3d", @@ -16770,8 +16770,8 @@ "databricks": "ab5ad6180652b3b00d1f5b01", "duckdb": "ab5ad6180652b3b00d1f5b01", "generic": "ab5ad6180652b3b00d1f5b01", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ab5ad6180652b3b00d1f5b01", +"motherduck": "ab5ad6180652b3b00d1f5b01", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ab5ad6180652b3b00d1f5b01", "postgresql": "ab5ad6180652b3b00d1f5b01", "snowflake": "f43dac38f34a1c0618a3f812", @@ -16787,8 +16787,8 @@ "databricks": "47a5c4cbcdc5fda84d74a947", "duckdb": "47a5c4cbcdc5fda84d74a947", "generic": "47a5c4cbcdc5fda84d74a947", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "47a5c4cbcdc5fda84d74a947", +"motherduck": "47a5c4cbcdc5fda84d74a947", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "47a5c4cbcdc5fda84d74a947", "postgresql": "47a5c4cbcdc5fda84d74a947", "snowflake": "47a5c4cbcdc5fda84d74a947", @@ -16804,8 +16804,8 @@ "databricks": "e46d4be44ab9e500e9190d26", "duckdb": "e46d4be44ab9e500e9190d26", "generic": "e46d4be44ab9e500e9190d26", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e46d4be44ab9e500e9190d26", +"motherduck": "e46d4be44ab9e500e9190d26", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e46d4be44ab9e500e9190d26", "postgresql": "e46d4be44ab9e500e9190d26", "snowflake": "e46d4be44ab9e500e9190d26", @@ -16821,8 +16821,8 @@ "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "8a3f094f18fb4047f0febf59", "generic": "fe23594c781e79cbb0c8927a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fe23594c781e79cbb0c8927a", +"motherduck": "8a3f094f18fb4047f0febf59", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b0ce99394388f977f67fd1df", "postgresql": "b0ce99394388f977f67fd1df", "snowflake": "38b2ec94bf02be40007ce525", @@ -16838,8 +16838,8 @@ "databricks": "de89632e277dbc8c97115419", "duckdb": "de89632e277dbc8c97115419", "generic": "de89632e277dbc8c97115419", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "de89632e277dbc8c97115419", +"motherduck": "de89632e277dbc8c97115419", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5abf4ec799a2ec32fb10da7e", "postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", @@ -16855,8 +16855,8 @@ "databricks": "4fb572024031cb7631777be1", "duckdb": "792f7d3389f076f50c713946", "generic": "792f7d3389f076f50c713946", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "792f7d3389f076f50c713946", +"motherduck": "792f7d3389f076f50c713946", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "792f7d3389f076f50c713946", "postgresql": "792f7d3389f076f50c713946", "snowflake": "792f7d3389f076f50c713946", @@ -16872,8 +16872,8 @@ "databricks": "aae2b1fe6f2d9807ecde412b", "duckdb": "aae2b1fe6f2d9807ecde412b", "generic": "aae2b1fe6f2d9807ecde412b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "aae2b1fe6f2d9807ecde412b", +"motherduck": "aae2b1fe6f2d9807ecde412b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "aae2b1fe6f2d9807ecde412b", "postgresql": "aae2b1fe6f2d9807ecde412b", "snowflake": "aae2b1fe6f2d9807ecde412b", @@ -16889,8 +16889,8 @@ "databricks": "ac92c18bc03647c2ff622e35", "duckdb": "16adcdd909f51e3f33d7cdde", "generic": "ac92c18bc03647c2ff622e35", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ac92c18bc03647c2ff622e35", +"motherduck": "16adcdd909f51e3f33d7cdde", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ac92c18bc03647c2ff622e35", "postgresql": "ac92c18bc03647c2ff622e35", "snowflake": "42ed29994060215ec30494f1", @@ -16906,8 +16906,8 @@ "databricks": "690b44350cdb6852c9bc71a3", "duckdb": "690b44350cdb6852c9bc71a3", "generic": "690b44350cdb6852c9bc71a3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "690b44350cdb6852c9bc71a3", +"motherduck": "690b44350cdb6852c9bc71a3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "690b44350cdb6852c9bc71a3", "postgresql": "690b44350cdb6852c9bc71a3", "snowflake": "690b44350cdb6852c9bc71a3", @@ -16923,8 +16923,8 @@ "databricks": "3a7ffeb8f81df6ccd57f3689", "duckdb": "3a7ffeb8f81df6ccd57f3689", "generic": "3a7ffeb8f81df6ccd57f3689", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3a7ffeb8f81df6ccd57f3689", +"motherduck": "3a7ffeb8f81df6ccd57f3689", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "062e51ec86ccd542eb78500d", "postgresql": "062e51ec86ccd542eb78500d", "snowflake": "3a7ffeb8f81df6ccd57f3689", @@ -16940,8 +16940,8 @@ "databricks": "c752fd9fe2ad3b86b8b899da", "duckdb": "c752fd9fe2ad3b86b8b899da", "generic": "c752fd9fe2ad3b86b8b899da", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c752fd9fe2ad3b86b8b899da", +"motherduck": "c752fd9fe2ad3b86b8b899da", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e998cd8f0f74af0526b75ff0", "postgresql": "e998cd8f0f74af0526b75ff0", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -16957,8 +16957,8 @@ "databricks": "01d51e9454e607e1bab153ef", "duckdb": "01d51e9454e607e1bab153ef", "generic": "01d51e9454e607e1bab153ef", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "01d51e9454e607e1bab153ef", +"motherduck": "01d51e9454e607e1bab153ef", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "01d51e9454e607e1bab153ef", "postgresql": "01d51e9454e607e1bab153ef", "snowflake": "01d51e9454e607e1bab153ef", @@ -16974,8 +16974,8 @@ "databricks": "802f1c1fcb97ddd5679172d2", "duckdb": "802f1c1fcb97ddd5679172d2", "generic": "802f1c1fcb97ddd5679172d2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "802f1c1fcb97ddd5679172d2", +"motherduck": "802f1c1fcb97ddd5679172d2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "802f1c1fcb97ddd5679172d2", "postgresql": "802f1c1fcb97ddd5679172d2", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -16991,8 +16991,8 @@ "databricks": "19a1a80ffc06169f32694de8", "duckdb": "19a1a80ffc06169f32694de8", "generic": "19a1a80ffc06169f32694de8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "19a1a80ffc06169f32694de8", +"motherduck": "19a1a80ffc06169f32694de8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ab1f5f46e69284778bd66d58", "postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", @@ -17008,8 +17008,8 @@ "databricks": "ebc821a95b9286e22c2b26d0", "duckdb": "ebc821a95b9286e22c2b26d0", "generic": "ebc821a95b9286e22c2b26d0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ebc821a95b9286e22c2b26d0", +"motherduck": "ebc821a95b9286e22c2b26d0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ebc821a95b9286e22c2b26d0", "postgresql": "ebc821a95b9286e22c2b26d0", "snowflake": "ebc821a95b9286e22c2b26d0", @@ -17025,8 +17025,8 @@ "databricks": "fe0bedb7dda0e9247a51fdee", "duckdb": "fe0bedb7dda0e9247a51fdee", "generic": "fe0bedb7dda0e9247a51fdee", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fe0bedb7dda0e9247a51fdee", +"motherduck": "fe0bedb7dda0e9247a51fdee", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "fe0bedb7dda0e9247a51fdee", "postgresql": "fe0bedb7dda0e9247a51fdee", "snowflake": "fe0bedb7dda0e9247a51fdee", @@ -17042,8 +17042,8 @@ "databricks": "e1250582e271934cbcb2a788", "duckdb": "e1250582e271934cbcb2a788", "generic": "e1250582e271934cbcb2a788", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e1250582e271934cbcb2a788", +"motherduck": "e1250582e271934cbcb2a788", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e1250582e271934cbcb2a788", "postgresql": "e1250582e271934cbcb2a788", "snowflake": "e1250582e271934cbcb2a788", @@ -17059,8 +17059,8 @@ "databricks": "dbf9ebbc7016f5ac5df2817e", "duckdb": "dbf9ebbc7016f5ac5df2817e", "generic": "dbf9ebbc7016f5ac5df2817e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "dbf9ebbc7016f5ac5df2817e", +"motherduck": "dbf9ebbc7016f5ac5df2817e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "4cccf86e62d627a7a8b57726", "postgresql": "4cccf86e62d627a7a8b57726", "snowflake": "dbf9ebbc7016f5ac5df2817e", @@ -17076,8 +17076,8 @@ "databricks": "aab0389e2b5470136660bf8a", "duckdb": "aab0389e2b5470136660bf8a", "generic": "aab0389e2b5470136660bf8a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "aab0389e2b5470136660bf8a", +"motherduck": "aab0389e2b5470136660bf8a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f16105cbc61d547e6c08efc8", "postgresql": "f16105cbc61d547e6c08efc8", "snowflake": "97a20344d811fb77c7690863", @@ -17093,8 +17093,8 @@ "databricks": "1bdf5908ca249b2331a0427c", "duckdb": "1bdf5908ca249b2331a0427c", "generic": "1bdf5908ca249b2331a0427c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1bdf5908ca249b2331a0427c", +"motherduck": "1bdf5908ca249b2331a0427c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1bdf5908ca249b2331a0427c", "postgresql": "1bdf5908ca249b2331a0427c", "snowflake": "8ff614b7306c0edbe36ce00b", @@ -17110,8 +17110,8 @@ "databricks": "6bd27be6a47ef34ec1defbad", "duckdb": "b0791f9f270e764eeeb6fb8e", "generic": "c7c38fdeaeb7858a44976f1d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c7c38fdeaeb7858a44976f1d", +"motherduck": "b0791f9f270e764eeeb6fb8e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "4d7604d41e3585ec187dec82", "postgresql": "4d7604d41e3585ec187dec82", "snowflake": "c7c38fdeaeb7858a44976f1d", @@ -17127,8 +17127,8 @@ "databricks": "0b9d9879e289ea9a6a3e3e70", "duckdb": "0b9d9879e289ea9a6a3e3e70", "generic": "0b9d9879e289ea9a6a3e3e70", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0b9d9879e289ea9a6a3e3e70", +"motherduck": "0b9d9879e289ea9a6a3e3e70", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0b9d9879e289ea9a6a3e3e70", "postgresql": "0b9d9879e289ea9a6a3e3e70", "snowflake": "0b9d9879e289ea9a6a3e3e70", @@ -17144,8 +17144,8 @@ "databricks": "ba0da7ef018b990fcc83cba1", "duckdb": "ba0da7ef018b990fcc83cba1", "generic": "ba0da7ef018b990fcc83cba1", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ba0da7ef018b990fcc83cba1", +"motherduck": "ba0da7ef018b990fcc83cba1", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ba0da7ef018b990fcc83cba1", "postgresql": "ba0da7ef018b990fcc83cba1", "snowflake": "ba0da7ef018b990fcc83cba1", @@ -17161,8 +17161,8 @@ "databricks": "d0f04a0ad4aff66d31330f7a", "duckdb": "9efea5795b256d49cab6a366", "generic": "9efea5795b256d49cab6a366", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9efea5795b256d49cab6a366", +"motherduck": "9efea5795b256d49cab6a366", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9efea5795b256d49cab6a366", "postgresql": "9efea5795b256d49cab6a366", "snowflake": "50d14e3a5691e6cb8689aa29", @@ -17178,8 +17178,8 @@ "databricks": "2795066c9a71fa3e3780c73b", "duckdb": "29525edc1ad146bad4372f59", "generic": "2795066c9a71fa3e3780c73b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "29525edc1ad146bad4372f59", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "29525edc1ad146bad4372f59", "postgresql": "29525edc1ad146bad4372f59", "snowflake": "b3b752c645651b849e23d39a", @@ -17195,8 +17195,8 @@ "databricks": "881106a4a0d2de0ff7c56373", "duckdb": "881106a4a0d2de0ff7c56373", "generic": "881106a4a0d2de0ff7c56373", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "881106a4a0d2de0ff7c56373", +"motherduck": "881106a4a0d2de0ff7c56373", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "cf167ec395fc843602e3b1e0", "postgresql": "cf167ec395fc843602e3b1e0", "snowflake": "881106a4a0d2de0ff7c56373", @@ -17212,8 +17212,8 @@ "databricks": "669084d1ea704ae5b8ee05c2", "duckdb": "669084d1ea704ae5b8ee05c2", "generic": "669084d1ea704ae5b8ee05c2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "669084d1ea704ae5b8ee05c2", +"motherduck": "669084d1ea704ae5b8ee05c2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "93c1b5b5960e26479baf88d0", "postgresql": "93c1b5b5960e26479baf88d0", "snowflake": "669084d1ea704ae5b8ee05c2", @@ -17229,8 +17229,8 @@ "databricks": "013a8446f043affffcf194f8", "duckdb": "013a8446f043affffcf194f8", "generic": "013a8446f043affffcf194f8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "013a8446f043affffcf194f8", +"motherduck": "013a8446f043affffcf194f8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b0477b36767a62145aa9158c", "postgresql": "b0477b36767a62145aa9158c", "snowflake": "97a20344d811fb77c7690863", @@ -17246,8 +17246,8 @@ "databricks": "804fec6876d0b43935790142", "duckdb": "804fec6876d0b43935790142", "generic": "804fec6876d0b43935790142", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "804fec6876d0b43935790142", +"motherduck": "804fec6876d0b43935790142", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "191fc59c754706512337edc1", "postgresql": "191fc59c754706512337edc1", "snowflake": "804fec6876d0b43935790142", @@ -17263,8 +17263,8 @@ "databricks": "0dcd6a19b7dc6f6b6d4a5006", "duckdb": "0dcd6a19b7dc6f6b6d4a5006", "generic": "0dcd6a19b7dc6f6b6d4a5006", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0dcd6a19b7dc6f6b6d4a5006", +"motherduck": "0dcd6a19b7dc6f6b6d4a5006", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0dcd6a19b7dc6f6b6d4a5006", "postgresql": "0dcd6a19b7dc6f6b6d4a5006", "snowflake": "0dcd6a19b7dc6f6b6d4a5006", @@ -17280,8 +17280,8 @@ "databricks": "9d6807d797be08f8376a5a19", "duckdb": "9d6807d797be08f8376a5a19", "generic": "9d6807d797be08f8376a5a19", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9d6807d797be08f8376a5a19", +"motherduck": "9d6807d797be08f8376a5a19", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9d6807d797be08f8376a5a19", "postgresql": "9d6807d797be08f8376a5a19", "snowflake": "9d6807d797be08f8376a5a19", @@ -17297,8 +17297,8 @@ "databricks": "3915f3f73328f0838497b2d5", "duckdb": "3915f3f73328f0838497b2d5", "generic": "3915f3f73328f0838497b2d5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3915f3f73328f0838497b2d5", +"motherduck": "3915f3f73328f0838497b2d5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3915f3f73328f0838497b2d5", "postgresql": "3915f3f73328f0838497b2d5", "snowflake": "f43dac38f34a1c0618a3f812", @@ -17314,8 +17314,8 @@ "databricks": "0dcd6a19b7dc6f6b6d4a5006", "duckdb": "0dcd6a19b7dc6f6b6d4a5006", "generic": "0dcd6a19b7dc6f6b6d4a5006", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0dcd6a19b7dc6f6b6d4a5006", +"motherduck": "0dcd6a19b7dc6f6b6d4a5006", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0dcd6a19b7dc6f6b6d4a5006", "postgresql": "0dcd6a19b7dc6f6b6d4a5006", "snowflake": "0dcd6a19b7dc6f6b6d4a5006", @@ -17331,8 +17331,8 @@ "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fe23594c781e79cbb0c8927a", +"motherduck": "fe23594c781e79cbb0c8927a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8a3f094f18fb4047f0febf59", "postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", @@ -17348,8 +17348,8 @@ "databricks": "d03c5e5797a8cbc0c6a81112", "duckdb": "d03c5e5797a8cbc0c6a81112", "generic": "d03c5e5797a8cbc0c6a81112", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d03c5e5797a8cbc0c6a81112", +"motherduck": "d03c5e5797a8cbc0c6a81112", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d03c5e5797a8cbc0c6a81112", "postgresql": "d03c5e5797a8cbc0c6a81112", "snowflake": "d03c5e5797a8cbc0c6a81112", @@ -17365,8 +17365,8 @@ "databricks": "6cffd1617edd6454241ee732", "duckdb": "6cffd1617edd6454241ee732", "generic": "6cffd1617edd6454241ee732", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6cffd1617edd6454241ee732", +"motherduck": "6cffd1617edd6454241ee732", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "6cffd1617edd6454241ee732", "postgresql": "6cffd1617edd6454241ee732", "snowflake": "6cffd1617edd6454241ee732", @@ -17382,8 +17382,8 @@ "databricks": "98d56a64e9a7ee905a58c95c", "duckdb": "815698ef0c2515c76f250086", "generic": "98d56a64e9a7ee905a58c95c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "815698ef0c2515c76f250086", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "815698ef0c2515c76f250086", "postgresql": "815698ef0c2515c76f250086", "snowflake": "822641f83fbdcddae47b409f", @@ -17399,8 +17399,8 @@ "databricks": "1275b5b0c52f69cf592c004b", "duckdb": "1275b5b0c52f69cf592c004b", "generic": "1275b5b0c52f69cf592c004b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1275b5b0c52f69cf592c004b", +"motherduck": "1275b5b0c52f69cf592c004b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1275b5b0c52f69cf592c004b", "postgresql": "1275b5b0c52f69cf592c004b", "snowflake": "1275b5b0c52f69cf592c004b", @@ -17416,8 +17416,8 @@ "databricks": "80d6c9c5f502ef25b9434309", "duckdb": "80d6c9c5f502ef25b9434309", "generic": "80d6c9c5f502ef25b9434309", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "80d6c9c5f502ef25b9434309", +"motherduck": "80d6c9c5f502ef25b9434309", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "80d6c9c5f502ef25b9434309", "postgresql": "80d6c9c5f502ef25b9434309", "snowflake": "80d6c9c5f502ef25b9434309", @@ -17433,8 +17433,8 @@ "databricks": "42f6a9fe8a8cc486c2c77ae1", "duckdb": "42f6a9fe8a8cc486c2c77ae1", "generic": "42f6a9fe8a8cc486c2c77ae1", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "42f6a9fe8a8cc486c2c77ae1", +"motherduck": "42f6a9fe8a8cc486c2c77ae1", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "42f6a9fe8a8cc486c2c77ae1", "postgresql": "42f6a9fe8a8cc486c2c77ae1", "snowflake": "42f6a9fe8a8cc486c2c77ae1", @@ -17450,8 +17450,8 @@ "databricks": "7894730ae1c06f4ea3920947", "duckdb": "7894730ae1c06f4ea3920947", "generic": "7894730ae1c06f4ea3920947", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "7894730ae1c06f4ea3920947", +"motherduck": "7894730ae1c06f4ea3920947", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7894730ae1c06f4ea3920947", "postgresql": "7894730ae1c06f4ea3920947", "snowflake": "7894730ae1c06f4ea3920947", @@ -17467,8 +17467,8 @@ "databricks": "3213ca7d92e0eff2f419451a", "duckdb": "a8f9cb9212d342eb9d2854d0", "generic": "3213ca7d92e0eff2f419451a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3213ca7d92e0eff2f419451a", +"motherduck": "a8f9cb9212d342eb9d2854d0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3213ca7d92e0eff2f419451a", "postgresql": "3213ca7d92e0eff2f419451a", "snowflake": "6dc518f5329c32ce13bfa506", @@ -17484,8 +17484,8 @@ "databricks": "9829fa9011a92bb968bb5996", "duckdb": "9829fa9011a92bb968bb5996", "generic": "9829fa9011a92bb968bb5996", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9829fa9011a92bb968bb5996", +"motherduck": "9829fa9011a92bb968bb5996", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "4e937b76a5c3e503c1fd6e22", "postgresql": "4e937b76a5c3e503c1fd6e22", "snowflake": "9829fa9011a92bb968bb5996", @@ -17501,8 +17501,8 @@ "databricks": "c50a316d091c821866e4bfba", "duckdb": "c50a316d091c821866e4bfba", "generic": "c50a316d091c821866e4bfba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -17518,8 +17518,8 @@ "databricks": "c50a316d091c821866e4bfba", "duckdb": "c50a316d091c821866e4bfba", "generic": "c50a316d091c821866e4bfba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -17535,8 +17535,8 @@ "databricks": "18db5afe2c1e80d1ee652b54", "duckdb": "18db5afe2c1e80d1ee652b54", "generic": "18db5afe2c1e80d1ee652b54", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "18db5afe2c1e80d1ee652b54", +"motherduck": "18db5afe2c1e80d1ee652b54", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "47d64a5994d42a2b577166ea", "postgresql": "47d64a5994d42a2b577166ea", "snowflake": "18db5afe2c1e80d1ee652b54", @@ -17552,8 +17552,8 @@ "databricks": "e6ffdece2977be55da73b59a", "duckdb": "c50a316d091c821866e4bfba", "generic": "e6ffdece2977be55da73b59a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e6ffdece2977be55da73b59a", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -17569,8 +17569,8 @@ "databricks": "19a1a80ffc06169f32694de8", "duckdb": "19a1a80ffc06169f32694de8", "generic": "19a1a80ffc06169f32694de8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "19a1a80ffc06169f32694de8", +"motherduck": "19a1a80ffc06169f32694de8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ab1f5f46e69284778bd66d58", "postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", @@ -17586,8 +17586,8 @@ "databricks": "8993aba016a0adde404608e3", "duckdb": "8993aba016a0adde404608e3", "generic": "8993aba016a0adde404608e3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8993aba016a0adde404608e3", +"motherduck": "8993aba016a0adde404608e3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8993aba016a0adde404608e3", "postgresql": "8993aba016a0adde404608e3", "snowflake": "8993aba016a0adde404608e3", @@ -17603,8 +17603,8 @@ "databricks": "837168a49b84e7ed918ea148", "duckdb": "837168a49b84e7ed918ea148", "generic": "837168a49b84e7ed918ea148", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "837168a49b84e7ed918ea148", +"motherduck": "837168a49b84e7ed918ea148", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c28e9468b9f7a7f4e67d5b30", "postgresql": "c28e9468b9f7a7f4e67d5b30", "snowflake": "837168a49b84e7ed918ea148", @@ -17620,8 +17620,8 @@ "databricks": "3915f3f73328f0838497b2d5", "duckdb": "3915f3f73328f0838497b2d5", "generic": "3915f3f73328f0838497b2d5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3915f3f73328f0838497b2d5", +"motherduck": "3915f3f73328f0838497b2d5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3915f3f73328f0838497b2d5", "postgresql": "3915f3f73328f0838497b2d5", "snowflake": "f43dac38f34a1c0618a3f812", @@ -17637,8 +17637,8 @@ "databricks": "d61c83ef8bfdf043fa672cb5", "duckdb": "d61c83ef8bfdf043fa672cb5", "generic": "d61c83ef8bfdf043fa672cb5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d61c83ef8bfdf043fa672cb5", +"motherduck": "d61c83ef8bfdf043fa672cb5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d61c83ef8bfdf043fa672cb5", "postgresql": "d61c83ef8bfdf043fa672cb5", "snowflake": "97a20344d811fb77c7690863", @@ -17654,8 +17654,8 @@ "databricks": "2bfa352725f9d8a91250f75e", "duckdb": "2bfa352725f9d8a91250f75e", "generic": "2bfa352725f9d8a91250f75e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2bfa352725f9d8a91250f75e", +"motherduck": "2bfa352725f9d8a91250f75e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2bfa352725f9d8a91250f75e", "postgresql": "2bfa352725f9d8a91250f75e", "snowflake": "2bfa352725f9d8a91250f75e", @@ -17671,8 +17671,8 @@ "databricks": "08736485547ba339930697e9", "duckdb": "08736485547ba339930697e9", "generic": "08736485547ba339930697e9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "08736485547ba339930697e9", +"motherduck": "08736485547ba339930697e9", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "eecd452b4f5f3f4133d0bd0a", "postgresql": "eecd452b4f5f3f4133d0bd0a", "snowflake": "08736485547ba339930697e9", @@ -17688,8 +17688,8 @@ "databricks": "c6889f2965db73017be6f6e7", "duckdb": "c6889f2965db73017be6f6e7", "generic": "c6889f2965db73017be6f6e7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c6889f2965db73017be6f6e7", +"motherduck": "c6889f2965db73017be6f6e7", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c6889f2965db73017be6f6e7", "postgresql": "c6889f2965db73017be6f6e7", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -17705,8 +17705,8 @@ "databricks": "461923a0d34defd0c726d6e5", "duckdb": "461923a0d34defd0c726d6e5", "generic": "461923a0d34defd0c726d6e5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "461923a0d34defd0c726d6e5", +"motherduck": "461923a0d34defd0c726d6e5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "461923a0d34defd0c726d6e5", "postgresql": "461923a0d34defd0c726d6e5", "snowflake": "461923a0d34defd0c726d6e5", @@ -17722,8 +17722,8 @@ "databricks": "624eb95d280fa0653ec73f7f", "duckdb": "624eb95d280fa0653ec73f7f", "generic": "624eb95d280fa0653ec73f7f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "624eb95d280fa0653ec73f7f", +"motherduck": "624eb95d280fa0653ec73f7f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9dd376ce83bd3444e52fdb88", "postgresql": "9dd376ce83bd3444e52fdb88", "snowflake": "624eb95d280fa0653ec73f7f", @@ -17739,8 +17739,8 @@ "databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", "generic": "fb4b6f5101f973d16ea8d1f3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fb4b6f5101f973d16ea8d1f3", +"motherduck": "70fb4728c3adcf6dbe4975d5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", @@ -17756,8 +17756,8 @@ "databricks": "bce60580ef3a1dc4ce569fff", "duckdb": "bce60580ef3a1dc4ce569fff", "generic": "bce60580ef3a1dc4ce569fff", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "bce60580ef3a1dc4ce569fff", +"motherduck": "bce60580ef3a1dc4ce569fff", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "eee236ea1427bdddbaf088ac", "postgresql": "eee236ea1427bdddbaf088ac", "snowflake": "bce60580ef3a1dc4ce569fff", @@ -17773,8 +17773,8 @@ "databricks": "a104f9b34b3d9d798095a6b4", "duckdb": "a104f9b34b3d9d798095a6b4", "generic": "a104f9b34b3d9d798095a6b4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a104f9b34b3d9d798095a6b4", +"motherduck": "a104f9b34b3d9d798095a6b4", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a104f9b34b3d9d798095a6b4", "postgresql": "a104f9b34b3d9d798095a6b4", "snowflake": "a104f9b34b3d9d798095a6b4", @@ -17790,8 +17790,8 @@ "databricks": "50b29536a9929d11dbcb949a", "duckdb": "858855737713d0cff353f8ac", "generic": "50b29536a9929d11dbcb949a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "50b29536a9929d11dbcb949a", +"motherduck": "858855737713d0cff353f8ac", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "74a30f6aa9def0b58afc616c", "postgresql": "74a30f6aa9def0b58afc616c", "snowflake": "858855737713d0cff353f8ac", @@ -17807,8 +17807,8 @@ "databricks": "62368fc15cefd5807807b209", "duckdb": "62368fc15cefd5807807b209", "generic": "62368fc15cefd5807807b209", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "62368fc15cefd5807807b209", +"motherduck": "62368fc15cefd5807807b209", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "62368fc15cefd5807807b209", "postgresql": "62368fc15cefd5807807b209", "snowflake": "62368fc15cefd5807807b209", @@ -17824,8 +17824,8 @@ "databricks": "e57ac1541fb51b362cd5b470", "duckdb": "e57ac1541fb51b362cd5b470", "generic": "e57ac1541fb51b362cd5b470", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e57ac1541fb51b362cd5b470", +"motherduck": "e57ac1541fb51b362cd5b470", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e57ac1541fb51b362cd5b470", "postgresql": "e57ac1541fb51b362cd5b470", "snowflake": "4b356542044f1267fcfc487a", @@ -17841,8 +17841,8 @@ "databricks": "4ce8ae479c93f9ff48189fc3", "duckdb": "4ce8ae479c93f9ff48189fc3", "generic": "4ce8ae479c93f9ff48189fc3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4ce8ae479c93f9ff48189fc3", +"motherduck": "4ce8ae479c93f9ff48189fc3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "4ce8ae479c93f9ff48189fc3", "postgresql": "4ce8ae479c93f9ff48189fc3", "snowflake": "4ce8ae479c93f9ff48189fc3", @@ -17858,8 +17858,8 @@ "databricks": "fb5907c56dd752ac5f1dbd95", "duckdb": "fb5907c56dd752ac5f1dbd95", "generic": "fb5907c56dd752ac5f1dbd95", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fb5907c56dd752ac5f1dbd95", +"motherduck": "fb5907c56dd752ac5f1dbd95", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "fb5907c56dd752ac5f1dbd95", "postgresql": "fb5907c56dd752ac5f1dbd95", "snowflake": "fb5907c56dd752ac5f1dbd95", @@ -17875,8 +17875,8 @@ "databricks": "aab5b077409069695e7d847d", "duckdb": "aab5b077409069695e7d847d", "generic": "aab5b077409069695e7d847d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "aab5b077409069695e7d847d", +"motherduck": "aab5b077409069695e7d847d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "aab5b077409069695e7d847d", "postgresql": "aab5b077409069695e7d847d", "snowflake": "aab5b077409069695e7d847d", @@ -17892,8 +17892,8 @@ "databricks": "a52676af0c6b750acc33f021", "duckdb": "d81d8c19074af0e39239164d", "generic": "a52676af0c6b750acc33f021", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a52676af0c6b750acc33f021", +"motherduck": "d81d8c19074af0e39239164d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a52676af0c6b750acc33f021", "postgresql": "a52676af0c6b750acc33f021", "snowflake": "1fec266134381c760a254d16", @@ -17909,8 +17909,8 @@ "databricks": "de89632e277dbc8c97115419", "duckdb": "de89632e277dbc8c97115419", "generic": "de89632e277dbc8c97115419", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "de89632e277dbc8c97115419", +"motherduck": "de89632e277dbc8c97115419", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "de89632e277dbc8c97115419", "postgresql": "de89632e277dbc8c97115419", "snowflake": "4b356542044f1267fcfc487a", @@ -17926,8 +17926,8 @@ "databricks": "b561884132dbe7e42b719110", "duckdb": "b561884132dbe7e42b719110", "generic": "b561884132dbe7e42b719110", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b561884132dbe7e42b719110", +"motherduck": "b561884132dbe7e42b719110", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5aa51d84ac5e6c7a9306f348", "postgresql": "5aa51d84ac5e6c7a9306f348", "snowflake": "b561884132dbe7e42b719110", @@ -17943,8 +17943,8 @@ "databricks": "e3bc1b8bad22d8104417b00f", "duckdb": "111af35848fe500ec2ae54b2", "generic": "111af35848fe500ec2ae54b2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e3bc1b8bad22d8104417b00f", +"motherduck": "111af35848fe500ec2ae54b2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "111af35848fe500ec2ae54b2", "postgresql": "111af35848fe500ec2ae54b2", "snowflake": "e3bc1b8bad22d8104417b00f", @@ -17960,8 +17960,8 @@ "databricks": "c9b810639795a7bfab91b98d", "duckdb": "a428302e4848ffe877284c84", "generic": "c9b810639795a7bfab91b98d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c9b810639795a7bfab91b98d", +"motherduck": "a428302e4848ffe877284c84", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a428302e4848ffe877284c84", "postgresql": "a428302e4848ffe877284c84", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -17977,8 +17977,8 @@ "databricks": "47d74d1b45fedd02ec465220", "duckdb": "47d74d1b45fedd02ec465220", "generic": "47d74d1b45fedd02ec465220", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "47d74d1b45fedd02ec465220", +"motherduck": "47d74d1b45fedd02ec465220", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "47d74d1b45fedd02ec465220", "postgresql": "47d74d1b45fedd02ec465220", "snowflake": "47d74d1b45fedd02ec465220", @@ -17994,8 +17994,8 @@ "databricks": "60a23b7dcae6c79f4de83068", "duckdb": "60a23b7dcae6c79f4de83068", "generic": "60a23b7dcae6c79f4de83068", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "60a23b7dcae6c79f4de83068", +"motherduck": "60a23b7dcae6c79f4de83068", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "60a23b7dcae6c79f4de83068", "postgresql": "60a23b7dcae6c79f4de83068", "snowflake": "60a23b7dcae6c79f4de83068", @@ -18011,8 +18011,8 @@ "databricks": "072b6126112a6217e028fc8f", "duckdb": "1879bc874141d9160106e020", "generic": "072b6126112a6217e028fc8f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "072b6126112a6217e028fc8f", +"motherduck": "1879bc874141d9160106e020", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c513dcc8ed8dfd8935374a47", "postgresql": "c513dcc8ed8dfd8935374a47", "snowflake": "c513dcc8ed8dfd8935374a47", @@ -18028,8 +18028,8 @@ "databricks": "2705f19942a4194a72576911", "duckdb": "2705f19942a4194a72576911", "generic": "2705f19942a4194a72576911", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2705f19942a4194a72576911", +"motherduck": "2705f19942a4194a72576911", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5226d7e2a62c7650a87d6e84", "postgresql": "5226d7e2a62c7650a87d6e84", "snowflake": "2705f19942a4194a72576911", @@ -18045,8 +18045,8 @@ "databricks": "9b1399d4f1923837359745f8", "duckdb": "9b1399d4f1923837359745f8", "generic": "9b1399d4f1923837359745f8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9b1399d4f1923837359745f8", +"motherduck": "9b1399d4f1923837359745f8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9b1399d4f1923837359745f8", "postgresql": "9b1399d4f1923837359745f8", "snowflake": "9b1399d4f1923837359745f8", @@ -18062,8 +18062,8 @@ "databricks": "3866c29c7e0df841d464b98c", "duckdb": "83f3a2696c58aa8d709a227a", "generic": "3866c29c7e0df841d464b98c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3866c29c7e0df841d464b98c", +"motherduck": "83f3a2696c58aa8d709a227a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3866c29c7e0df841d464b98c", "postgresql": "3866c29c7e0df841d464b98c", "snowflake": "06a9af03923de91e5e27860a", @@ -18079,8 +18079,8 @@ "databricks": "5b121b9b6ea0450c8d33ee34", "duckdb": "d229e289e63364bf0d56e920", "generic": "5b121b9b6ea0450c8d33ee34", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5b121b9b6ea0450c8d33ee34", +"motherduck": "d229e289e63364bf0d56e920", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5b121b9b6ea0450c8d33ee34", "postgresql": "5b121b9b6ea0450c8d33ee34", "snowflake": "d90e8915a295b2cd5e690bd1", @@ -18096,8 +18096,8 @@ "databricks": "a3acd7ba35e81a800f967bd3", "duckdb": "42d4a23d9a52e79d0c3458d3", "generic": "a3acd7ba35e81a800f967bd3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a3acd7ba35e81a800f967bd3", +"motherduck": "42d4a23d9a52e79d0c3458d3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "42d4a23d9a52e79d0c3458d3", "postgresql": "42d4a23d9a52e79d0c3458d3", "snowflake": "a3acd7ba35e81a800f967bd3", @@ -18113,8 +18113,8 @@ "databricks": "c6106f30f23f392e17eadef4", "duckdb": "c6106f30f23f392e17eadef4", "generic": "c6106f30f23f392e17eadef4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c6106f30f23f392e17eadef4", +"motherduck": "c6106f30f23f392e17eadef4", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f43dac38f34a1c0618a3f812", "postgresql": "f43dac38f34a1c0618a3f812", "snowflake": "c6106f30f23f392e17eadef4", @@ -18130,8 +18130,8 @@ "databricks": "804fec6876d0b43935790142", "duckdb": "804fec6876d0b43935790142", "generic": "804fec6876d0b43935790142", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "804fec6876d0b43935790142", +"motherduck": "804fec6876d0b43935790142", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "191fc59c754706512337edc1", "postgresql": "191fc59c754706512337edc1", "snowflake": "804fec6876d0b43935790142", @@ -18147,8 +18147,8 @@ "databricks": "79278352c17c9de0f5a28af5", "duckdb": "79278352c17c9de0f5a28af5", "generic": "79278352c17c9de0f5a28af5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "79278352c17c9de0f5a28af5", +"motherduck": "79278352c17c9de0f5a28af5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "79278352c17c9de0f5a28af5", "postgresql": "79278352c17c9de0f5a28af5", "snowflake": "97a20344d811fb77c7690863", @@ -18164,8 +18164,8 @@ "databricks": "7b87db889221e05e13ec3845", "duckdb": "194c385aa00f52f45426748f", "generic": "7b87db889221e05e13ec3845", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "7b87db889221e05e13ec3845", +"motherduck": "194c385aa00f52f45426748f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "07fffe8743dfd4e88e6b3f18", "postgresql": "07fffe8743dfd4e88e6b3f18", "snowflake": "7b87db889221e05e13ec3845", @@ -18181,8 +18181,8 @@ "databricks": "19a1a80ffc06169f32694de8", "duckdb": "19a1a80ffc06169f32694de8", "generic": "19a1a80ffc06169f32694de8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "19a1a80ffc06169f32694de8", +"motherduck": "19a1a80ffc06169f32694de8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ab1f5f46e69284778bd66d58", "postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", @@ -18198,8 +18198,8 @@ "databricks": "eb72e9a8e16bab9ee01146ee", "duckdb": "c69a366e715a81bca598d475", "generic": "c69a366e715a81bca598d475", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c69a366e715a81bca598d475", +"motherduck": "c69a366e715a81bca598d475", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c69a366e715a81bca598d475", "postgresql": "c69a366e715a81bca598d475", "snowflake": "f41d94b0bed9910074641da6", @@ -18215,8 +18215,8 @@ "databricks": "558da38ec4d9d5d6c72f60cf", "duckdb": "558da38ec4d9d5d6c72f60cf", "generic": "558da38ec4d9d5d6c72f60cf", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "558da38ec4d9d5d6c72f60cf", +"motherduck": "558da38ec4d9d5d6c72f60cf", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "558da38ec4d9d5d6c72f60cf", "postgresql": "558da38ec4d9d5d6c72f60cf", "snowflake": "558da38ec4d9d5d6c72f60cf", @@ -18232,8 +18232,8 @@ "databricks": "b210f8eb26c37b422f786c6b", "duckdb": "b210f8eb26c37b422f786c6b", "generic": "b210f8eb26c37b422f786c6b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b210f8eb26c37b422f786c6b", +"motherduck": "b210f8eb26c37b422f786c6b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b210f8eb26c37b422f786c6b", "postgresql": "b210f8eb26c37b422f786c6b", "snowflake": "b210f8eb26c37b422f786c6b", @@ -18249,8 +18249,8 @@ "databricks": "279ea7f6f6a3910de04b795b", "duckdb": "279ea7f6f6a3910de04b795b", "generic": "279ea7f6f6a3910de04b795b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "279ea7f6f6a3910de04b795b", +"motherduck": "279ea7f6f6a3910de04b795b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "bc520aebd5872e40030ec7d9", "postgresql": "bc520aebd5872e40030ec7d9", "snowflake": "279ea7f6f6a3910de04b795b", @@ -18266,8 +18266,8 @@ "databricks": "92e8caae709d951bb8ed59cb", "duckdb": "92e8caae709d951bb8ed59cb", "generic": "92e8caae709d951bb8ed59cb", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "92e8caae709d951bb8ed59cb", +"motherduck": "92e8caae709d951bb8ed59cb", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "92e8caae709d951bb8ed59cb", "postgresql": "92e8caae709d951bb8ed59cb", "snowflake": "92e8caae709d951bb8ed59cb", @@ -18283,8 +18283,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1eb7fb4e8cf75773c5573562", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1eb7fb4e8cf75773c5573562", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -18300,8 +18300,8 @@ "databricks": "a686bb6db17ab079cfc79224", "duckdb": "a686bb6db17ab079cfc79224", "generic": "a686bb6db17ab079cfc79224", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a686bb6db17ab079cfc79224", +"motherduck": "a686bb6db17ab079cfc79224", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "74a30f6aa9def0b58afc616c", "postgresql": "74a30f6aa9def0b58afc616c", "snowflake": "1096c3c8687c8e8eb538ff74", @@ -18317,8 +18317,8 @@ "databricks": "56f06885daf94b4fcc0a624b", "duckdb": "56f06885daf94b4fcc0a624b", "generic": "56f06885daf94b4fcc0a624b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "56f06885daf94b4fcc0a624b", +"motherduck": "56f06885daf94b4fcc0a624b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d8c1f10bbe9d46ecb6ee0bc7", "postgresql": "d8c1f10bbe9d46ecb6ee0bc7", "snowflake": "56f06885daf94b4fcc0a624b", @@ -18334,8 +18334,8 @@ "databricks": "d61c83ef8bfdf043fa672cb5", "duckdb": "d61c83ef8bfdf043fa672cb5", "generic": "d61c83ef8bfdf043fa672cb5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d61c83ef8bfdf043fa672cb5", +"motherduck": "d61c83ef8bfdf043fa672cb5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d61c83ef8bfdf043fa672cb5", "postgresql": "d61c83ef8bfdf043fa672cb5", "snowflake": "97a20344d811fb77c7690863", @@ -18351,8 +18351,8 @@ "databricks": "0703bf71a54942bdaa28406e", "duckdb": "0703bf71a54942bdaa28406e", "generic": "0703bf71a54942bdaa28406e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0703bf71a54942bdaa28406e", +"motherduck": "0703bf71a54942bdaa28406e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0703bf71a54942bdaa28406e", "postgresql": "0703bf71a54942bdaa28406e", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -18368,8 +18368,8 @@ "databricks": "e57ac1541fb51b362cd5b470", "duckdb": "396b2ce32efb5365b393c230", "generic": "e57ac1541fb51b362cd5b470", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e57ac1541fb51b362cd5b470", +"motherduck": "396b2ce32efb5365b393c230", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "396b2ce32efb5365b393c230", "postgresql": "396b2ce32efb5365b393c230", "snowflake": "4b356542044f1267fcfc487a", @@ -18385,8 +18385,8 @@ "databricks": "d61c83ef8bfdf043fa672cb5", "duckdb": "305aaf0b95cee629dd46380c", "generic": "d61c83ef8bfdf043fa672cb5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d61c83ef8bfdf043fa672cb5", +"motherduck": "305aaf0b95cee629dd46380c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d61c83ef8bfdf043fa672cb5", "postgresql": "d61c83ef8bfdf043fa672cb5", "snowflake": "97a20344d811fb77c7690863", @@ -18402,8 +18402,8 @@ "databricks": "669084d1ea704ae5b8ee05c2", "duckdb": "669084d1ea704ae5b8ee05c2", "generic": "669084d1ea704ae5b8ee05c2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "669084d1ea704ae5b8ee05c2", +"motherduck": "669084d1ea704ae5b8ee05c2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "93c1b5b5960e26479baf88d0", "postgresql": "93c1b5b5960e26479baf88d0", "snowflake": "669084d1ea704ae5b8ee05c2", @@ -18419,8 +18419,8 @@ "databricks": "d2c97d221599d038d9875873", "duckdb": "f7fca17b74cc50ae3de8c826", "generic": "d2c97d221599d038d9875873", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d2c97d221599d038d9875873", +"motherduck": "f7fca17b74cc50ae3de8c826", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "55155766823b6a3a5e882fc2", "postgresql": "55155766823b6a3a5e882fc2", "snowflake": "d2c97d221599d038d9875873", @@ -18436,8 +18436,8 @@ "databricks": "d1dc9a140dbe2eb1874c6ad7", "duckdb": "d1dc9a140dbe2eb1874c6ad7", "generic": "d1dc9a140dbe2eb1874c6ad7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d1dc9a140dbe2eb1874c6ad7", +"motherduck": "d1dc9a140dbe2eb1874c6ad7", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d1dc9a140dbe2eb1874c6ad7", "postgresql": "d1dc9a140dbe2eb1874c6ad7", "snowflake": "d1dc9a140dbe2eb1874c6ad7", @@ -18453,8 +18453,8 @@ "databricks": "4c78bdde0b2dfd77ab71c9e8", "duckdb": "4c78bdde0b2dfd77ab71c9e8", "generic": "4c78bdde0b2dfd77ab71c9e8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4c78bdde0b2dfd77ab71c9e8", +"motherduck": "4c78bdde0b2dfd77ab71c9e8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e802515d969e770f686478b5", "postgresql": "e802515d969e770f686478b5", "snowflake": "4c78bdde0b2dfd77ab71c9e8", @@ -18470,8 +18470,8 @@ "databricks": "3f71ab5f3c1c040da96f2cec", "duckdb": "3f71ab5f3c1c040da96f2cec", "generic": "3f71ab5f3c1c040da96f2cec", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3f71ab5f3c1c040da96f2cec", +"motherduck": "3f71ab5f3c1c040da96f2cec", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3f71ab5f3c1c040da96f2cec", "postgresql": "3f71ab5f3c1c040da96f2cec", "snowflake": "3f71ab5f3c1c040da96f2cec", @@ -18487,8 +18487,8 @@ "databricks": "8a3f094f18fb4047f0febf59", "duckdb": "8a3f094f18fb4047f0febf59", "generic": "8a3f094f18fb4047f0febf59", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5c28019dce48a74708c90a76", +"motherduck": "8a3f094f18fb4047f0febf59", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8a3f094f18fb4047f0febf59", "postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "850deea990b52b82b36a9848", @@ -18504,8 +18504,8 @@ "databricks": "f774d5e8feadbf11edaa26b1", "duckdb": "f46cf4b4e131aa122b5b33d6", "generic": "f774d5e8feadbf11edaa26b1", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f774d5e8feadbf11edaa26b1", +"motherduck": "f46cf4b4e131aa122b5b33d6", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f46cf4b4e131aa122b5b33d6", "postgresql": "f46cf4b4e131aa122b5b33d6", "snowflake": "f46cf4b4e131aa122b5b33d6", @@ -18521,8 +18521,8 @@ "databricks": "f43dac38f34a1c0618a3f812", "duckdb": "f43dac38f34a1c0618a3f812", "generic": "f43dac38f34a1c0618a3f812", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f43dac38f34a1c0618a3f812", +"motherduck": "f43dac38f34a1c0618a3f812", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f43dac38f34a1c0618a3f812", "postgresql": "f43dac38f34a1c0618a3f812", "snowflake": "f43dac38f34a1c0618a3f812", @@ -18538,8 +18538,8 @@ "databricks": "6eb49ceb88fcb2297831ca05", "duckdb": "6eb49ceb88fcb2297831ca05", "generic": "6eb49ceb88fcb2297831ca05", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6eb49ceb88fcb2297831ca05", +"motherduck": "6eb49ceb88fcb2297831ca05", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "623e22ea8921c9e57a15475b", "postgresql": "623e22ea8921c9e57a15475b", "snowflake": "6eb49ceb88fcb2297831ca05", @@ -18555,8 +18555,8 @@ "databricks": "853b2c0ad6ea4565b1e301e8", "duckdb": "853b2c0ad6ea4565b1e301e8", "generic": "853b2c0ad6ea4565b1e301e8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "853b2c0ad6ea4565b1e301e8", +"motherduck": "853b2c0ad6ea4565b1e301e8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "853b2c0ad6ea4565b1e301e8", "postgresql": "853b2c0ad6ea4565b1e301e8", "snowflake": "853b2c0ad6ea4565b1e301e8", @@ -18572,8 +18572,8 @@ "databricks": "e2879cf4770e7f48d2dd704a", "duckdb": "e2879cf4770e7f48d2dd704a", "generic": "e2879cf4770e7f48d2dd704a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e2879cf4770e7f48d2dd704a", +"motherduck": "e2879cf4770e7f48d2dd704a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2d4aea8dec5521e258596f3d", "postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", @@ -18589,8 +18589,8 @@ "databricks": "5865b96f5136d463181f63e4", "duckdb": "1558a2a87f510cca4baaad5b", "generic": "5865b96f5136d463181f63e4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5865b96f5136d463181f63e4", +"motherduck": "1558a2a87f510cca4baaad5b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1558a2a87f510cca4baaad5b", "postgresql": "1558a2a87f510cca4baaad5b", "snowflake": "67b5874a2c569226c490211f", @@ -18606,8 +18606,8 @@ "databricks": "8441d00030df06ad6a5534d3", "duckdb": "8441d00030df06ad6a5534d3", "generic": "8441d00030df06ad6a5534d3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8441d00030df06ad6a5534d3", +"motherduck": "8441d00030df06ad6a5534d3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8441d00030df06ad6a5534d3", "postgresql": "8441d00030df06ad6a5534d3", "snowflake": "97a20344d811fb77c7690863", @@ -18623,8 +18623,8 @@ "databricks": "f43dac38f34a1c0618a3f812", "duckdb": "f43dac38f34a1c0618a3f812", "generic": "f43dac38f34a1c0618a3f812", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f43dac38f34a1c0618a3f812", +"motherduck": "f43dac38f34a1c0618a3f812", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f43dac38f34a1c0618a3f812", "postgresql": "f43dac38f34a1c0618a3f812", "snowflake": "f43dac38f34a1c0618a3f812", @@ -18640,8 +18640,8 @@ "databricks": "c50a316d091c821866e4bfba", "duckdb": "c50a316d091c821866e4bfba", "generic": "c50a316d091c821866e4bfba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -18657,8 +18657,8 @@ "databricks": "d043825e95555167dc033b5f", "duckdb": "d043825e95555167dc033b5f", "generic": "d043825e95555167dc033b5f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d043825e95555167dc033b5f", +"motherduck": "d043825e95555167dc033b5f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d043825e95555167dc033b5f", "postgresql": "d043825e95555167dc033b5f", "snowflake": "d043825e95555167dc033b5f", @@ -18674,8 +18674,8 @@ "databricks": "84e86bec7646b6ad19ceef59", "duckdb": "6a2162b5abe5a73734c47041", "generic": "6a2162b5abe5a73734c47041", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6a2162b5abe5a73734c47041", +"motherduck": "6a2162b5abe5a73734c47041", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "6a2162b5abe5a73734c47041", "postgresql": "6a2162b5abe5a73734c47041", "snowflake": "16d902e899040d0c0c9dcf58", @@ -18691,8 +18691,8 @@ "databricks": "56245289768b0fb155df9ffd", "duckdb": "56245289768b0fb155df9ffd", "generic": "56245289768b0fb155df9ffd", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "56245289768b0fb155df9ffd", +"motherduck": "56245289768b0fb155df9ffd", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "56245289768b0fb155df9ffd", "postgresql": "56245289768b0fb155df9ffd", "snowflake": "56245289768b0fb155df9ffd", @@ -18708,8 +18708,8 @@ "databricks": "5abf4ec799a2ec32fb10da7e", "duckdb": "5abf4ec799a2ec32fb10da7e", "generic": "5abf4ec799a2ec32fb10da7e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5abf4ec799a2ec32fb10da7e", +"motherduck": "5abf4ec799a2ec32fb10da7e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5abf4ec799a2ec32fb10da7e", "postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "a5656d70e208254d1d3e9b4f", @@ -18725,8 +18725,8 @@ "databricks": "362b5422259610d45387d0ab", "duckdb": "362b5422259610d45387d0ab", "generic": "362b5422259610d45387d0ab", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "362b5422259610d45387d0ab", +"motherduck": "362b5422259610d45387d0ab", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "362b5422259610d45387d0ab", "postgresql": "362b5422259610d45387d0ab", "snowflake": "362b5422259610d45387d0ab", @@ -18742,8 +18742,8 @@ "databricks": "b2c9b6ebca6c097f3caa0aa8", "duckdb": "b2c9b6ebca6c097f3caa0aa8", "generic": "b2c9b6ebca6c097f3caa0aa8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b2c9b6ebca6c097f3caa0aa8", +"motherduck": "b2c9b6ebca6c097f3caa0aa8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b2c9b6ebca6c097f3caa0aa8", "postgresql": "b2c9b6ebca6c097f3caa0aa8", "snowflake": "b2c9b6ebca6c097f3caa0aa8", @@ -18759,8 +18759,8 @@ "databricks": "b2c07944c578e7e9c9297bd8", "duckdb": "25f9faed41d5d23ca3fa8175", "generic": "25f9faed41d5d23ca3fa8175", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "25f9faed41d5d23ca3fa8175", +"motherduck": "25f9faed41d5d23ca3fa8175", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "25f9faed41d5d23ca3fa8175", "postgresql": "25f9faed41d5d23ca3fa8175", "snowflake": "fc6c5aa7025d9a737c71c606", @@ -18776,8 +18776,8 @@ "databricks": "1e75e5227bea0ae294ac7af1", "duckdb": "19bdd92ea0cc5fb028657510", "generic": "78b2cf275c640af54feeca31", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "19bdd92ea0cc5fb028657510", +"motherduck": "19bdd92ea0cc5fb028657510", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "78b2cf275c640af54feeca31", "postgresql": "78b2cf275c640af54feeca31", "snowflake": "8ff614b7306c0edbe36ce00b", @@ -18793,8 +18793,8 @@ "databricks": "f7fca17b74cc50ae3de8c826", "duckdb": "f7fca17b74cc50ae3de8c826", "generic": "f7fca17b74cc50ae3de8c826", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f7fca17b74cc50ae3de8c826", +"motherduck": "f7fca17b74cc50ae3de8c826", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f7fca17b74cc50ae3de8c826", "postgresql": "f7fca17b74cc50ae3de8c826", "snowflake": "55155766823b6a3a5e882fc2", @@ -18810,8 +18810,8 @@ "databricks": "e7a76004da6461a939ce5fef", "duckdb": "e7a76004da6461a939ce5fef", "generic": "e7a76004da6461a939ce5fef", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e7a76004da6461a939ce5fef", +"motherduck": "e7a76004da6461a939ce5fef", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e7a76004da6461a939ce5fef", "postgresql": "e7a76004da6461a939ce5fef", "snowflake": "e7a76004da6461a939ce5fef", @@ -18827,8 +18827,8 @@ "databricks": "60a23b7dcae6c79f4de83068", "duckdb": "60a23b7dcae6c79f4de83068", "generic": "60a23b7dcae6c79f4de83068", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "60a23b7dcae6c79f4de83068", +"motherduck": "60a23b7dcae6c79f4de83068", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "60a23b7dcae6c79f4de83068", "postgresql": "60a23b7dcae6c79f4de83068", "snowflake": "60a23b7dcae6c79f4de83068", @@ -18844,8 +18844,8 @@ "databricks": "a47af75c6582f749e1d2b2e7", "duckdb": "a47af75c6582f749e1d2b2e7", "generic": "a47af75c6582f749e1d2b2e7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a47af75c6582f749e1d2b2e7", +"motherduck": "a47af75c6582f749e1d2b2e7", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a47af75c6582f749e1d2b2e7", "postgresql": "a47af75c6582f749e1d2b2e7", "snowflake": "a47af75c6582f749e1d2b2e7", @@ -18861,8 +18861,8 @@ "databricks": "d61c83ef8bfdf043fa672cb5", "duckdb": "d61c83ef8bfdf043fa672cb5", "generic": "d61c83ef8bfdf043fa672cb5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d61c83ef8bfdf043fa672cb5", +"motherduck": "d61c83ef8bfdf043fa672cb5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d61c83ef8bfdf043fa672cb5", "postgresql": "d61c83ef8bfdf043fa672cb5", "snowflake": "97a20344d811fb77c7690863", @@ -18878,8 +18878,8 @@ "databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", "generic": "fb4b6f5101f973d16ea8d1f3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fb4b6f5101f973d16ea8d1f3", +"motherduck": "70fb4728c3adcf6dbe4975d5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", @@ -18895,8 +18895,8 @@ "databricks": "e1c74d96ac578f9323bf1bbd", "duckdb": "e1c74d96ac578f9323bf1bbd", "generic": "e1c74d96ac578f9323bf1bbd", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e1c74d96ac578f9323bf1bbd", +"motherduck": "e1c74d96ac578f9323bf1bbd", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e1c74d96ac578f9323bf1bbd", "postgresql": "e1c74d96ac578f9323bf1bbd", "snowflake": "e1c74d96ac578f9323bf1bbd", @@ -18912,8 +18912,8 @@ "databricks": "200c00626c910f6fd6177bb5", "duckdb": "200c00626c910f6fd6177bb5", "generic": "200c00626c910f6fd6177bb5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "200c00626c910f6fd6177bb5", +"motherduck": "200c00626c910f6fd6177bb5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "200c00626c910f6fd6177bb5", "postgresql": "200c00626c910f6fd6177bb5", "snowflake": "200c00626c910f6fd6177bb5", @@ -18929,8 +18929,8 @@ "databricks": "c50a316d091c821866e4bfba", "duckdb": "c50a316d091c821866e4bfba", "generic": "c50a316d091c821866e4bfba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -18946,8 +18946,8 @@ "databricks": "fe0bedb7dda0e9247a51fdee", "duckdb": "fe0bedb7dda0e9247a51fdee", "generic": "fe0bedb7dda0e9247a51fdee", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fe0bedb7dda0e9247a51fdee", +"motherduck": "fe0bedb7dda0e9247a51fdee", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "fe0bedb7dda0e9247a51fdee", "postgresql": "fe0bedb7dda0e9247a51fdee", "snowflake": "fe0bedb7dda0e9247a51fdee", @@ -18963,8 +18963,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1eb7fb4e8cf75773c5573562", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1eb7fb4e8cf75773c5573562", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -18980,8 +18980,8 @@ "databricks": "c6889f2965db73017be6f6e7", "duckdb": "c6889f2965db73017be6f6e7", "generic": "c6889f2965db73017be6f6e7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c6889f2965db73017be6f6e7", +"motherduck": "c6889f2965db73017be6f6e7", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c6889f2965db73017be6f6e7", "postgresql": "c6889f2965db73017be6f6e7", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -18997,8 +18997,8 @@ "databricks": "e3bc1b8bad22d8104417b00f", "duckdb": "aed0419e7f2ce7c0a8892248", "generic": "aed0419e7f2ce7c0a8892248", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "aed0419e7f2ce7c0a8892248", +"motherduck": "aed0419e7f2ce7c0a8892248", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "111af35848fe500ec2ae54b2", "postgresql": "111af35848fe500ec2ae54b2", "snowflake": "aed0419e7f2ce7c0a8892248", @@ -19014,8 +19014,8 @@ "databricks": "55a086bc8305ad303017ae10", "duckdb": "55a086bc8305ad303017ae10", "generic": "55a086bc8305ad303017ae10", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "55a086bc8305ad303017ae10", +"motherduck": "55a086bc8305ad303017ae10", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "55a086bc8305ad303017ae10", "postgresql": "55a086bc8305ad303017ae10", "snowflake": "55a086bc8305ad303017ae10", @@ -19031,8 +19031,8 @@ "databricks": "d08ef9483feed34fc0ee4319", "duckdb": "f7598fbf399afabe1e01a358", "generic": "d08ef9483feed34fc0ee4319", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d08ef9483feed34fc0ee4319", +"motherduck": "f7598fbf399afabe1e01a358", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f7598fbf399afabe1e01a358", "postgresql": "f7598fbf399afabe1e01a358", "snowflake": "d08ef9483feed34fc0ee4319", @@ -19048,8 +19048,8 @@ "databricks": "d3b6a6835220f9cedfa363c7", "duckdb": "d3b6a6835220f9cedfa363c7", "generic": "d3b6a6835220f9cedfa363c7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d3b6a6835220f9cedfa363c7", +"motherduck": "d3b6a6835220f9cedfa363c7", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d3b6a6835220f9cedfa363c7", "postgresql": "d3b6a6835220f9cedfa363c7", "snowflake": "d3b6a6835220f9cedfa363c7", @@ -19065,8 +19065,8 @@ "databricks": "3d23f811a03db519b598c815", "duckdb": "3d23f811a03db519b598c815", "generic": "3d23f811a03db519b598c815", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3d23f811a03db519b598c815", +"motherduck": "3d23f811a03db519b598c815", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3d23f811a03db519b598c815", "postgresql": "3d23f811a03db519b598c815", "snowflake": "3d23f811a03db519b598c815", @@ -19082,8 +19082,8 @@ "databricks": "f816d7b3b97815f308346e20", "duckdb": "f816d7b3b97815f308346e20", "generic": "f816d7b3b97815f308346e20", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f816d7b3b97815f308346e20", +"motherduck": "f816d7b3b97815f308346e20", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f816d7b3b97815f308346e20", "postgresql": "f816d7b3b97815f308346e20", "snowflake": "f816d7b3b97815f308346e20", @@ -19099,8 +19099,8 @@ "databricks": "e3bc1b8bad22d8104417b00f", "duckdb": "aed0419e7f2ce7c0a8892248", "generic": "aed0419e7f2ce7c0a8892248", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "aed0419e7f2ce7c0a8892248", +"motherduck": "aed0419e7f2ce7c0a8892248", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "111af35848fe500ec2ae54b2", "postgresql": "111af35848fe500ec2ae54b2", "snowflake": "aed0419e7f2ce7c0a8892248", @@ -19116,8 +19116,8 @@ "databricks": "57922064a7dc546cbb50e42b", "duckdb": "57922064a7dc546cbb50e42b", "generic": "57922064a7dc546cbb50e42b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "57922064a7dc546cbb50e42b", +"motherduck": "57922064a7dc546cbb50e42b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "57922064a7dc546cbb50e42b", "postgresql": "57922064a7dc546cbb50e42b", "snowflake": "57922064a7dc546cbb50e42b", @@ -19133,8 +19133,8 @@ "databricks": "1e765712a4c0f6d3649350c6", "duckdb": "7b2fd741638cbd96bb38d7e0", "generic": "1e765712a4c0f6d3649350c6", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1e765712a4c0f6d3649350c6", +"motherduck": "7b2fd741638cbd96bb38d7e0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1e765712a4c0f6d3649350c6", "postgresql": "1e765712a4c0f6d3649350c6", "snowflake": "7b2fd741638cbd96bb38d7e0", @@ -19150,8 +19150,8 @@ "databricks": "e4f0244beaa01667e80482b6", "duckdb": "a766fea06fc070b352976b0a", "generic": "a766fea06fc070b352976b0a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a766fea06fc070b352976b0a", +"motherduck": "a766fea06fc070b352976b0a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a766fea06fc070b352976b0a", "postgresql": "a766fea06fc070b352976b0a", "snowflake": "a766fea06fc070b352976b0a", @@ -19167,8 +19167,8 @@ "databricks": "15b3387f6c4d73bcf113983a", "duckdb": "15b3387f6c4d73bcf113983a", "generic": "15b3387f6c4d73bcf113983a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "15b3387f6c4d73bcf113983a", +"motherduck": "15b3387f6c4d73bcf113983a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "15b3387f6c4d73bcf113983a", "postgresql": "15b3387f6c4d73bcf113983a", "snowflake": "15b3387f6c4d73bcf113983a", @@ -19184,8 +19184,8 @@ "databricks": "d34f84c1b71b9c9c0c20abb0", "duckdb": "d34f84c1b71b9c9c0c20abb0", "generic": "d34f84c1b71b9c9c0c20abb0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d34f84c1b71b9c9c0c20abb0", +"motherduck": "d34f84c1b71b9c9c0c20abb0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d34f84c1b71b9c9c0c20abb0", "postgresql": "d34f84c1b71b9c9c0c20abb0", "snowflake": "d34f84c1b71b9c9c0c20abb0", @@ -19201,8 +19201,8 @@ "databricks": "14cd6e78d75f61b2bd1bbd71", "duckdb": "14cd6e78d75f61b2bd1bbd71", "generic": "14cd6e78d75f61b2bd1bbd71", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "14cd6e78d75f61b2bd1bbd71", +"motherduck": "14cd6e78d75f61b2bd1bbd71", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "14cd6e78d75f61b2bd1bbd71", "postgresql": "14cd6e78d75f61b2bd1bbd71", "snowflake": "14cd6e78d75f61b2bd1bbd71", @@ -19218,8 +19218,8 @@ "databricks": "96e8f438bf42d79cd8b1fada", "duckdb": "96e8f438bf42d79cd8b1fada", "generic": "96e8f438bf42d79cd8b1fada", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "96e8f438bf42d79cd8b1fada", +"motherduck": "96e8f438bf42d79cd8b1fada", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "96e8f438bf42d79cd8b1fada", "postgresql": "96e8f438bf42d79cd8b1fada", "snowflake": "96e8f438bf42d79cd8b1fada", @@ -19235,8 +19235,8 @@ "databricks": "260b29f255364b2044abe127", "duckdb": "260b29f255364b2044abe127", "generic": "260b29f255364b2044abe127", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "260b29f255364b2044abe127", +"motherduck": "260b29f255364b2044abe127", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "260b29f255364b2044abe127", "postgresql": "260b29f255364b2044abe127", "snowflake": "260b29f255364b2044abe127", @@ -19252,8 +19252,8 @@ "databricks": "e5199d22ebbd1383c1e821df", "duckdb": "e5199d22ebbd1383c1e821df", "generic": "e5199d22ebbd1383c1e821df", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e5199d22ebbd1383c1e821df", +"motherduck": "e5199d22ebbd1383c1e821df", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e5199d22ebbd1383c1e821df", "postgresql": "e5199d22ebbd1383c1e821df", "snowflake": "e5199d22ebbd1383c1e821df", @@ -19269,8 +19269,8 @@ "databricks": "336fd1b7a1f7d8105c29280c", "duckdb": "0baaca265927140414f32b37", "generic": "336fd1b7a1f7d8105c29280c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "336fd1b7a1f7d8105c29280c", +"motherduck": "0baaca265927140414f32b37", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9ea4932505663b43f527adad", "postgresql": "9ea4932505663b43f527adad", "snowflake": "336fd1b7a1f7d8105c29280c", @@ -19286,8 +19286,8 @@ "databricks": "3f71ab5f3c1c040da96f2cec", "duckdb": "3f71ab5f3c1c040da96f2cec", "generic": "3f71ab5f3c1c040da96f2cec", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3f71ab5f3c1c040da96f2cec", +"motherduck": "3f71ab5f3c1c040da96f2cec", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3f71ab5f3c1c040da96f2cec", "postgresql": "3f71ab5f3c1c040da96f2cec", "snowflake": "3f71ab5f3c1c040da96f2cec", @@ -19303,8 +19303,8 @@ "databricks": "802f1c1fcb97ddd5679172d2", "duckdb": "802f1c1fcb97ddd5679172d2", "generic": "802f1c1fcb97ddd5679172d2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "802f1c1fcb97ddd5679172d2", +"motherduck": "802f1c1fcb97ddd5679172d2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "802f1c1fcb97ddd5679172d2", "postgresql": "802f1c1fcb97ddd5679172d2", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -19320,8 +19320,8 @@ "databricks": "d91c8f17eb347629690400c7", "duckdb": "d91c8f17eb347629690400c7", "generic": "d91c8f17eb347629690400c7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d91c8f17eb347629690400c7", +"motherduck": "d91c8f17eb347629690400c7", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d91c8f17eb347629690400c7", "postgresql": "d91c8f17eb347629690400c7", "snowflake": "d91c8f17eb347629690400c7", @@ -19337,8 +19337,8 @@ "databricks": "2bfa352725f9d8a91250f75e", "duckdb": "2bfa352725f9d8a91250f75e", "generic": "2bfa352725f9d8a91250f75e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2bfa352725f9d8a91250f75e", +"motherduck": "2bfa352725f9d8a91250f75e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2bfa352725f9d8a91250f75e", "postgresql": "2bfa352725f9d8a91250f75e", "snowflake": "2bfa352725f9d8a91250f75e", @@ -19354,8 +19354,8 @@ "databricks": "9a66317fb8d040bf991502c0", "duckdb": "314ed506f24e92300a425780", "generic": "9a66317fb8d040bf991502c0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9a66317fb8d040bf991502c0", +"motherduck": "314ed506f24e92300a425780", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "314ed506f24e92300a425780", "postgresql": "314ed506f24e92300a425780", "snowflake": "314ed506f24e92300a425780", @@ -19371,8 +19371,8 @@ "databricks": "2873244018c69d6d42752bdc", "duckdb": "2873244018c69d6d42752bdc", "generic": "2873244018c69d6d42752bdc", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2873244018c69d6d42752bdc", +"motherduck": "2873244018c69d6d42752bdc", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2873244018c69d6d42752bdc", "postgresql": "2873244018c69d6d42752bdc", "snowflake": "2873244018c69d6d42752bdc", @@ -19388,8 +19388,8 @@ "databricks": "3d23f811a03db519b598c815", "duckdb": "3d23f811a03db519b598c815", "generic": "3d23f811a03db519b598c815", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3d23f811a03db519b598c815", +"motherduck": "3d23f811a03db519b598c815", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3d23f811a03db519b598c815", "postgresql": "3d23f811a03db519b598c815", "snowflake": "3d23f811a03db519b598c815", @@ -19405,8 +19405,8 @@ "databricks": "2e8f6dfec4597fb9373329e7", "duckdb": "087a9417cb511ff7bcfd6ccb", "generic": "2e8f6dfec4597fb9373329e7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2e8f6dfec4597fb9373329e7", +"motherduck": "087a9417cb511ff7bcfd6ccb", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "383f8922f4b6f167d384037d", "postgresql": "383f8922f4b6f167d384037d", "snowflake": "383f8922f4b6f167d384037d", @@ -19422,8 +19422,8 @@ "databricks": "19a1a80ffc06169f32694de8", "duckdb": "19a1a80ffc06169f32694de8", "generic": "19a1a80ffc06169f32694de8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "19a1a80ffc06169f32694de8", +"motherduck": "19a1a80ffc06169f32694de8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ab1f5f46e69284778bd66d58", "postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", @@ -19439,8 +19439,8 @@ "databricks": "b017fb3c48f125e51b6db123", "duckdb": "b017fb3c48f125e51b6db123", "generic": "b017fb3c48f125e51b6db123", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b017fb3c48f125e51b6db123", +"motherduck": "b017fb3c48f125e51b6db123", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b017fb3c48f125e51b6db123", "postgresql": "b017fb3c48f125e51b6db123", "snowflake": "b017fb3c48f125e51b6db123", @@ -19456,8 +19456,8 @@ "databricks": "a7120d1b1abcb844c87a968a", "duckdb": "a7120d1b1abcb844c87a968a", "generic": "a7120d1b1abcb844c87a968a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a7120d1b1abcb844c87a968a", +"motherduck": "a7120d1b1abcb844c87a968a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a7120d1b1abcb844c87a968a", "postgresql": "a7120d1b1abcb844c87a968a", "snowflake": "a7120d1b1abcb844c87a968a", @@ -19473,8 +19473,8 @@ "databricks": "fd31c8e468772027804119ec", "duckdb": "fd31c8e468772027804119ec", "generic": "fd31c8e468772027804119ec", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fd31c8e468772027804119ec", +"motherduck": "fd31c8e468772027804119ec", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "fd31c8e468772027804119ec", "postgresql": "fd31c8e468772027804119ec", "snowflake": "fd31c8e468772027804119ec", @@ -19490,8 +19490,8 @@ "databricks": "4169d9651c2a3667fc561f32", "duckdb": "4169d9651c2a3667fc561f32", "generic": "4169d9651c2a3667fc561f32", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4169d9651c2a3667fc561f32", +"motherduck": "4169d9651c2a3667fc561f32", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "4169d9651c2a3667fc561f32", "postgresql": "4169d9651c2a3667fc561f32", "snowflake": "b210f8eb26c37b422f786c6b", @@ -19507,8 +19507,8 @@ "databricks": "3f71ab5f3c1c040da96f2cec", "duckdb": "3f71ab5f3c1c040da96f2cec", "generic": "3f71ab5f3c1c040da96f2cec", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3f71ab5f3c1c040da96f2cec", +"motherduck": "3f71ab5f3c1c040da96f2cec", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3f71ab5f3c1c040da96f2cec", "postgresql": "3f71ab5f3c1c040da96f2cec", "snowflake": "3f71ab5f3c1c040da96f2cec", @@ -19524,8 +19524,8 @@ "databricks": "eed0645c1e86fa1a6e35ccaa", "duckdb": "eed0645c1e86fa1a6e35ccaa", "generic": "eed0645c1e86fa1a6e35ccaa", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "eed0645c1e86fa1a6e35ccaa", +"motherduck": "eed0645c1e86fa1a6e35ccaa", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b0477b36767a62145aa9158c", "postgresql": "b0477b36767a62145aa9158c", "snowflake": "97a20344d811fb77c7690863", @@ -19541,8 +19541,8 @@ "databricks": "3a7ffeb8f81df6ccd57f3689", "duckdb": "3a7ffeb8f81df6ccd57f3689", "generic": "3a7ffeb8f81df6ccd57f3689", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3a7ffeb8f81df6ccd57f3689", +"motherduck": "3a7ffeb8f81df6ccd57f3689", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "062e51ec86ccd542eb78500d", "postgresql": "062e51ec86ccd542eb78500d", "snowflake": "3a7ffeb8f81df6ccd57f3689", @@ -19558,8 +19558,8 @@ "databricks": "e1c74d96ac578f9323bf1bbd", "duckdb": "e1c74d96ac578f9323bf1bbd", "generic": "e1c74d96ac578f9323bf1bbd", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e1c74d96ac578f9323bf1bbd", +"motherduck": "e1c74d96ac578f9323bf1bbd", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e1c74d96ac578f9323bf1bbd", "postgresql": "e1c74d96ac578f9323bf1bbd", "snowflake": "e1c74d96ac578f9323bf1bbd", @@ -19575,8 +19575,8 @@ "databricks": "69ec26c1982add4704b948a6", "duckdb": "427810237ffbfe3ee6dbe0d4", "generic": "6f2f02617b722e48d3f365c2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6f2f02617b722e48d3f365c2", +"motherduck": "427810237ffbfe3ee6dbe0d4", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "6f2f02617b722e48d3f365c2", "postgresql": "6f2f02617b722e48d3f365c2", "snowflake": "a4325ee9f4d61463eb81badb", @@ -19592,8 +19592,8 @@ "databricks": "c50a316d091c821866e4bfba", "duckdb": "c50a316d091c821866e4bfba", "generic": "c50a316d091c821866e4bfba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -19609,8 +19609,8 @@ "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fe23594c781e79cbb0c8927a", +"motherduck": "fe23594c781e79cbb0c8927a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8a3f094f18fb4047f0febf59", "postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", @@ -19626,8 +19626,8 @@ "databricks": "1e75e5227bea0ae294ac7af1", "duckdb": "19bdd92ea0cc5fb028657510", "generic": "b8321619b794db091c9e96a1", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b8321619b794db091c9e96a1", +"motherduck": "19bdd92ea0cc5fb028657510", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "bce26544873d6ec9191c9a86", "postgresql": "bce26544873d6ec9191c9a86", "snowflake": "b8321619b794db091c9e96a1", @@ -19643,8 +19643,8 @@ "databricks": "19a1a80ffc06169f32694de8", "duckdb": "19a1a80ffc06169f32694de8", "generic": "19a1a80ffc06169f32694de8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "19a1a80ffc06169f32694de8", +"motherduck": "19a1a80ffc06169f32694de8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "19a1a80ffc06169f32694de8", "postgresql": "19a1a80ffc06169f32694de8", "snowflake": "19a1a80ffc06169f32694de8", @@ -19660,8 +19660,8 @@ "databricks": "26e762643db86e7646e57cba", "duckdb": "26e762643db86e7646e57cba", "generic": "26e762643db86e7646e57cba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "26e762643db86e7646e57cba", +"motherduck": "26e762643db86e7646e57cba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "26e762643db86e7646e57cba", "postgresql": "26e762643db86e7646e57cba", "snowflake": "26e762643db86e7646e57cba", @@ -19677,8 +19677,8 @@ "databricks": "078c0e4b25cdb89eb4ebd88f", "duckdb": "078c0e4b25cdb89eb4ebd88f", "generic": "078c0e4b25cdb89eb4ebd88f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "078c0e4b25cdb89eb4ebd88f", +"motherduck": "078c0e4b25cdb89eb4ebd88f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "078c0e4b25cdb89eb4ebd88f", "postgresql": "078c0e4b25cdb89eb4ebd88f", "snowflake": "6306b9e88f7b13dbb6a32c00", @@ -19694,8 +19694,8 @@ "databricks": "7cd31289a5b99b5a5e24492d", "duckdb": "7cd31289a5b99b5a5e24492d", "generic": "7cd31289a5b99b5a5e24492d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "7cd31289a5b99b5a5e24492d", +"motherduck": "7cd31289a5b99b5a5e24492d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c5bdaf88df7ef3d9ae693106", "postgresql": "c5bdaf88df7ef3d9ae693106", "snowflake": "7cd31289a5b99b5a5e24492d", @@ -19711,8 +19711,8 @@ "databricks": "251aba3fef503ce64556c701", "duckdb": "251aba3fef503ce64556c701", "generic": "251aba3fef503ce64556c701", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "251aba3fef503ce64556c701", +"motherduck": "251aba3fef503ce64556c701", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b7d6fc051a3c1ea6deb29ff4", "postgresql": "b7d6fc051a3c1ea6deb29ff4", "snowflake": "251aba3fef503ce64556c701", @@ -19728,8 +19728,8 @@ "databricks": "3eb44072dafa3bef75c97b6a", "duckdb": "3eb44072dafa3bef75c97b6a", "generic": "3eb44072dafa3bef75c97b6a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3eb44072dafa3bef75c97b6a", +"motherduck": "3eb44072dafa3bef75c97b6a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3eb44072dafa3bef75c97b6a", "postgresql": "3eb44072dafa3bef75c97b6a", "snowflake": "3eb44072dafa3bef75c97b6a", @@ -19745,8 +19745,8 @@ "databricks": "596dafb6039bd1951f03c6a3", "duckdb": "596dafb6039bd1951f03c6a3", "generic": "596dafb6039bd1951f03c6a3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "596dafb6039bd1951f03c6a3", +"motherduck": "596dafb6039bd1951f03c6a3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0d0c279c483ce8c07a9e7375", "postgresql": "0d0c279c483ce8c07a9e7375", "snowflake": "596dafb6039bd1951f03c6a3", @@ -19762,8 +19762,8 @@ "databricks": "b2c9b6ebca6c097f3caa0aa8", "duckdb": "b2c9b6ebca6c097f3caa0aa8", "generic": "b2c9b6ebca6c097f3caa0aa8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b2c9b6ebca6c097f3caa0aa8", +"motherduck": "b2c9b6ebca6c097f3caa0aa8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b2c9b6ebca6c097f3caa0aa8", "postgresql": "b2c9b6ebca6c097f3caa0aa8", "snowflake": "b2c9b6ebca6c097f3caa0aa8", @@ -19779,8 +19779,8 @@ "databricks": "d2c97d221599d038d9875873", "duckdb": "d2c97d221599d038d9875873", "generic": "d2c97d221599d038d9875873", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d2c97d221599d038d9875873", +"motherduck": "d2c97d221599d038d9875873", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "55155766823b6a3a5e882fc2", "postgresql": "55155766823b6a3a5e882fc2", "snowflake": "d2c97d221599d038d9875873", @@ -19796,8 +19796,8 @@ "databricks": "4d90ccac34ef123bb0b2a15c", "duckdb": "4d90ccac34ef123bb0b2a15c", "generic": "4d90ccac34ef123bb0b2a15c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4d90ccac34ef123bb0b2a15c", +"motherduck": "4d90ccac34ef123bb0b2a15c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "4d90ccac34ef123bb0b2a15c", "postgresql": "4d90ccac34ef123bb0b2a15c", "snowflake": "4d90ccac34ef123bb0b2a15c", @@ -19813,8 +19813,8 @@ "databricks": "1ae302067c05dc9ae3a029f2", "duckdb": "1ae302067c05dc9ae3a029f2", "generic": "1ae302067c05dc9ae3a029f2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1ae302067c05dc9ae3a029f2", +"motherduck": "1ae302067c05dc9ae3a029f2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1ae302067c05dc9ae3a029f2", "postgresql": "1ae302067c05dc9ae3a029f2", "snowflake": "1ae302067c05dc9ae3a029f2", @@ -19830,8 +19830,8 @@ "databricks": "8441d00030df06ad6a5534d3", "duckdb": "8441d00030df06ad6a5534d3", "generic": "8441d00030df06ad6a5534d3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8441d00030df06ad6a5534d3", +"motherduck": "8441d00030df06ad6a5534d3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8441d00030df06ad6a5534d3", "postgresql": "8441d00030df06ad6a5534d3", "snowflake": "97a20344d811fb77c7690863", @@ -19847,8 +19847,8 @@ "databricks": "b1ec979036647e3f2686cece", "duckdb": "b1ec979036647e3f2686cece", "generic": "b1ec979036647e3f2686cece", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b1ec979036647e3f2686cece", +"motherduck": "b1ec979036647e3f2686cece", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b1ec979036647e3f2686cece", "postgresql": "b1ec979036647e3f2686cece", "snowflake": "b1ec979036647e3f2686cece", @@ -19864,8 +19864,8 @@ "databricks": "962ea05bab624c1f8c259a65", "duckdb": "962ea05bab624c1f8c259a65", "generic": "962ea05bab624c1f8c259a65", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "962ea05bab624c1f8c259a65", +"motherduck": "962ea05bab624c1f8c259a65", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "962ea05bab624c1f8c259a65", "postgresql": "962ea05bab624c1f8c259a65", "snowflake": "962ea05bab624c1f8c259a65", @@ -19881,8 +19881,8 @@ "databricks": "f319941cc6ec088f46ecbadb", "duckdb": "f319941cc6ec088f46ecbadb", "generic": "f319941cc6ec088f46ecbadb", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f319941cc6ec088f46ecbadb", +"motherduck": "f319941cc6ec088f46ecbadb", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e664f46cfdd4441496e27023", "postgresql": "e664f46cfdd4441496e27023", "snowflake": "f319941cc6ec088f46ecbadb", @@ -19898,8 +19898,8 @@ "databricks": "79278352c17c9de0f5a28af5", "duckdb": "79278352c17c9de0f5a28af5", "generic": "79278352c17c9de0f5a28af5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "79278352c17c9de0f5a28af5", +"motherduck": "79278352c17c9de0f5a28af5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "79278352c17c9de0f5a28af5", "postgresql": "79278352c17c9de0f5a28af5", "snowflake": "97a20344d811fb77c7690863", @@ -19915,8 +19915,8 @@ "databricks": "717cf79ef777823eb3576deb", "duckdb": "717cf79ef777823eb3576deb", "generic": "717cf79ef777823eb3576deb", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "717cf79ef777823eb3576deb", +"motherduck": "717cf79ef777823eb3576deb", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "717cf79ef777823eb3576deb", "postgresql": "717cf79ef777823eb3576deb", "snowflake": "717cf79ef777823eb3576deb", @@ -19932,8 +19932,8 @@ "databricks": "8362a1ff79ce7c9350d11210", "duckdb": "8362a1ff79ce7c9350d11210", "generic": "8362a1ff79ce7c9350d11210", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8362a1ff79ce7c9350d11210", +"motherduck": "8362a1ff79ce7c9350d11210", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8362a1ff79ce7c9350d11210", "postgresql": "8362a1ff79ce7c9350d11210", "snowflake": "8362a1ff79ce7c9350d11210", @@ -19949,8 +19949,8 @@ "databricks": "688179272da645629077e980", "duckdb": "688179272da645629077e980", "generic": "688179272da645629077e980", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "688179272da645629077e980", +"motherduck": "688179272da645629077e980", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "688179272da645629077e980", "postgresql": "688179272da645629077e980", "snowflake": "b210f8eb26c37b422f786c6b", @@ -19966,8 +19966,8 @@ "databricks": "2e5b1ffe62d65d4d8c389ecd", "duckdb": "2e5b1ffe62d65d4d8c389ecd", "generic": "2e5b1ffe62d65d4d8c389ecd", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2e5b1ffe62d65d4d8c389ecd", +"motherduck": "2e5b1ffe62d65d4d8c389ecd", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2e5b1ffe62d65d4d8c389ecd", "postgresql": "2e5b1ffe62d65d4d8c389ecd", "snowflake": "2e5b1ffe62d65d4d8c389ecd", @@ -19983,8 +19983,8 @@ "databricks": "e6badc927d1adf4563b4f3a5", "duckdb": "e6badc927d1adf4563b4f3a5", "generic": "e6badc927d1adf4563b4f3a5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e6badc927d1adf4563b4f3a5", +"motherduck": "e6badc927d1adf4563b4f3a5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e6badc927d1adf4563b4f3a5", "postgresql": "e6badc927d1adf4563b4f3a5", "snowflake": "e6badc927d1adf4563b4f3a5", @@ -20000,8 +20000,8 @@ "databricks": "9070740b5c7f7fb5d091ad05", "duckdb": "9070740b5c7f7fb5d091ad05", "generic": "9070740b5c7f7fb5d091ad05", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9070740b5c7f7fb5d091ad05", +"motherduck": "9070740b5c7f7fb5d091ad05", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9070740b5c7f7fb5d091ad05", "postgresql": "9070740b5c7f7fb5d091ad05", "snowflake": "9070740b5c7f7fb5d091ad05", @@ -20017,8 +20017,8 @@ "databricks": "607f7c82658f39cce394b124", "duckdb": "607f7c82658f39cce394b124", "generic": "607f7c82658f39cce394b124", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "607f7c82658f39cce394b124", +"motherduck": "607f7c82658f39cce394b124", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "607f7c82658f39cce394b124", "postgresql": "607f7c82658f39cce394b124", "snowflake": "607f7c82658f39cce394b124", @@ -20034,8 +20034,8 @@ "databricks": "b3d92948d21d50dcbeb5f80b", "duckdb": "b3d92948d21d50dcbeb5f80b", "generic": "b3d92948d21d50dcbeb5f80b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b3d92948d21d50dcbeb5f80b", +"motherduck": "b3d92948d21d50dcbeb5f80b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b3d92948d21d50dcbeb5f80b", "postgresql": "b3d92948d21d50dcbeb5f80b", "snowflake": "b3d92948d21d50dcbeb5f80b", @@ -20051,8 +20051,8 @@ "databricks": "e6ffdece2977be55da73b59a", "duckdb": "c50a316d091c821866e4bfba", "generic": "e6ffdece2977be55da73b59a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e6ffdece2977be55da73b59a", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -20068,8 +20068,8 @@ "databricks": "f4c43a87a78970bca1f88b09", "duckdb": "f4c43a87a78970bca1f88b09", "generic": "f4c43a87a78970bca1f88b09", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f4c43a87a78970bca1f88b09", +"motherduck": "f4c43a87a78970bca1f88b09", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f4c43a87a78970bca1f88b09", "postgresql": "f4c43a87a78970bca1f88b09", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -20085,8 +20085,8 @@ "databricks": "2279ed9a279f48d8451671cf", "duckdb": "2279ed9a279f48d8451671cf", "generic": "2279ed9a279f48d8451671cf", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2279ed9a279f48d8451671cf", +"motherduck": "2279ed9a279f48d8451671cf", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2279ed9a279f48d8451671cf", "postgresql": "2279ed9a279f48d8451671cf", "snowflake": "2279ed9a279f48d8451671cf", @@ -20102,8 +20102,8 @@ "databricks": "881106a4a0d2de0ff7c56373", "duckdb": "881106a4a0d2de0ff7c56373", "generic": "881106a4a0d2de0ff7c56373", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "881106a4a0d2de0ff7c56373", +"motherduck": "881106a4a0d2de0ff7c56373", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "cf167ec395fc843602e3b1e0", "postgresql": "cf167ec395fc843602e3b1e0", "snowflake": "881106a4a0d2de0ff7c56373", @@ -20119,8 +20119,8 @@ "databricks": "f774d5e8feadbf11edaa26b1", "duckdb": "f46cf4b4e131aa122b5b33d6", "generic": "f774d5e8feadbf11edaa26b1", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f774d5e8feadbf11edaa26b1", +"motherduck": "f46cf4b4e131aa122b5b33d6", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f46cf4b4e131aa122b5b33d6", "postgresql": "f46cf4b4e131aa122b5b33d6", "snowflake": "f46cf4b4e131aa122b5b33d6", @@ -20136,8 +20136,8 @@ "databricks": "8441d00030df06ad6a5534d3", "duckdb": "8441d00030df06ad6a5534d3", "generic": "8441d00030df06ad6a5534d3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8441d00030df06ad6a5534d3", +"motherduck": "8441d00030df06ad6a5534d3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8441d00030df06ad6a5534d3", "postgresql": "8441d00030df06ad6a5534d3", "snowflake": "97a20344d811fb77c7690863", @@ -20153,8 +20153,8 @@ "databricks": "0475b57e9e9da81e87fa09ba", "duckdb": "0475b57e9e9da81e87fa09ba", "generic": "0475b57e9e9da81e87fa09ba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0475b57e9e9da81e87fa09ba", +"motherduck": "0475b57e9e9da81e87fa09ba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0475b57e9e9da81e87fa09ba", "postgresql": "0475b57e9e9da81e87fa09ba", "snowflake": "0475b57e9e9da81e87fa09ba", @@ -20170,8 +20170,8 @@ "databricks": "43e3e82e80c9c1580eb61a1d", "duckdb": "43e3e82e80c9c1580eb61a1d", "generic": "43e3e82e80c9c1580eb61a1d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "43e3e82e80c9c1580eb61a1d", +"motherduck": "43e3e82e80c9c1580eb61a1d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "43e3e82e80c9c1580eb61a1d", "postgresql": "43e3e82e80c9c1580eb61a1d", "snowflake": "43e3e82e80c9c1580eb61a1d", @@ -20187,8 +20187,8 @@ "databricks": "846f4ee64c34aab0cdabb9a0", "duckdb": "51b0e597909858f7f7aa4571", "generic": "51b0e597909858f7f7aa4571", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "51b0e597909858f7f7aa4571", +"motherduck": "51b0e597909858f7f7aa4571", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "51b0e597909858f7f7aa4571", "postgresql": "51b0e597909858f7f7aa4571", "snowflake": "4d94d72d93a870dcc1521bee", @@ -20204,8 +20204,8 @@ "databricks": "962ea05bab624c1f8c259a65", "duckdb": "962ea05bab624c1f8c259a65", "generic": "962ea05bab624c1f8c259a65", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "962ea05bab624c1f8c259a65", +"motherduck": "962ea05bab624c1f8c259a65", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "962ea05bab624c1f8c259a65", "postgresql": "962ea05bab624c1f8c259a65", "snowflake": "962ea05bab624c1f8c259a65", @@ -20221,8 +20221,8 @@ "databricks": "1275b5b0c52f69cf592c004b", "duckdb": "1275b5b0c52f69cf592c004b", "generic": "1275b5b0c52f69cf592c004b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1275b5b0c52f69cf592c004b", +"motherduck": "1275b5b0c52f69cf592c004b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1275b5b0c52f69cf592c004b", "postgresql": "1275b5b0c52f69cf592c004b", "snowflake": "1275b5b0c52f69cf592c004b", @@ -20238,8 +20238,8 @@ "databricks": "11d9e4471b50590a198e94a1", "duckdb": "a01889d0707cf6b364efb5f9", "generic": "94834ae92073b4606184619b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "a01889d0707cf6b364efb5f9", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a01889d0707cf6b364efb5f9", "postgresql": "a01889d0707cf6b364efb5f9", "snowflake": "5bc09c1a33ccb4bd2e4075e5", @@ -20255,8 +20255,8 @@ "databricks": "19a1a80ffc06169f32694de8", "duckdb": "19a1a80ffc06169f32694de8", "generic": "19a1a80ffc06169f32694de8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "19a1a80ffc06169f32694de8", +"motherduck": "19a1a80ffc06169f32694de8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ab1f5f46e69284778bd66d58", "postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", @@ -20272,8 +20272,8 @@ "databricks": "ba3896413716dbcd432e62dd", "duckdb": "c43574432afc90bcba6944b2", "generic": "ba3896413716dbcd432e62dd", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ba3896413716dbcd432e62dd", +"motherduck": "c43574432afc90bcba6944b2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ba3896413716dbcd432e62dd", "postgresql": "ba3896413716dbcd432e62dd", "snowflake": "26094c8570001998e247761e", @@ -20289,8 +20289,8 @@ "databricks": "d90a78129f7050a540984f6f", "duckdb": "66f7ca02161349d637fa7d4e", "generic": "66f7ca02161349d637fa7d4e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "66f7ca02161349d637fa7d4e", +"motherduck": "66f7ca02161349d637fa7d4e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "66f7ca02161349d637fa7d4e", "postgresql": "66f7ca02161349d637fa7d4e", "snowflake": "66f7ca02161349d637fa7d4e", @@ -20306,8 +20306,8 @@ "databricks": "83fdbd1f0a6ee4e72faa4d94", "duckdb": "83fdbd1f0a6ee4e72faa4d94", "generic": "83fdbd1f0a6ee4e72faa4d94", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "83fdbd1f0a6ee4e72faa4d94", +"motherduck": "83fdbd1f0a6ee4e72faa4d94", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "83fdbd1f0a6ee4e72faa4d94", "postgresql": "83fdbd1f0a6ee4e72faa4d94", "snowflake": "83fdbd1f0a6ee4e72faa4d94", @@ -20323,8 +20323,8 @@ "databricks": "e2879cf4770e7f48d2dd704a", "duckdb": "e2879cf4770e7f48d2dd704a", "generic": "e2879cf4770e7f48d2dd704a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e2879cf4770e7f48d2dd704a", +"motherduck": "e2879cf4770e7f48d2dd704a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2d4aea8dec5521e258596f3d", "postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", @@ -20340,8 +20340,8 @@ "databricks": "7c8df74df226e73b459bfdba", "duckdb": "7c8df74df226e73b459bfdba", "generic": "7c8df74df226e73b459bfdba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "7c8df74df226e73b459bfdba", +"motherduck": "7c8df74df226e73b459bfdba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7c8df74df226e73b459bfdba", "postgresql": "7c8df74df226e73b459bfdba", "snowflake": "7c8df74df226e73b459bfdba", @@ -20357,8 +20357,8 @@ "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "8a3f094f18fb4047f0febf59", "generic": "fe23594c781e79cbb0c8927a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fe23594c781e79cbb0c8927a", +"motherduck": "8a3f094f18fb4047f0febf59", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b0ce99394388f977f67fd1df", "postgresql": "b0ce99394388f977f67fd1df", "snowflake": "38b2ec94bf02be40007ce525", @@ -20374,8 +20374,8 @@ "databricks": "38b2ec94bf02be40007ce525", "duckdb": "38b2ec94bf02be40007ce525", "generic": "38b2ec94bf02be40007ce525", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "38b2ec94bf02be40007ce525", +"motherduck": "38b2ec94bf02be40007ce525", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "850deea990b52b82b36a9848", "postgresql": "850deea990b52b82b36a9848", "snowflake": "38b2ec94bf02be40007ce525", @@ -20391,8 +20391,8 @@ "databricks": "69ccced5b5ccc61253f2656b", "duckdb": "627ea26821da68299a37ddb8", "generic": "69ccced5b5ccc61253f2656b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "69ccced5b5ccc61253f2656b", +"motherduck": "627ea26821da68299a37ddb8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "69ccced5b5ccc61253f2656b", "postgresql": "69ccced5b5ccc61253f2656b", "snowflake": "5a7da03d64d9bf65f9984782", @@ -20408,8 +20408,8 @@ "databricks": "4920de3b1203a0d66fc1b3cd", "duckdb": "4920de3b1203a0d66fc1b3cd", "generic": "4920de3b1203a0d66fc1b3cd", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4920de3b1203a0d66fc1b3cd", +"motherduck": "4920de3b1203a0d66fc1b3cd", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8682e002a09d580d930440bf", "postgresql": "8682e002a09d580d930440bf", "snowflake": "4920de3b1203a0d66fc1b3cd", @@ -20425,8 +20425,8 @@ "databricks": "7914189ff18b0e70ddf7d042", "duckdb": "7914189ff18b0e70ddf7d042", "generic": "7914189ff18b0e70ddf7d042", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "7914189ff18b0e70ddf7d042", +"motherduck": "7914189ff18b0e70ddf7d042", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7914189ff18b0e70ddf7d042", "postgresql": "7914189ff18b0e70ddf7d042", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -20442,8 +20442,8 @@ "databricks": "c6889f2965db73017be6f6e7", "duckdb": "c6889f2965db73017be6f6e7", "generic": "c6889f2965db73017be6f6e7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c6889f2965db73017be6f6e7", +"motherduck": "c6889f2965db73017be6f6e7", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c6889f2965db73017be6f6e7", "postgresql": "c6889f2965db73017be6f6e7", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -20459,8 +20459,8 @@ "databricks": "4d0f0db1a550ed1e0da1a043", "duckdb": "4d0f0db1a550ed1e0da1a043", "generic": "4d0f0db1a550ed1e0da1a043", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4d0f0db1a550ed1e0da1a043", +"motherduck": "4d0f0db1a550ed1e0da1a043", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e47320d1030c166a05193bc4", "postgresql": "e47320d1030c166a05193bc4", "snowflake": "4d0f0db1a550ed1e0da1a043", @@ -20476,8 +20476,8 @@ "databricks": "a9d9e03f8e981cec9dca7694", "duckdb": "a9d9e03f8e981cec9dca7694", "generic": "a9d9e03f8e981cec9dca7694", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a9d9e03f8e981cec9dca7694", +"motherduck": "a9d9e03f8e981cec9dca7694", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a9d9e03f8e981cec9dca7694", "postgresql": "a9d9e03f8e981cec9dca7694", "snowflake": "a9d9e03f8e981cec9dca7694", @@ -20493,8 +20493,8 @@ "databricks": "95961ec6c293c3d6b6bf65ec", "duckdb": "95961ec6c293c3d6b6bf65ec", "generic": "95961ec6c293c3d6b6bf65ec", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "95961ec6c293c3d6b6bf65ec", +"motherduck": "95961ec6c293c3d6b6bf65ec", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "95961ec6c293c3d6b6bf65ec", "postgresql": "95961ec6c293c3d6b6bf65ec", "snowflake": "95961ec6c293c3d6b6bf65ec", @@ -20510,8 +20510,8 @@ "databricks": "2c5e4442c23cf9e2b37caa7e", "duckdb": "2c5e4442c23cf9e2b37caa7e", "generic": "2c5e4442c23cf9e2b37caa7e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2c5e4442c23cf9e2b37caa7e", +"motherduck": "2c5e4442c23cf9e2b37caa7e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2c5e4442c23cf9e2b37caa7e", "postgresql": "2c5e4442c23cf9e2b37caa7e", "snowflake": "2c5e4442c23cf9e2b37caa7e", @@ -20527,8 +20527,8 @@ "databricks": "c9b810639795a7bfab91b98d", "duckdb": "a428302e4848ffe877284c84", "generic": "c9b810639795a7bfab91b98d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c9b810639795a7bfab91b98d", +"motherduck": "a428302e4848ffe877284c84", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a428302e4848ffe877284c84", "postgresql": "a428302e4848ffe877284c84", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -20544,8 +20544,8 @@ "databricks": "1ebb508c9c012fda785eac26", "duckdb": "8702414b6372d27d90111c0d", "generic": "a7fcdf83ba95c5481cac4059", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "8702414b6372d27d90111c0d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8702414b6372d27d90111c0d", "postgresql": "8702414b6372d27d90111c0d", "snowflake": "521d89d8efac986351afc368", @@ -20561,8 +20561,8 @@ "databricks": "38b2ec94bf02be40007ce525", "duckdb": "38b2ec94bf02be40007ce525", "generic": "38b2ec94bf02be40007ce525", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "38b2ec94bf02be40007ce525", +"motherduck": "38b2ec94bf02be40007ce525", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "850deea990b52b82b36a9848", "postgresql": "850deea990b52b82b36a9848", "snowflake": "38b2ec94bf02be40007ce525", @@ -20578,8 +20578,8 @@ "databricks": "3dbbe73ee4310c73030e8110", "duckdb": "3dbbe73ee4310c73030e8110", "generic": "3dbbe73ee4310c73030e8110", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3dbbe73ee4310c73030e8110", +"motherduck": "3dbbe73ee4310c73030e8110", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3dbbe73ee4310c73030e8110", "postgresql": "3dbbe73ee4310c73030e8110", "snowflake": "f43dac38f34a1c0618a3f812", @@ -20595,8 +20595,8 @@ "databricks": "62368fc15cefd5807807b209", "duckdb": "62368fc15cefd5807807b209", "generic": "62368fc15cefd5807807b209", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "62368fc15cefd5807807b209", +"motherduck": "62368fc15cefd5807807b209", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "62368fc15cefd5807807b209", "postgresql": "62368fc15cefd5807807b209", "snowflake": "62368fc15cefd5807807b209", @@ -20612,8 +20612,8 @@ "databricks": "1bdf5908ca249b2331a0427c", "duckdb": "1bdf5908ca249b2331a0427c", "generic": "1bdf5908ca249b2331a0427c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1bdf5908ca249b2331a0427c", +"motherduck": "1bdf5908ca249b2331a0427c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1bdf5908ca249b2331a0427c", "postgresql": "1bdf5908ca249b2331a0427c", "snowflake": "8ff614b7306c0edbe36ce00b", @@ -20629,8 +20629,8 @@ "databricks": "dd061dac05d8a112fce62140", "duckdb": "dd061dac05d8a112fce62140", "generic": "dd061dac05d8a112fce62140", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "dd061dac05d8a112fce62140", +"motherduck": "dd061dac05d8a112fce62140", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "dd061dac05d8a112fce62140", "postgresql": "dd061dac05d8a112fce62140", "snowflake": "dd061dac05d8a112fce62140", @@ -20646,8 +20646,8 @@ "databricks": "9ce61cdfd35b2a8b31faa983", "duckdb": "d7b4d15e2e3140069c23e6d4", "generic": "d7b4d15e2e3140069c23e6d4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d7b4d15e2e3140069c23e6d4", +"motherduck": "d7b4d15e2e3140069c23e6d4", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d7b4d15e2e3140069c23e6d4", "postgresql": "d7b4d15e2e3140069c23e6d4", "snowflake": "906644f12c56f91f8767953d", @@ -20663,8 +20663,8 @@ "databricks": "cd932c5de0731b5ff7bddd24", "duckdb": "cd932c5de0731b5ff7bddd24", "generic": "cd932c5de0731b5ff7bddd24", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "cd932c5de0731b5ff7bddd24", +"motherduck": "cd932c5de0731b5ff7bddd24", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "cd932c5de0731b5ff7bddd24", "postgresql": "cd932c5de0731b5ff7bddd24", "snowflake": "cd932c5de0731b5ff7bddd24", @@ -20680,8 +20680,8 @@ "databricks": "104d3e046c47032c37f947b8", "duckdb": "104d3e046c47032c37f947b8", "generic": "104d3e046c47032c37f947b8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "104d3e046c47032c37f947b8", +"motherduck": "104d3e046c47032c37f947b8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "104d3e046c47032c37f947b8", "postgresql": "104d3e046c47032c37f947b8", "snowflake": "104d3e046c47032c37f947b8", @@ -20697,8 +20697,8 @@ "databricks": "f179445103bc721a2dc04002", "duckdb": "f179445103bc721a2dc04002", "generic": "f179445103bc721a2dc04002", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f179445103bc721a2dc04002", +"motherduck": "f179445103bc721a2dc04002", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f179445103bc721a2dc04002", "postgresql": "f179445103bc721a2dc04002", "snowflake": "f179445103bc721a2dc04002", @@ -20714,8 +20714,8 @@ "databricks": "7a924f1a13854aa3260f3f57", "duckdb": "7a924f1a13854aa3260f3f57", "generic": "7a924f1a13854aa3260f3f57", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "7a924f1a13854aa3260f3f57", +"motherduck": "7a924f1a13854aa3260f3f57", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7a924f1a13854aa3260f3f57", "postgresql": "7a924f1a13854aa3260f3f57", "snowflake": "7a924f1a13854aa3260f3f57", @@ -20731,8 +20731,8 @@ "databricks": "3d4143f0295abc5ed5a3d8cc", "duckdb": "3d4143f0295abc5ed5a3d8cc", "generic": "3d4143f0295abc5ed5a3d8cc", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3d4143f0295abc5ed5a3d8cc", +"motherduck": "3d4143f0295abc5ed5a3d8cc", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "359568909836c6e577ab7e8c", "postgresql": "359568909836c6e577ab7e8c", "snowflake": "3d4143f0295abc5ed5a3d8cc", @@ -20748,8 +20748,8 @@ "databricks": "3f71ab5f3c1c040da96f2cec", "duckdb": "3f71ab5f3c1c040da96f2cec", "generic": "3f71ab5f3c1c040da96f2cec", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3f71ab5f3c1c040da96f2cec", +"motherduck": "3f71ab5f3c1c040da96f2cec", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3f71ab5f3c1c040da96f2cec", "postgresql": "3f71ab5f3c1c040da96f2cec", "snowflake": "3f71ab5f3c1c040da96f2cec", @@ -20765,8 +20765,8 @@ "databricks": "d4a375fcf7c3190951992469", "duckdb": "d4a375fcf7c3190951992469", "generic": "d4a375fcf7c3190951992469", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d4a375fcf7c3190951992469", +"motherduck": "d4a375fcf7c3190951992469", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d4a375fcf7c3190951992469", "postgresql": "d4a375fcf7c3190951992469", "snowflake": "6306b9e88f7b13dbb6a32c00", @@ -20782,8 +20782,8 @@ "databricks": "78709b3317abd96cc20340f7", "duckdb": "78709b3317abd96cc20340f7", "generic": "78709b3317abd96cc20340f7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "78709b3317abd96cc20340f7", +"motherduck": "78709b3317abd96cc20340f7", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "78709b3317abd96cc20340f7", "postgresql": "78709b3317abd96cc20340f7", "snowflake": "78709b3317abd96cc20340f7", @@ -20799,8 +20799,8 @@ "databricks": "652ff8b21f638eac8fdc46c5", "duckdb": "652ff8b21f638eac8fdc46c5", "generic": "652ff8b21f638eac8fdc46c5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "652ff8b21f638eac8fdc46c5", +"motherduck": "652ff8b21f638eac8fdc46c5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "652ff8b21f638eac8fdc46c5", "postgresql": "652ff8b21f638eac8fdc46c5", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -20816,8 +20816,8 @@ "databricks": "293fdb322059805900912bee", "duckdb": "293fdb322059805900912bee", "generic": "293fdb322059805900912bee", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "293fdb322059805900912bee", +"motherduck": "293fdb322059805900912bee", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "293fdb322059805900912bee", "postgresql": "293fdb322059805900912bee", "snowflake": "293fdb322059805900912bee", @@ -20833,8 +20833,8 @@ "databricks": "3d23f811a03db519b598c815", "duckdb": "3d23f811a03db519b598c815", "generic": "3d23f811a03db519b598c815", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3d23f811a03db519b598c815", +"motherduck": "3d23f811a03db519b598c815", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3d23f811a03db519b598c815", "postgresql": "3d23f811a03db519b598c815", "snowflake": "3d23f811a03db519b598c815", @@ -20850,8 +20850,8 @@ "databricks": "1231fdcee1164056bb998fd8", "duckdb": "9cf8454127727005f919e6fc", "generic": "9a5c2bd43f94970e2d4d2210", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "84e2dbd148c2ae9480447652", +"motherduck": "9cf8454127727005f919e6fc", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "eaa1040a087ac34088fbabf6", "postgresql": "eaa1040a087ac34088fbabf6", "snowflake": "143732e0372eaf73cb82cfe7", @@ -20867,8 +20867,8 @@ "databricks": "2166772bfafca596e1de9d10", "duckdb": "2166772bfafca596e1de9d10", "generic": "2166772bfafca596e1de9d10", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2166772bfafca596e1de9d10", +"motherduck": "2166772bfafca596e1de9d10", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2166772bfafca596e1de9d10", "postgresql": "2166772bfafca596e1de9d10", "snowflake": "2166772bfafca596e1de9d10", @@ -20884,8 +20884,8 @@ "databricks": "8441d00030df06ad6a5534d3", "duckdb": "8441d00030df06ad6a5534d3", "generic": "8441d00030df06ad6a5534d3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8441d00030df06ad6a5534d3", +"motherduck": "8441d00030df06ad6a5534d3", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8441d00030df06ad6a5534d3", "postgresql": "8441d00030df06ad6a5534d3", "snowflake": "97a20344d811fb77c7690863", @@ -20901,8 +20901,8 @@ "databricks": "013a8446f043affffcf194f8", "duckdb": "013a8446f043affffcf194f8", "generic": "013a8446f043affffcf194f8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "013a8446f043affffcf194f8", +"motherduck": "013a8446f043affffcf194f8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "013a8446f043affffcf194f8", "postgresql": "013a8446f043affffcf194f8", "snowflake": "97a20344d811fb77c7690863", @@ -20918,8 +20918,8 @@ "databricks": "461923a0d34defd0c726d6e5", "duckdb": "461923a0d34defd0c726d6e5", "generic": "461923a0d34defd0c726d6e5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "461923a0d34defd0c726d6e5", +"motherduck": "461923a0d34defd0c726d6e5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "461923a0d34defd0c726d6e5", "postgresql": "461923a0d34defd0c726d6e5", "snowflake": "461923a0d34defd0c726d6e5", @@ -20935,8 +20935,8 @@ "databricks": "dcbfb83ead5b0e32e500635b", "duckdb": "dcbfb83ead5b0e32e500635b", "generic": "dcbfb83ead5b0e32e500635b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "dcbfb83ead5b0e32e500635b", +"motherduck": "dcbfb83ead5b0e32e500635b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "dcbfb83ead5b0e32e500635b", "postgresql": "dcbfb83ead5b0e32e500635b", "snowflake": "dcbfb83ead5b0e32e500635b", @@ -20952,8 +20952,8 @@ "databricks": "a686bb6db17ab079cfc79224", "duckdb": "a686bb6db17ab079cfc79224", "generic": "a686bb6db17ab079cfc79224", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a686bb6db17ab079cfc79224", +"motherduck": "a686bb6db17ab079cfc79224", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "74a30f6aa9def0b58afc616c", "postgresql": "74a30f6aa9def0b58afc616c", "snowflake": "1096c3c8687c8e8eb538ff74", @@ -20969,8 +20969,8 @@ "databricks": "e2879cf4770e7f48d2dd704a", "duckdb": "9d006c5299feb04426ce31ac", "generic": "e2879cf4770e7f48d2dd704a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e2879cf4770e7f48d2dd704a", +"motherduck": "9d006c5299feb04426ce31ac", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2d4aea8dec5521e258596f3d", "postgresql": "2d4aea8dec5521e258596f3d", "snowflake": "97a20344d811fb77c7690863", @@ -20986,8 +20986,8 @@ "databricks": "d2c97d221599d038d9875873", "duckdb": "d2c97d221599d038d9875873", "generic": "d2c97d221599d038d9875873", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d2c97d221599d038d9875873", +"motherduck": "d2c97d221599d038d9875873", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "55155766823b6a3a5e882fc2", "postgresql": "55155766823b6a3a5e882fc2", "snowflake": "d2c97d221599d038d9875873", @@ -21003,8 +21003,8 @@ "databricks": "013a8446f043affffcf194f8", "duckdb": "013a8446f043affffcf194f8", "generic": "013a8446f043affffcf194f8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "013a8446f043affffcf194f8", +"motherduck": "013a8446f043affffcf194f8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "013a8446f043affffcf194f8", "postgresql": "013a8446f043affffcf194f8", "snowflake": "97a20344d811fb77c7690863", @@ -21020,8 +21020,8 @@ "databricks": "c9b810639795a7bfab91b98d", "duckdb": "a428302e4848ffe877284c84", "generic": "c9b810639795a7bfab91b98d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c9b810639795a7bfab91b98d", +"motherduck": "a428302e4848ffe877284c84", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a428302e4848ffe877284c84", "postgresql": "a428302e4848ffe877284c84", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -21037,8 +21037,8 @@ "databricks": "8790273499dae4e6cbd2d034", "duckdb": "8790273499dae4e6cbd2d034", "generic": "8790273499dae4e6cbd2d034", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8790273499dae4e6cbd2d034", +"motherduck": "8790273499dae4e6cbd2d034", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8790273499dae4e6cbd2d034", "postgresql": "8790273499dae4e6cbd2d034", "snowflake": "9aff7fcc7d550e1a8d105471", @@ -21054,8 +21054,8 @@ "databricks": "e3bc1b8bad22d8104417b00f", "duckdb": "19e127c23b4598727a2d8ceb", "generic": "19e127c23b4598727a2d8ceb", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "aed0419e7f2ce7c0a8892248", +"motherduck": "19e127c23b4598727a2d8ceb", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "111af35848fe500ec2ae54b2", "postgresql": "111af35848fe500ec2ae54b2", "snowflake": "aed0419e7f2ce7c0a8892248", @@ -21071,8 +21071,8 @@ "databricks": "1e75e5227bea0ae294ac7af1", "duckdb": "19bdd92ea0cc5fb028657510", "generic": "b8321619b794db091c9e96a1", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b8321619b794db091c9e96a1", +"motherduck": "19bdd92ea0cc5fb028657510", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b8321619b794db091c9e96a1", "postgresql": "b8321619b794db091c9e96a1", "snowflake": "b8321619b794db091c9e96a1", @@ -21088,8 +21088,8 @@ "databricks": "bce60580ef3a1dc4ce569fff", "duckdb": "bce60580ef3a1dc4ce569fff", "generic": "bce60580ef3a1dc4ce569fff", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "bce60580ef3a1dc4ce569fff", +"motherduck": "bce60580ef3a1dc4ce569fff", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "eee236ea1427bdddbaf088ac", "postgresql": "eee236ea1427bdddbaf088ac", "snowflake": "bce60580ef3a1dc4ce569fff", @@ -21105,8 +21105,8 @@ "databricks": "d61c83ef8bfdf043fa672cb5", "duckdb": "305aaf0b95cee629dd46380c", "generic": "d61c83ef8bfdf043fa672cb5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d61c83ef8bfdf043fa672cb5", +"motherduck": "305aaf0b95cee629dd46380c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d61c83ef8bfdf043fa672cb5", "postgresql": "d61c83ef8bfdf043fa672cb5", "snowflake": "97a20344d811fb77c7690863", @@ -21122,8 +21122,8 @@ "databricks": "73ed909ac0c80ba48ae6d17c", "duckdb": "73ed909ac0c80ba48ae6d17c", "generic": "73ed909ac0c80ba48ae6d17c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "73ed909ac0c80ba48ae6d17c", +"motherduck": "73ed909ac0c80ba48ae6d17c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "73ed909ac0c80ba48ae6d17c", "postgresql": "73ed909ac0c80ba48ae6d17c", "snowflake": "73ed909ac0c80ba48ae6d17c", @@ -21139,8 +21139,8 @@ "databricks": "c6654395c62d21b415299f72", "duckdb": "0712c33a016f7dd6a37ee011", "generic": "c6654395c62d21b415299f72", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c6654395c62d21b415299f72", +"motherduck": "0712c33a016f7dd6a37ee011", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c6654395c62d21b415299f72", "postgresql": "c6654395c62d21b415299f72", "snowflake": "0712c33a016f7dd6a37ee011", @@ -21156,8 +21156,8 @@ "databricks": "c50a316d091c821866e4bfba", "duckdb": "c50a316d091c821866e4bfba", "generic": "c50a316d091c821866e4bfba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -21173,8 +21173,8 @@ "databricks": "5abf4ec799a2ec32fb10da7e", "duckdb": "5abf4ec799a2ec32fb10da7e", "generic": "5abf4ec799a2ec32fb10da7e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5abf4ec799a2ec32fb10da7e", +"motherduck": "5abf4ec799a2ec32fb10da7e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5abf4ec799a2ec32fb10da7e", "postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "a5656d70e208254d1d3e9b4f", @@ -21190,8 +21190,8 @@ "databricks": "53100c87dc0e46dc31d2c6cc", "duckdb": "53100c87dc0e46dc31d2c6cc", "generic": "53100c87dc0e46dc31d2c6cc", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "53100c87dc0e46dc31d2c6cc", +"motherduck": "53100c87dc0e46dc31d2c6cc", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "53100c87dc0e46dc31d2c6cc", "postgresql": "53100c87dc0e46dc31d2c6cc", "snowflake": "53100c87dc0e46dc31d2c6cc", @@ -21207,8 +21207,8 @@ "databricks": "6992a9c8899f9fe5de26869c", "duckdb": "081a4ec9ca8c1ffbb8c41e4d", "generic": "6992a9c8899f9fe5de26869c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "081a4ec9ca8c1ffbb8c41e4d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "081a4ec9ca8c1ffbb8c41e4d", "postgresql": "081a4ec9ca8c1ffbb8c41e4d", "snowflake": "808e51c2deac58e29ecb851e", @@ -21224,8 +21224,8 @@ "databricks": "0c4610f182f18bf41d306e01", "duckdb": "0c4610f182f18bf41d306e01", "generic": "0c4610f182f18bf41d306e01", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0c4610f182f18bf41d306e01", +"motherduck": "0c4610f182f18bf41d306e01", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0c4610f182f18bf41d306e01", "postgresql": "0c4610f182f18bf41d306e01", "snowflake": "0c4610f182f18bf41d306e01", @@ -21241,8 +21241,8 @@ "databricks": "e068bc199d71601c823bb254", "duckdb": "e068bc199d71601c823bb254", "generic": "e068bc199d71601c823bb254", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e068bc199d71601c823bb254", +"motherduck": "e068bc199d71601c823bb254", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e068bc199d71601c823bb254", "postgresql": "e068bc199d71601c823bb254", "snowflake": "e068bc199d71601c823bb254", @@ -21258,8 +21258,8 @@ "databricks": "18036ada4276b0930d870788", "duckdb": "18036ada4276b0930d870788", "generic": "18036ada4276b0930d870788", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "18036ada4276b0930d870788", +"motherduck": "18036ada4276b0930d870788", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "899d81b12a6c3aed6cf9dd53", "postgresql": "899d81b12a6c3aed6cf9dd53", "snowflake": "18036ada4276b0930d870788", @@ -21275,8 +21275,8 @@ "databricks": "360298d6139a1e6f476862de", "duckdb": "3b86998e2968bafafb02d6c0", "generic": "2f6a1345f274f18462c576a9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "3b86998e2968bafafb02d6c0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3b86998e2968bafafb02d6c0", "postgresql": "3b86998e2968bafafb02d6c0", "snowflake": "f5272904633fd6945c70dc00", @@ -21292,8 +21292,8 @@ "databricks": "c6889f2965db73017be6f6e7", "duckdb": "c6889f2965db73017be6f6e7", "generic": "c6889f2965db73017be6f6e7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c6889f2965db73017be6f6e7", +"motherduck": "c6889f2965db73017be6f6e7", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c6889f2965db73017be6f6e7", "postgresql": "c6889f2965db73017be6f6e7", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -21309,8 +21309,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1eb7fb4e8cf75773c5573562", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1eb7fb4e8cf75773c5573562", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -21326,8 +21326,8 @@ "databricks": "93fcb7d3c2979f1531daccf5", "duckdb": "93fcb7d3c2979f1531daccf5", "generic": "93fcb7d3c2979f1531daccf5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "93fcb7d3c2979f1531daccf5", +"motherduck": "93fcb7d3c2979f1531daccf5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "93fcb7d3c2979f1531daccf5", "postgresql": "93fcb7d3c2979f1531daccf5", "snowflake": "93fcb7d3c2979f1531daccf5", @@ -21343,8 +21343,8 @@ "databricks": "0475b57e9e9da81e87fa09ba", "duckdb": "0475b57e9e9da81e87fa09ba", "generic": "0475b57e9e9da81e87fa09ba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0475b57e9e9da81e87fa09ba", +"motherduck": "0475b57e9e9da81e87fa09ba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0475b57e9e9da81e87fa09ba", "postgresql": "0475b57e9e9da81e87fa09ba", "snowflake": "0475b57e9e9da81e87fa09ba", @@ -21360,8 +21360,8 @@ "databricks": "f35059f8984f0ac12ad1dc75", "duckdb": "f35059f8984f0ac12ad1dc75", "generic": "f35059f8984f0ac12ad1dc75", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f35059f8984f0ac12ad1dc75", +"motherduck": "f35059f8984f0ac12ad1dc75", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f35059f8984f0ac12ad1dc75", "postgresql": "f35059f8984f0ac12ad1dc75", "snowflake": "f35059f8984f0ac12ad1dc75", @@ -21377,8 +21377,8 @@ "databricks": "5aa51d84ac5e6c7a9306f348", "duckdb": "5aa51d84ac5e6c7a9306f348", "generic": "5aa51d84ac5e6c7a9306f348", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5aa51d84ac5e6c7a9306f348", +"motherduck": "5aa51d84ac5e6c7a9306f348", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5aa51d84ac5e6c7a9306f348", "postgresql": "5aa51d84ac5e6c7a9306f348", "snowflake": "5aa51d84ac5e6c7a9306f348", @@ -21394,8 +21394,8 @@ "databricks": "3dbbe73ee4310c73030e8110", "duckdb": "3dbbe73ee4310c73030e8110", "generic": "3dbbe73ee4310c73030e8110", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3dbbe73ee4310c73030e8110", +"motherduck": "3dbbe73ee4310c73030e8110", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3dbbe73ee4310c73030e8110", "postgresql": "3dbbe73ee4310c73030e8110", "snowflake": "f43dac38f34a1c0618a3f812", @@ -21411,8 +21411,8 @@ "databricks": "f43dac38f34a1c0618a3f812", "duckdb": "f43dac38f34a1c0618a3f812", "generic": "f43dac38f34a1c0618a3f812", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f43dac38f34a1c0618a3f812", +"motherduck": "f43dac38f34a1c0618a3f812", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f43dac38f34a1c0618a3f812", "postgresql": "f43dac38f34a1c0618a3f812", "snowflake": "f43dac38f34a1c0618a3f812", @@ -21428,8 +21428,8 @@ "databricks": "42a76d5ff05d0f91911ff368", "duckdb": "42a76d5ff05d0f91911ff368", "generic": "42a76d5ff05d0f91911ff368", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "42a76d5ff05d0f91911ff368", +"motherduck": "42a76d5ff05d0f91911ff368", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "42a76d5ff05d0f91911ff368", "postgresql": "42a76d5ff05d0f91911ff368", "snowflake": "42a76d5ff05d0f91911ff368", @@ -21445,8 +21445,8 @@ "databricks": "7cd31289a5b99b5a5e24492d", "duckdb": "7cd31289a5b99b5a5e24492d", "generic": "7cd31289a5b99b5a5e24492d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "7cd31289a5b99b5a5e24492d", +"motherduck": "7cd31289a5b99b5a5e24492d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c5bdaf88df7ef3d9ae693106", "postgresql": "c5bdaf88df7ef3d9ae693106", "snowflake": "7cd31289a5b99b5a5e24492d", @@ -21462,8 +21462,8 @@ "databricks": "cbd04e906c540e91d7c8ea88", "duckdb": "cbd04e906c540e91d7c8ea88", "generic": "cbd04e906c540e91d7c8ea88", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "cbd04e906c540e91d7c8ea88", +"motherduck": "cbd04e906c540e91d7c8ea88", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "cbd04e906c540e91d7c8ea88", "postgresql": "cbd04e906c540e91d7c8ea88", "snowflake": "cbd04e906c540e91d7c8ea88", @@ -21479,8 +21479,8 @@ "databricks": "7177019d5faf5b3046623257", "duckdb": "7177019d5faf5b3046623257", "generic": "7177019d5faf5b3046623257", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "7177019d5faf5b3046623257", +"motherduck": "7177019d5faf5b3046623257", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7177019d5faf5b3046623257", "postgresql": "7177019d5faf5b3046623257", "snowflake": "7177019d5faf5b3046623257", @@ -21496,8 +21496,8 @@ "databricks": "277693b7c19b2696f01403ba", "duckdb": "277693b7c19b2696f01403ba", "generic": "277693b7c19b2696f01403ba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "277693b7c19b2696f01403ba", +"motherduck": "277693b7c19b2696f01403ba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "277693b7c19b2696f01403ba", "postgresql": "277693b7c19b2696f01403ba", "snowflake": "277693b7c19b2696f01403ba", @@ -21513,8 +21513,8 @@ "databricks": "c50a316d091c821866e4bfba", "duckdb": "c50a316d091c821866e4bfba", "generic": "c50a316d091c821866e4bfba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -21530,8 +21530,8 @@ "databricks": "1bdf5908ca249b2331a0427c", "duckdb": "1bdf5908ca249b2331a0427c", "generic": "1bdf5908ca249b2331a0427c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1bdf5908ca249b2331a0427c", +"motherduck": "1bdf5908ca249b2331a0427c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1bdf5908ca249b2331a0427c", "postgresql": "1bdf5908ca249b2331a0427c", "snowflake": "8ff614b7306c0edbe36ce00b", @@ -21547,8 +21547,8 @@ "databricks": "e1c74d96ac578f9323bf1bbd", "duckdb": "e1c74d96ac578f9323bf1bbd", "generic": "e1c74d96ac578f9323bf1bbd", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e1c74d96ac578f9323bf1bbd", +"motherduck": "e1c74d96ac578f9323bf1bbd", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e1c74d96ac578f9323bf1bbd", "postgresql": "e1c74d96ac578f9323bf1bbd", "snowflake": "e1c74d96ac578f9323bf1bbd", @@ -21564,8 +21564,8 @@ "databricks": "cafb5cff5526dd419baad0ed", "duckdb": "cafb5cff5526dd419baad0ed", "generic": "cafb5cff5526dd419baad0ed", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "cafb5cff5526dd419baad0ed", +"motherduck": "cafb5cff5526dd419baad0ed", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "cafb5cff5526dd419baad0ed", "postgresql": "cafb5cff5526dd419baad0ed", "snowflake": "cafb5cff5526dd419baad0ed", @@ -21581,8 +21581,8 @@ "databricks": "e8b6e0bd2ded7b9eec6209e4", "duckdb": "e8b6e0bd2ded7b9eec6209e4", "generic": "e8b6e0bd2ded7b9eec6209e4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e8b6e0bd2ded7b9eec6209e4", +"motherduck": "e8b6e0bd2ded7b9eec6209e4", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e8b6e0bd2ded7b9eec6209e4", "postgresql": "e8b6e0bd2ded7b9eec6209e4", "snowflake": "e8b6e0bd2ded7b9eec6209e4", @@ -21598,8 +21598,8 @@ "databricks": "b2c9b6ebca6c097f3caa0aa8", "duckdb": "b2c9b6ebca6c097f3caa0aa8", "generic": "b2c9b6ebca6c097f3caa0aa8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b2c9b6ebca6c097f3caa0aa8", +"motherduck": "b2c9b6ebca6c097f3caa0aa8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b2c9b6ebca6c097f3caa0aa8", "postgresql": "b2c9b6ebca6c097f3caa0aa8", "snowflake": "b2c9b6ebca6c097f3caa0aa8", @@ -21615,8 +21615,8 @@ "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fe23594c781e79cbb0c8927a", +"motherduck": "fe23594c781e79cbb0c8927a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8a3f094f18fb4047f0febf59", "postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", @@ -21632,8 +21632,8 @@ "databricks": "18db5afe2c1e80d1ee652b54", "duckdb": "18db5afe2c1e80d1ee652b54", "generic": "18db5afe2c1e80d1ee652b54", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "18db5afe2c1e80d1ee652b54", +"motherduck": "18db5afe2c1e80d1ee652b54", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "18db5afe2c1e80d1ee652b54", "postgresql": "18db5afe2c1e80d1ee652b54", "snowflake": "18db5afe2c1e80d1ee652b54", @@ -21649,8 +21649,8 @@ "databricks": "5524945ebcb15c363d317b3c", "duckdb": "219314eaf94cabfe1c7e559c", "generic": "4aab4bdd20039d5e16058b1b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "219314eaf94cabfe1c7e559c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "219314eaf94cabfe1c7e559c", "postgresql": "219314eaf94cabfe1c7e559c", "snowflake": "b767a0334d4c0090a22665f5", @@ -21666,8 +21666,8 @@ "databricks": "84e86bec7646b6ad19ceef59", "duckdb": "6a2162b5abe5a73734c47041", "generic": "6a2162b5abe5a73734c47041", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6a2162b5abe5a73734c47041", +"motherduck": "6a2162b5abe5a73734c47041", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "6a2162b5abe5a73734c47041", "postgresql": "6a2162b5abe5a73734c47041", "snowflake": "16d902e899040d0c0c9dcf58", @@ -21683,8 +21683,8 @@ "databricks": "8790273499dae4e6cbd2d034", "duckdb": "8790273499dae4e6cbd2d034", "generic": "8790273499dae4e6cbd2d034", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8790273499dae4e6cbd2d034", +"motherduck": "8790273499dae4e6cbd2d034", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8790273499dae4e6cbd2d034", "postgresql": "8790273499dae4e6cbd2d034", "snowflake": "9aff7fcc7d550e1a8d105471", @@ -21700,8 +21700,8 @@ "databricks": "669084d1ea704ae5b8ee05c2", "duckdb": "669084d1ea704ae5b8ee05c2", "generic": "669084d1ea704ae5b8ee05c2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "669084d1ea704ae5b8ee05c2", +"motherduck": "669084d1ea704ae5b8ee05c2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "93c1b5b5960e26479baf88d0", "postgresql": "93c1b5b5960e26479baf88d0", "snowflake": "669084d1ea704ae5b8ee05c2", @@ -21717,8 +21717,8 @@ "databricks": "77298ddff3580b41e361ba6d", "duckdb": "bf74994a132713cd5a93b70b", "generic": "74d4939af66c472f17ecfa08", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ce873d09a353281e316461aa", +"motherduck": "bf74994a132713cd5a93b70b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "cf182821f911fadedb1f868d", "postgresql": "cf182821f911fadedb1f868d", "snowflake": "5c5b258591d9f7cb305a4220", @@ -21734,8 +21734,8 @@ "databricks": "b66ef401bfc62ea5f67697f0", "duckdb": "b66ef401bfc62ea5f67697f0", "generic": "b66ef401bfc62ea5f67697f0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b66ef401bfc62ea5f67697f0", +"motherduck": "b66ef401bfc62ea5f67697f0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b66ef401bfc62ea5f67697f0", "postgresql": "b66ef401bfc62ea5f67697f0", "snowflake": "b66ef401bfc62ea5f67697f0", @@ -21751,8 +21751,8 @@ "databricks": "8203bd5901a8ec71ff7c103e", "duckdb": "8203bd5901a8ec71ff7c103e", "generic": "8203bd5901a8ec71ff7c103e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8203bd5901a8ec71ff7c103e", +"motherduck": "8203bd5901a8ec71ff7c103e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8203bd5901a8ec71ff7c103e", "postgresql": "8203bd5901a8ec71ff7c103e", "snowflake": "8203bd5901a8ec71ff7c103e", @@ -21768,8 +21768,8 @@ "databricks": "aab0389e2b5470136660bf8a", "duckdb": "aab0389e2b5470136660bf8a", "generic": "aab0389e2b5470136660bf8a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "aab0389e2b5470136660bf8a", +"motherduck": "aab0389e2b5470136660bf8a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f16105cbc61d547e6c08efc8", "postgresql": "f16105cbc61d547e6c08efc8", "snowflake": "97a20344d811fb77c7690863", @@ -21785,8 +21785,8 @@ "databricks": "6199046833cb1b5de6e50c1f", "duckdb": "6199046833cb1b5de6e50c1f", "generic": "6199046833cb1b5de6e50c1f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6199046833cb1b5de6e50c1f", +"motherduck": "6199046833cb1b5de6e50c1f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "6199046833cb1b5de6e50c1f", "postgresql": "6199046833cb1b5de6e50c1f", "snowflake": "6199046833cb1b5de6e50c1f", @@ -21802,8 +21802,8 @@ "databricks": "f1158467f7ab7b47d7e993d0", "duckdb": "f1158467f7ab7b47d7e993d0", "generic": "f1158467f7ab7b47d7e993d0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f1158467f7ab7b47d7e993d0", +"motherduck": "f1158467f7ab7b47d7e993d0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f1158467f7ab7b47d7e993d0", "postgresql": "f1158467f7ab7b47d7e993d0", "snowflake": "f1158467f7ab7b47d7e993d0", @@ -21819,8 +21819,8 @@ "databricks": "a33d60d9b009878ab623c1d8", "duckdb": "23f70cb2333740d79361d52e", "generic": "a33d60d9b009878ab623c1d8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a33d60d9b009878ab623c1d8", +"motherduck": "23f70cb2333740d79361d52e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f276e907770196606577c932", "postgresql": "f276e907770196606577c932", "snowflake": "23f70cb2333740d79361d52e", @@ -21836,8 +21836,8 @@ "databricks": "c50a316d091c821866e4bfba", "duckdb": "c50a316d091c821866e4bfba", "generic": "c50a316d091c821866e4bfba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -21853,8 +21853,8 @@ "databricks": "669084d1ea704ae5b8ee05c2", "duckdb": "669084d1ea704ae5b8ee05c2", "generic": "669084d1ea704ae5b8ee05c2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "669084d1ea704ae5b8ee05c2", +"motherduck": "669084d1ea704ae5b8ee05c2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "93c1b5b5960e26479baf88d0", "postgresql": "93c1b5b5960e26479baf88d0", "snowflake": "669084d1ea704ae5b8ee05c2", @@ -21870,8 +21870,8 @@ "databricks": "527932781ba4bab32cb5e881", "duckdb": "527932781ba4bab32cb5e881", "generic": "527932781ba4bab32cb5e881", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "527932781ba4bab32cb5e881", +"motherduck": "527932781ba4bab32cb5e881", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "527932781ba4bab32cb5e881", "postgresql": "527932781ba4bab32cb5e881", "snowflake": "527932781ba4bab32cb5e881", @@ -21887,8 +21887,8 @@ "databricks": "623dbde002b57284f477b0b5", "duckdb": "623dbde002b57284f477b0b5", "generic": "623dbde002b57284f477b0b5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "623dbde002b57284f477b0b5", +"motherduck": "623dbde002b57284f477b0b5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "623dbde002b57284f477b0b5", "postgresql": "623dbde002b57284f477b0b5", "snowflake": "623dbde002b57284f477b0b5", @@ -21904,8 +21904,8 @@ "databricks": "a12a2ad8c11bbf6a41794d33", "duckdb": "a12a2ad8c11bbf6a41794d33", "generic": "a12a2ad8c11bbf6a41794d33", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a12a2ad8c11bbf6a41794d33", +"motherduck": "a12a2ad8c11bbf6a41794d33", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a12a2ad8c11bbf6a41794d33", "postgresql": "a12a2ad8c11bbf6a41794d33", "snowflake": "a12a2ad8c11bbf6a41794d33", @@ -21921,8 +21921,8 @@ "databricks": "558da38ec4d9d5d6c72f60cf", "duckdb": "558da38ec4d9d5d6c72f60cf", "generic": "558da38ec4d9d5d6c72f60cf", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "558da38ec4d9d5d6c72f60cf", +"motherduck": "558da38ec4d9d5d6c72f60cf", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "558da38ec4d9d5d6c72f60cf", "postgresql": "558da38ec4d9d5d6c72f60cf", "snowflake": "558da38ec4d9d5d6c72f60cf", @@ -21938,8 +21938,8 @@ "databricks": "9a63ad6168418af23d9b6fd1", "duckdb": "363159f251e264604338e690", "generic": "9a63ad6168418af23d9b6fd1", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9a63ad6168418af23d9b6fd1", +"motherduck": "363159f251e264604338e690", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f276e907770196606577c932", "postgresql": "f276e907770196606577c932", "snowflake": "363159f251e264604338e690", @@ -21955,8 +21955,8 @@ "databricks": "b6200c25bc3749f5724d237c", "duckdb": "b6200c25bc3749f5724d237c", "generic": "b6200c25bc3749f5724d237c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b6200c25bc3749f5724d237c", +"motherduck": "b6200c25bc3749f5724d237c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b6200c25bc3749f5724d237c", "postgresql": "b6200c25bc3749f5724d237c", "snowflake": "b6200c25bc3749f5724d237c", @@ -21972,8 +21972,8 @@ "databricks": "e57ac1541fb51b362cd5b470", "duckdb": "e57ac1541fb51b362cd5b470", "generic": "e57ac1541fb51b362cd5b470", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e57ac1541fb51b362cd5b470", +"motherduck": "e57ac1541fb51b362cd5b470", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e57ac1541fb51b362cd5b470", "postgresql": "e57ac1541fb51b362cd5b470", "snowflake": "4b356542044f1267fcfc487a", @@ -21989,8 +21989,8 @@ "databricks": "2bfa352725f9d8a91250f75e", "duckdb": "2bfa352725f9d8a91250f75e", "generic": "2bfa352725f9d8a91250f75e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2bfa352725f9d8a91250f75e", +"motherduck": "2bfa352725f9d8a91250f75e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2bfa352725f9d8a91250f75e", "postgresql": "2bfa352725f9d8a91250f75e", "snowflake": "2bfa352725f9d8a91250f75e", @@ -22006,8 +22006,8 @@ "databricks": "1071df52a9e8350ac0c2cd33", "duckdb": "129cd0245509c52e0d7ac815", "generic": "1071df52a9e8350ac0c2cd33", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1071df52a9e8350ac0c2cd33", +"motherduck": "129cd0245509c52e0d7ac815", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1071df52a9e8350ac0c2cd33", "postgresql": "1071df52a9e8350ac0c2cd33", "snowflake": "24b68111659c8d395da7df6a", @@ -22023,8 +22023,8 @@ "databricks": "3aac0f59706540c7a59cbce4", "duckdb": "3d2b3b1097973d173f205074", "generic": "3aac0f59706540c7a59cbce4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "3d2b3b1097973d173f205074", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3d2b3b1097973d173f205074", "postgresql": "3d2b3b1097973d173f205074", "snowflake": "0a5fcaec13e26e3caacd5345", @@ -22040,8 +22040,8 @@ "databricks": "94006e4eb73bd1f728de7a83", "duckdb": "2f6203391b0efaaf0a3c9d00", "generic": "2f6203391b0efaaf0a3c9d00", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2f6203391b0efaaf0a3c9d00", +"motherduck": "2f6203391b0efaaf0a3c9d00", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2f6203391b0efaaf0a3c9d00", "postgresql": "2f6203391b0efaaf0a3c9d00", "snowflake": "dc61037c72b2e7b4730d26cc", @@ -22057,8 +22057,8 @@ "databricks": "6a534f7a641457b40e35d984", "duckdb": "6a534f7a641457b40e35d984", "generic": "6a534f7a641457b40e35d984", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6a534f7a641457b40e35d984", +"motherduck": "6a534f7a641457b40e35d984", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "6a534f7a641457b40e35d984", "postgresql": "6a534f7a641457b40e35d984", "snowflake": "6a534f7a641457b40e35d984", @@ -22074,8 +22074,8 @@ "databricks": "60b39a68a18438603dd5496d", "duckdb": "60b39a68a18438603dd5496d", "generic": "60b39a68a18438603dd5496d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "60b39a68a18438603dd5496d", +"motherduck": "60b39a68a18438603dd5496d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7a007e6ff9e9465c9d7fadea", "postgresql": "7a007e6ff9e9465c9d7fadea", "snowflake": "60b39a68a18438603dd5496d", @@ -22091,8 +22091,8 @@ "databricks": "126765943545b2608ebe7128", "duckdb": "126765943545b2608ebe7128", "generic": "126765943545b2608ebe7128", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "126765943545b2608ebe7128", +"motherduck": "126765943545b2608ebe7128", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "126765943545b2608ebe7128", "postgresql": "126765943545b2608ebe7128", "snowflake": "126765943545b2608ebe7128", @@ -22108,8 +22108,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1eb7fb4e8cf75773c5573562", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1eb7fb4e8cf75773c5573562", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -22125,8 +22125,8 @@ "databricks": "9c2cff8be7225cf79883ca65", "duckdb": "9c2cff8be7225cf79883ca65", "generic": "9c2cff8be7225cf79883ca65", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9c2cff8be7225cf79883ca65", +"motherduck": "9c2cff8be7225cf79883ca65", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9c2cff8be7225cf79883ca65", "postgresql": "9c2cff8be7225cf79883ca65", "snowflake": "9c2cff8be7225cf79883ca65", @@ -22142,8 +22142,8 @@ "databricks": "cf05d378f282d0abe941a82f", "duckdb": "cf05d378f282d0abe941a82f", "generic": "cf05d378f282d0abe941a82f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "cf05d378f282d0abe941a82f", +"motherduck": "cf05d378f282d0abe941a82f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "cf05d378f282d0abe941a82f", "postgresql": "cf05d378f282d0abe941a82f", "snowflake": "cf05d378f282d0abe941a82f", @@ -22159,8 +22159,8 @@ "databricks": "4b356542044f1267fcfc487a", "duckdb": "5abf4ec799a2ec32fb10da7e", "generic": "4b356542044f1267fcfc487a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4b356542044f1267fcfc487a", +"motherduck": "5abf4ec799a2ec32fb10da7e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a5656d70e208254d1d3e9b4f", "postgresql": "a5656d70e208254d1d3e9b4f", "snowflake": "4b356542044f1267fcfc487a", @@ -22176,8 +22176,8 @@ "databricks": "d7b74701deb8b87c32deb7a6", "duckdb": "d7b74701deb8b87c32deb7a6", "generic": "d7b74701deb8b87c32deb7a6", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d7b74701deb8b87c32deb7a6", +"motherduck": "d7b74701deb8b87c32deb7a6", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d7b74701deb8b87c32deb7a6", "postgresql": "d7b74701deb8b87c32deb7a6", "snowflake": "d7b74701deb8b87c32deb7a6", @@ -22193,8 +22193,8 @@ "databricks": "078c0e4b25cdb89eb4ebd88f", "duckdb": "078c0e4b25cdb89eb4ebd88f", "generic": "078c0e4b25cdb89eb4ebd88f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "078c0e4b25cdb89eb4ebd88f", +"motherduck": "078c0e4b25cdb89eb4ebd88f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "078c0e4b25cdb89eb4ebd88f", "postgresql": "078c0e4b25cdb89eb4ebd88f", "snowflake": "6306b9e88f7b13dbb6a32c00", @@ -22210,8 +22210,8 @@ "databricks": "e46d4be44ab9e500e9190d26", "duckdb": "e46d4be44ab9e500e9190d26", "generic": "e46d4be44ab9e500e9190d26", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e46d4be44ab9e500e9190d26", +"motherduck": "e46d4be44ab9e500e9190d26", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "251aba3fef503ce64556c701", "postgresql": "251aba3fef503ce64556c701", "snowflake": "e46d4be44ab9e500e9190d26", @@ -22227,8 +22227,8 @@ "databricks": "85a8004226c2b77c6b697bab", "duckdb": "dcd0570bcd1ca6e7cbe9b206", "generic": "85a8004226c2b77c6b697bab", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "85a8004226c2b77c6b697bab", +"motherduck": "dcd0570bcd1ca6e7cbe9b206", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "85a8004226c2b77c6b697bab", "postgresql": "85a8004226c2b77c6b697bab", "snowflake": "339dae99bd73c126e700c594", @@ -22244,8 +22244,8 @@ "databricks": "00402a032fb29ce84ecc4615", "duckdb": "00402a032fb29ce84ecc4615", "generic": "00402a032fb29ce84ecc4615", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "00402a032fb29ce84ecc4615", +"motherduck": "00402a032fb29ce84ecc4615", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "00402a032fb29ce84ecc4615", "postgresql": "00402a032fb29ce84ecc4615", "snowflake": "00402a032fb29ce84ecc4615", @@ -22261,8 +22261,8 @@ "databricks": "52921fa22fa8a5b698ef368f", "duckdb": "52921fa22fa8a5b698ef368f", "generic": "52921fa22fa8a5b698ef368f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "52921fa22fa8a5b698ef368f", +"motherduck": "52921fa22fa8a5b698ef368f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "52921fa22fa8a5b698ef368f", "postgresql": "52921fa22fa8a5b698ef368f", "snowflake": "52921fa22fa8a5b698ef368f", @@ -22278,8 +22278,8 @@ "databricks": "4fb572024031cb7631777be1", "duckdb": "4114b18e7e26d16613331fa7", "generic": "4114b18e7e26d16613331fa7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4114b18e7e26d16613331fa7", +"motherduck": "4114b18e7e26d16613331fa7", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "4114b18e7e26d16613331fa7", "postgresql": "4114b18e7e26d16613331fa7", "snowflake": "4114b18e7e26d16613331fa7", @@ -22295,8 +22295,8 @@ "databricks": "5aa51d84ac5e6c7a9306f348", "duckdb": "5aa51d84ac5e6c7a9306f348", "generic": "5aa51d84ac5e6c7a9306f348", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5aa51d84ac5e6c7a9306f348", +"motherduck": "5aa51d84ac5e6c7a9306f348", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5aa51d84ac5e6c7a9306f348", "postgresql": "5aa51d84ac5e6c7a9306f348", "snowflake": "5aa51d84ac5e6c7a9306f348", @@ -22312,8 +22312,8 @@ "databricks": "58f6dfd7cabdc84b33dc2a96", "duckdb": "630c8b82ec44c7208a6df405", "generic": "fa5c71c0d2354f76510bf3e7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "630c8b82ec44c7208a6df405", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "630c8b82ec44c7208a6df405", "postgresql": "630c8b82ec44c7208a6df405", "snowflake": "8efe0c6c8c4444b8fe68129b", @@ -22329,8 +22329,8 @@ "databricks": "aab0389e2b5470136660bf8a", "duckdb": "aab0389e2b5470136660bf8a", "generic": "aab0389e2b5470136660bf8a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "aab0389e2b5470136660bf8a", +"motherduck": "aab0389e2b5470136660bf8a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f16105cbc61d547e6c08efc8", "postgresql": "f16105cbc61d547e6c08efc8", "snowflake": "97a20344d811fb77c7690863", @@ -22346,8 +22346,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1eb7fb4e8cf75773c5573562", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1eb7fb4e8cf75773c5573562", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -22363,8 +22363,8 @@ "databricks": "dbd808c57538b7d212ed9a7c", "duckdb": "dbd808c57538b7d212ed9a7c", "generic": "dbd808c57538b7d212ed9a7c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "dbd808c57538b7d212ed9a7c", +"motherduck": "dbd808c57538b7d212ed9a7c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "932963c69958d2a639768fd3", "postgresql": "932963c69958d2a639768fd3", "snowflake": "dbd808c57538b7d212ed9a7c", @@ -22380,8 +22380,8 @@ "databricks": "ebc821a95b9286e22c2b26d0", "duckdb": "ebc821a95b9286e22c2b26d0", "generic": "ebc821a95b9286e22c2b26d0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ebc821a95b9286e22c2b26d0", +"motherduck": "ebc821a95b9286e22c2b26d0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ebc821a95b9286e22c2b26d0", "postgresql": "ebc821a95b9286e22c2b26d0", "snowflake": "ebc821a95b9286e22c2b26d0", @@ -22397,8 +22397,8 @@ "databricks": "79278352c17c9de0f5a28af5", "duckdb": "79278352c17c9de0f5a28af5", "generic": "79278352c17c9de0f5a28af5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "79278352c17c9de0f5a28af5", +"motherduck": "79278352c17c9de0f5a28af5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "79278352c17c9de0f5a28af5", "postgresql": "79278352c17c9de0f5a28af5", "snowflake": "97a20344d811fb77c7690863", @@ -22414,8 +22414,8 @@ "databricks": "bbc7d9037d6f0383eb14d7eb", "duckdb": "bbc7d9037d6f0383eb14d7eb", "generic": "bbc7d9037d6f0383eb14d7eb", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "bbc7d9037d6f0383eb14d7eb", +"motherduck": "bbc7d9037d6f0383eb14d7eb", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "bbc7d9037d6f0383eb14d7eb", "postgresql": "bbc7d9037d6f0383eb14d7eb", "snowflake": "bbc7d9037d6f0383eb14d7eb", @@ -22431,8 +22431,8 @@ "databricks": "38b2ec94bf02be40007ce525", "duckdb": "38b2ec94bf02be40007ce525", "generic": "38b2ec94bf02be40007ce525", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "38b2ec94bf02be40007ce525", +"motherduck": "38b2ec94bf02be40007ce525", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "850deea990b52b82b36a9848", "postgresql": "850deea990b52b82b36a9848", "snowflake": "38b2ec94bf02be40007ce525", @@ -22448,8 +22448,8 @@ "databricks": "cb06a5553de70a4924ed6600", "duckdb": "cb06a5553de70a4924ed6600", "generic": "cb06a5553de70a4924ed6600", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "cb06a5553de70a4924ed6600", +"motherduck": "cb06a5553de70a4924ed6600", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "cb06a5553de70a4924ed6600", "postgresql": "cb06a5553de70a4924ed6600", "snowflake": "cb06a5553de70a4924ed6600", @@ -22465,8 +22465,8 @@ "databricks": "38b2ec94bf02be40007ce525", "duckdb": "38b2ec94bf02be40007ce525", "generic": "38b2ec94bf02be40007ce525", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "38b2ec94bf02be40007ce525", +"motherduck": "38b2ec94bf02be40007ce525", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "850deea990b52b82b36a9848", "postgresql": "850deea990b52b82b36a9848", "snowflake": "38b2ec94bf02be40007ce525", @@ -22482,8 +22482,8 @@ "databricks": "7cd31289a5b99b5a5e24492d", "duckdb": "7cd31289a5b99b5a5e24492d", "generic": "7cd31289a5b99b5a5e24492d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "7cd31289a5b99b5a5e24492d", +"motherduck": "7cd31289a5b99b5a5e24492d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c5bdaf88df7ef3d9ae693106", "postgresql": "c5bdaf88df7ef3d9ae693106", "snowflake": "7cd31289a5b99b5a5e24492d", @@ -22499,8 +22499,8 @@ "databricks": "4e7908098c87141abb62306c", "duckdb": "1eb7826ddab37fc01598a8e9", "generic": "4e7908098c87141abb62306c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "1eb7826ddab37fc01598a8e9", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8ca050e9a43027521099ab95", "postgresql": "8ca050e9a43027521099ab95", "snowflake": "92f647d5460e4e2e7e25fb6d", @@ -22516,8 +22516,8 @@ "databricks": "a0131c4099eb4915760bab4a", "duckdb": "a0131c4099eb4915760bab4a", "generic": "a0131c4099eb4915760bab4a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a0131c4099eb4915760bab4a", +"motherduck": "a0131c4099eb4915760bab4a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a0131c4099eb4915760bab4a", "postgresql": "a0131c4099eb4915760bab4a", "snowflake": "a0131c4099eb4915760bab4a", @@ -22533,8 +22533,8 @@ "databricks": "b561884132dbe7e42b719110", "duckdb": "b561884132dbe7e42b719110", "generic": "b561884132dbe7e42b719110", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b561884132dbe7e42b719110", +"motherduck": "b561884132dbe7e42b719110", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5aa51d84ac5e6c7a9306f348", "postgresql": "5aa51d84ac5e6c7a9306f348", "snowflake": "b561884132dbe7e42b719110", @@ -22550,8 +22550,8 @@ "databricks": "de89632e277dbc8c97115419", "duckdb": "5abf4ec799a2ec32fb10da7e", "generic": "de89632e277dbc8c97115419", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "de89632e277dbc8c97115419", +"motherduck": "5abf4ec799a2ec32fb10da7e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5abf4ec799a2ec32fb10da7e", "postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", @@ -22567,8 +22567,8 @@ "databricks": "a428302e4848ffe877284c84", "duckdb": "a428302e4848ffe877284c84", "generic": "a428302e4848ffe877284c84", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a428302e4848ffe877284c84", +"motherduck": "a428302e4848ffe877284c84", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a428302e4848ffe877284c84", "postgresql": "a428302e4848ffe877284c84", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -22584,8 +22584,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1eb7fb4e8cf75773c5573562", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1eb7fb4e8cf75773c5573562", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -22601,8 +22601,8 @@ "databricks": "86b7927f577e56d61eb9f879", "duckdb": "86b7927f577e56d61eb9f879", "generic": "86b7927f577e56d61eb9f879", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "86b7927f577e56d61eb9f879", +"motherduck": "86b7927f577e56d61eb9f879", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "86b7927f577e56d61eb9f879", "postgresql": "86b7927f577e56d61eb9f879", "snowflake": "86b7927f577e56d61eb9f879", @@ -22618,8 +22618,8 @@ "databricks": "c82815abe30b3d099903f838", "duckdb": "c82815abe30b3d099903f838", "generic": "c82815abe30b3d099903f838", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c82815abe30b3d099903f838", +"motherduck": "c82815abe30b3d099903f838", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c82815abe30b3d099903f838", "postgresql": "c82815abe30b3d099903f838", "snowflake": "c82815abe30b3d099903f838", @@ -22635,8 +22635,8 @@ "databricks": "7d23077fb72f7bac9a49a632", "duckdb": "7d23077fb72f7bac9a49a632", "generic": "7d23077fb72f7bac9a49a632", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "7d23077fb72f7bac9a49a632", +"motherduck": "7d23077fb72f7bac9a49a632", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7d23077fb72f7bac9a49a632", "postgresql": "7d23077fb72f7bac9a49a632", "snowflake": "7d23077fb72f7bac9a49a632", @@ -22652,8 +22652,8 @@ "databricks": "177c462b3a0270e894c8111c", "duckdb": "177c462b3a0270e894c8111c", "generic": "177c462b3a0270e894c8111c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "177c462b3a0270e894c8111c", +"motherduck": "177c462b3a0270e894c8111c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2625125fd986796ed33a61b7", "postgresql": "2625125fd986796ed33a61b7", "snowflake": "177c462b3a0270e894c8111c", @@ -22669,8 +22669,8 @@ "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fe23594c781e79cbb0c8927a", +"motherduck": "fe23594c781e79cbb0c8927a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "fe23594c781e79cbb0c8927a", "postgresql": "fe23594c781e79cbb0c8927a", "snowflake": "38b2ec94bf02be40007ce525", @@ -22686,8 +22686,8 @@ "databricks": "08736485547ba339930697e9", "duckdb": "08736485547ba339930697e9", "generic": "08736485547ba339930697e9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "08736485547ba339930697e9", +"motherduck": "08736485547ba339930697e9", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "eecd452b4f5f3f4133d0bd0a", "postgresql": "eecd452b4f5f3f4133d0bd0a", "snowflake": "08736485547ba339930697e9", @@ -22703,8 +22703,8 @@ "databricks": "8d81e8dacd544a5e07c3b1ea", "duckdb": "8d81e8dacd544a5e07c3b1ea", "generic": "8d81e8dacd544a5e07c3b1ea", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8d81e8dacd544a5e07c3b1ea", +"motherduck": "8d81e8dacd544a5e07c3b1ea", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8d81e8dacd544a5e07c3b1ea", "postgresql": "8d81e8dacd544a5e07c3b1ea", "snowflake": "8d81e8dacd544a5e07c3b1ea", @@ -22720,8 +22720,8 @@ "databricks": "3e611643f0beab4953c4e09b", "duckdb": "3e611643f0beab4953c4e09b", "generic": "3e611643f0beab4953c4e09b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3e611643f0beab4953c4e09b", +"motherduck": "3e611643f0beab4953c4e09b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "cae477ce59c72869f37a9a40", "postgresql": "cae477ce59c72869f37a9a40", "snowflake": "3e611643f0beab4953c4e09b", @@ -22737,8 +22737,8 @@ "databricks": "e2eee356d05f550b28bf85ad", "duckdb": "e2eee356d05f550b28bf85ad", "generic": "e2eee356d05f550b28bf85ad", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e2eee356d05f550b28bf85ad", +"motherduck": "e2eee356d05f550b28bf85ad", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e2eee356d05f550b28bf85ad", "postgresql": "e2eee356d05f550b28bf85ad", "snowflake": "e2eee356d05f550b28bf85ad", @@ -22754,8 +22754,8 @@ "databricks": "669084d1ea704ae5b8ee05c2", "duckdb": "669084d1ea704ae5b8ee05c2", "generic": "669084d1ea704ae5b8ee05c2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "669084d1ea704ae5b8ee05c2", +"motherduck": "669084d1ea704ae5b8ee05c2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "93c1b5b5960e26479baf88d0", "postgresql": "93c1b5b5960e26479baf88d0", "snowflake": "669084d1ea704ae5b8ee05c2", @@ -22771,8 +22771,8 @@ "databricks": "3dbbe73ee4310c73030e8110", "duckdb": "3dbbe73ee4310c73030e8110", "generic": "3dbbe73ee4310c73030e8110", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3dbbe73ee4310c73030e8110", +"motherduck": "3dbbe73ee4310c73030e8110", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3dbbe73ee4310c73030e8110", "postgresql": "3dbbe73ee4310c73030e8110", "snowflake": "f43dac38f34a1c0618a3f812", @@ -22788,8 +22788,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1eb7fb4e8cf75773c5573562", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1eb7fb4e8cf75773c5573562", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -22805,8 +22805,8 @@ "databricks": "6ebf45a787a223241c8d9a47", "duckdb": "6ebf45a787a223241c8d9a47", "generic": "6ebf45a787a223241c8d9a47", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6ebf45a787a223241c8d9a47", +"motherduck": "6ebf45a787a223241c8d9a47", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "6ebf45a787a223241c8d9a47", "postgresql": "6ebf45a787a223241c8d9a47", "snowflake": "6ebf45a787a223241c8d9a47", @@ -22822,8 +22822,8 @@ "databricks": "cd9c38fc2cf84c8551e29278", "duckdb": "cd9c38fc2cf84c8551e29278", "generic": "cd9c38fc2cf84c8551e29278", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "cd9c38fc2cf84c8551e29278", +"motherduck": "cd9c38fc2cf84c8551e29278", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "cd9c38fc2cf84c8551e29278", "postgresql": "cd9c38fc2cf84c8551e29278", "snowflake": "cd9c38fc2cf84c8551e29278", @@ -22839,8 +22839,8 @@ "databricks": "e57ac1541fb51b362cd5b470", "duckdb": "e57ac1541fb51b362cd5b470", "generic": "e57ac1541fb51b362cd5b470", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e57ac1541fb51b362cd5b470", +"motherduck": "e57ac1541fb51b362cd5b470", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e57ac1541fb51b362cd5b470", "postgresql": "e57ac1541fb51b362cd5b470", "snowflake": "4b356542044f1267fcfc487a", @@ -22856,8 +22856,8 @@ "databricks": "a05c10798b5cb323b7dc3ed4", "duckdb": "225fc4f7b28377ba911e4a47", "generic": "a05c10798b5cb323b7dc3ed4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a05c10798b5cb323b7dc3ed4", +"motherduck": "225fc4f7b28377ba911e4a47", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "4e87e4788e5ac719db00d4e4", "postgresql": "4e87e4788e5ac719db00d4e4", "snowflake": "225fc4f7b28377ba911e4a47", @@ -22873,8 +22873,8 @@ "databricks": "dbf9ebbc7016f5ac5df2817e", "duckdb": "dbf9ebbc7016f5ac5df2817e", "generic": "dbf9ebbc7016f5ac5df2817e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "dbf9ebbc7016f5ac5df2817e", +"motherduck": "dbf9ebbc7016f5ac5df2817e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "4cccf86e62d627a7a8b57726", "postgresql": "4cccf86e62d627a7a8b57726", "snowflake": "dbf9ebbc7016f5ac5df2817e", @@ -22890,8 +22890,8 @@ "databricks": "5debd5336e9bc9ec70e731b7", "duckdb": "5debd5336e9bc9ec70e731b7", "generic": "5debd5336e9bc9ec70e731b7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5debd5336e9bc9ec70e731b7", +"motherduck": "5debd5336e9bc9ec70e731b7", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5debd5336e9bc9ec70e731b7", "postgresql": "5debd5336e9bc9ec70e731b7", "snowflake": "5debd5336e9bc9ec70e731b7", @@ -22907,8 +22907,8 @@ "databricks": "d2c97d221599d038d9875873", "duckdb": "d2c97d221599d038d9875873", "generic": "d2c97d221599d038d9875873", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d2c97d221599d038d9875873", +"motherduck": "d2c97d221599d038d9875873", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "55155766823b6a3a5e882fc2", "postgresql": "55155766823b6a3a5e882fc2", "snowflake": "d2c97d221599d038d9875873", @@ -22924,8 +22924,8 @@ "databricks": "1fdc0b9f28b115c175e88d46", "duckdb": "1fdc0b9f28b115c175e88d46", "generic": "1fdc0b9f28b115c175e88d46", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1fdc0b9f28b115c175e88d46", +"motherduck": "1fdc0b9f28b115c175e88d46", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1fdc0b9f28b115c175e88d46", "postgresql": "1fdc0b9f28b115c175e88d46", "snowflake": "1fdc0b9f28b115c175e88d46", @@ -22941,8 +22941,8 @@ "databricks": "f38ef32a483f1c4f89ae309e", "duckdb": "f38ef32a483f1c4f89ae309e", "generic": "f38ef32a483f1c4f89ae309e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f38ef32a483f1c4f89ae309e", +"motherduck": "f38ef32a483f1c4f89ae309e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f38ef32a483f1c4f89ae309e", "postgresql": "f38ef32a483f1c4f89ae309e", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -22958,8 +22958,8 @@ "databricks": "4281b54de06f589b3507035a", "duckdb": "5620e893a908c6743b863bf6", "generic": "5620e893a908c6743b863bf6", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5620e893a908c6743b863bf6", +"motherduck": "5620e893a908c6743b863bf6", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5620e893a908c6743b863bf6", "postgresql": "5620e893a908c6743b863bf6", "snowflake": "5620e893a908c6743b863bf6", @@ -22975,8 +22975,8 @@ "databricks": "c0d59e65280108f563dbb04b", "duckdb": "c0d59e65280108f563dbb04b", "generic": "c0d59e65280108f563dbb04b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c0d59e65280108f563dbb04b", +"motherduck": "c0d59e65280108f563dbb04b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c0d59e65280108f563dbb04b", "postgresql": "c0d59e65280108f563dbb04b", "snowflake": "c0d59e65280108f563dbb04b", @@ -22992,8 +22992,8 @@ "databricks": "d16d75bc8832411932952499", "duckdb": "d16d75bc8832411932952499", "generic": "d16d75bc8832411932952499", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d16d75bc8832411932952499", +"motherduck": "d16d75bc8832411932952499", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9372facf3130ca098b0fd00a", "postgresql": "9372facf3130ca098b0fd00a", "snowflake": "d16d75bc8832411932952499", @@ -23009,8 +23009,8 @@ "databricks": "de89632e277dbc8c97115419", "duckdb": "5abf4ec799a2ec32fb10da7e", "generic": "de89632e277dbc8c97115419", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "de89632e277dbc8c97115419", +"motherduck": "5abf4ec799a2ec32fb10da7e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5abf4ec799a2ec32fb10da7e", "postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "a5656d70e208254d1d3e9b4f", @@ -23026,8 +23026,8 @@ "databricks": "6cffd1617edd6454241ee732", "duckdb": "6cffd1617edd6454241ee732", "generic": "6cffd1617edd6454241ee732", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6cffd1617edd6454241ee732", +"motherduck": "6cffd1617edd6454241ee732", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "6cffd1617edd6454241ee732", "postgresql": "6cffd1617edd6454241ee732", "snowflake": "6cffd1617edd6454241ee732", @@ -23043,8 +23043,8 @@ "databricks": "a686bb6db17ab079cfc79224", "duckdb": "a686bb6db17ab079cfc79224", "generic": "a686bb6db17ab079cfc79224", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a686bb6db17ab079cfc79224", +"motherduck": "a686bb6db17ab079cfc79224", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "74a30f6aa9def0b58afc616c", "postgresql": "74a30f6aa9def0b58afc616c", "snowflake": "1096c3c8687c8e8eb538ff74", @@ -23060,8 +23060,8 @@ "databricks": "c752fd9fe2ad3b86b8b899da", "duckdb": "c752fd9fe2ad3b86b8b899da", "generic": "c752fd9fe2ad3b86b8b899da", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c752fd9fe2ad3b86b8b899da", +"motherduck": "c752fd9fe2ad3b86b8b899da", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e998cd8f0f74af0526b75ff0", "postgresql": "e998cd8f0f74af0526b75ff0", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -23077,8 +23077,8 @@ "databricks": "b52d5a83269c4ad64bd3416e", "duckdb": "b52d5a83269c4ad64bd3416e", "generic": "b52d5a83269c4ad64bd3416e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b52d5a83269c4ad64bd3416e", +"motherduck": "b52d5a83269c4ad64bd3416e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b52d5a83269c4ad64bd3416e", "postgresql": "b52d5a83269c4ad64bd3416e", "snowflake": "b52d5a83269c4ad64bd3416e", @@ -23094,8 +23094,8 @@ "databricks": "f98c7776673323299fbfe6b7", "duckdb": "e3729d015bce3152bae7cd02", "generic": "f98c7776673323299fbfe6b7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "e3729d015bce3152bae7cd02", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e3729d015bce3152bae7cd02", "postgresql": "e3729d015bce3152bae7cd02", "snowflake": "a2a30dfc031641a60ba584fa", @@ -23111,8 +23111,8 @@ "databricks": "eeb8bd6105754e78339cd228", "duckdb": "f2ed52f6bf46b4db9343551c", "generic": "eeb8bd6105754e78339cd228", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "eeb8bd6105754e78339cd228", +"motherduck": "f2ed52f6bf46b4db9343551c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "eeb8bd6105754e78339cd228", "postgresql": "eeb8bd6105754e78339cd228", "snowflake": "f2ed52f6bf46b4db9343551c", @@ -23128,8 +23128,8 @@ "databricks": "de89632e277dbc8c97115419", "duckdb": "de89632e277dbc8c97115419", "generic": "de89632e277dbc8c97115419", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "de89632e277dbc8c97115419", +"motherduck": "de89632e277dbc8c97115419", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5abf4ec799a2ec32fb10da7e", "postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", @@ -23145,8 +23145,8 @@ "databricks": "88ee4a69672b4b9b3aa21e3d", "duckdb": "88ee4a69672b4b9b3aa21e3d", "generic": "88ee4a69672b4b9b3aa21e3d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "88ee4a69672b4b9b3aa21e3d", +"motherduck": "88ee4a69672b4b9b3aa21e3d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "88ee4a69672b4b9b3aa21e3d", "postgresql": "88ee4a69672b4b9b3aa21e3d", "snowflake": "88ee4a69672b4b9b3aa21e3d", @@ -23162,8 +23162,8 @@ "databricks": "ab5ad6180652b3b00d1f5b01", "duckdb": "ab5ad6180652b3b00d1f5b01", "generic": "ab5ad6180652b3b00d1f5b01", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ab5ad6180652b3b00d1f5b01", +"motherduck": "ab5ad6180652b3b00d1f5b01", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ab5ad6180652b3b00d1f5b01", "postgresql": "ab5ad6180652b3b00d1f5b01", "snowflake": "226beccc1d430773f4e67b70", @@ -23179,8 +23179,8 @@ "databricks": "555111267818cbd3d0b07105", "duckdb": "555111267818cbd3d0b07105", "generic": "555111267818cbd3d0b07105", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "555111267818cbd3d0b07105", +"motherduck": "555111267818cbd3d0b07105", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "555111267818cbd3d0b07105", "postgresql": "555111267818cbd3d0b07105", "snowflake": "555111267818cbd3d0b07105", @@ -23196,8 +23196,8 @@ "databricks": "18db5afe2c1e80d1ee652b54", "duckdb": "18db5afe2c1e80d1ee652b54", "generic": "18db5afe2c1e80d1ee652b54", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "18db5afe2c1e80d1ee652b54", +"motherduck": "18db5afe2c1e80d1ee652b54", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "47d64a5994d42a2b577166ea", "postgresql": "47d64a5994d42a2b577166ea", "snowflake": "18db5afe2c1e80d1ee652b54", @@ -23213,8 +23213,8 @@ "databricks": "9aff7fcc7d550e1a8d105471", "duckdb": "9aff7fcc7d550e1a8d105471", "generic": "9aff7fcc7d550e1a8d105471", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9aff7fcc7d550e1a8d105471", +"motherduck": "9aff7fcc7d550e1a8d105471", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9aff7fcc7d550e1a8d105471", "postgresql": "9aff7fcc7d550e1a8d105471", "snowflake": "9aff7fcc7d550e1a8d105471", @@ -23230,8 +23230,8 @@ "databricks": "e8b6e0bd2ded7b9eec6209e4", "duckdb": "e8b6e0bd2ded7b9eec6209e4", "generic": "e8b6e0bd2ded7b9eec6209e4", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e8b6e0bd2ded7b9eec6209e4", +"motherduck": "e8b6e0bd2ded7b9eec6209e4", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e8b6e0bd2ded7b9eec6209e4", "postgresql": "e8b6e0bd2ded7b9eec6209e4", "snowflake": "e8b6e0bd2ded7b9eec6209e4", @@ -23247,8 +23247,8 @@ "databricks": "e08a957777bd278f921ca0ee", "duckdb": "e08a957777bd278f921ca0ee", "generic": "e08a957777bd278f921ca0ee", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e08a957777bd278f921ca0ee", +"motherduck": "e08a957777bd278f921ca0ee", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e08a957777bd278f921ca0ee", "postgresql": "e08a957777bd278f921ca0ee", "snowflake": "e08a957777bd278f921ca0ee", @@ -23264,8 +23264,8 @@ "databricks": "da72f481881cd577b304e9e7", "duckdb": "da72f481881cd577b304e9e7", "generic": "da72f481881cd577b304e9e7", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "da72f481881cd577b304e9e7", +"motherduck": "da72f481881cd577b304e9e7", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "da72f481881cd577b304e9e7", "postgresql": "da72f481881cd577b304e9e7", "snowflake": "da72f481881cd577b304e9e7", @@ -23281,8 +23281,8 @@ "databricks": "b0791eeb8731828818f87f78", "duckdb": "b0791eeb8731828818f87f78", "generic": "b0791eeb8731828818f87f78", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b0791eeb8731828818f87f78", +"motherduck": "b0791eeb8731828818f87f78", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b0791eeb8731828818f87f78", "postgresql": "b0791eeb8731828818f87f78", "snowflake": "b0791eeb8731828818f87f78", @@ -23298,8 +23298,8 @@ "databricks": "d4b3585f1e47f5f60a2c28ee", "duckdb": "d4b3585f1e47f5f60a2c28ee", "generic": "d4b3585f1e47f5f60a2c28ee", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d4b3585f1e47f5f60a2c28ee", +"motherduck": "d4b3585f1e47f5f60a2c28ee", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d4b3585f1e47f5f60a2c28ee", "postgresql": "d4b3585f1e47f5f60a2c28ee", "snowflake": "d4b3585f1e47f5f60a2c28ee", @@ -23315,8 +23315,8 @@ "databricks": "62368fc15cefd5807807b209", "duckdb": "62368fc15cefd5807807b209", "generic": "62368fc15cefd5807807b209", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "62368fc15cefd5807807b209", +"motherduck": "62368fc15cefd5807807b209", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "62368fc15cefd5807807b209", "postgresql": "62368fc15cefd5807807b209", "snowflake": "62368fc15cefd5807807b209", @@ -23332,8 +23332,8 @@ "databricks": "4b356542044f1267fcfc487a", "duckdb": "4b356542044f1267fcfc487a", "generic": "4b356542044f1267fcfc487a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4b356542044f1267fcfc487a", +"motherduck": "4b356542044f1267fcfc487a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a5656d70e208254d1d3e9b4f", "postgresql": "a5656d70e208254d1d3e9b4f", "snowflake": "4b356542044f1267fcfc487a", @@ -23349,8 +23349,8 @@ "databricks": "5abf4ec799a2ec32fb10da7e", "duckdb": "5abf4ec799a2ec32fb10da7e", "generic": "5abf4ec799a2ec32fb10da7e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5abf4ec799a2ec32fb10da7e", +"motherduck": "5abf4ec799a2ec32fb10da7e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5abf4ec799a2ec32fb10da7e", "postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "a5656d70e208254d1d3e9b4f", @@ -23366,8 +23366,8 @@ "databricks": "7ca149227eff68da5bfa0404", "duckdb": "7ca149227eff68da5bfa0404", "generic": "7ca149227eff68da5bfa0404", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "7ca149227eff68da5bfa0404", +"motherduck": "7ca149227eff68da5bfa0404", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7ca149227eff68da5bfa0404", "postgresql": "7ca149227eff68da5bfa0404", "snowflake": "7ca149227eff68da5bfa0404", @@ -23383,8 +23383,8 @@ "databricks": "3dbbe73ee4310c73030e8110", "duckdb": "3dbbe73ee4310c73030e8110", "generic": "3dbbe73ee4310c73030e8110", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3dbbe73ee4310c73030e8110", +"motherduck": "3dbbe73ee4310c73030e8110", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3dbbe73ee4310c73030e8110", "postgresql": "3dbbe73ee4310c73030e8110", "snowflake": "f43dac38f34a1c0618a3f812", @@ -23400,8 +23400,8 @@ "databricks": "19cc45192479751e55bffbfe", "duckdb": "19cc45192479751e55bffbfe", "generic": "19cc45192479751e55bffbfe", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "19cc45192479751e55bffbfe", +"motherduck": "19cc45192479751e55bffbfe", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "19cc45192479751e55bffbfe", "postgresql": "19cc45192479751e55bffbfe", "snowflake": "19cc45192479751e55bffbfe", @@ -23417,8 +23417,8 @@ "databricks": "300e2b67af892b463d85f9cd", "duckdb": "300e2b67af892b463d85f9cd", "generic": "300e2b67af892b463d85f9cd", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "300e2b67af892b463d85f9cd", +"motherduck": "300e2b67af892b463d85f9cd", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "300e2b67af892b463d85f9cd", "postgresql": "300e2b67af892b463d85f9cd", "snowflake": "300e2b67af892b463d85f9cd", @@ -23434,8 +23434,8 @@ "databricks": "1e75e5227bea0ae294ac7af1", "duckdb": "19bdd92ea0cc5fb028657510", "generic": "1e75e5227bea0ae294ac7af1", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "19bdd92ea0cc5fb028657510", +"motherduck": "19bdd92ea0cc5fb028657510", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "19bdd92ea0cc5fb028657510", "postgresql": "19bdd92ea0cc5fb028657510", "snowflake": "8ff614b7306c0edbe36ce00b", @@ -23451,8 +23451,8 @@ "databricks": "94f6df33eccb97170eccb01b", "duckdb": "94f6df33eccb97170eccb01b", "generic": "94f6df33eccb97170eccb01b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "94f6df33eccb97170eccb01b", +"motherduck": "94f6df33eccb97170eccb01b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "94f6df33eccb97170eccb01b", "postgresql": "94f6df33eccb97170eccb01b", "snowflake": "94f6df33eccb97170eccb01b", @@ -23468,8 +23468,8 @@ "databricks": "19a1a80ffc06169f32694de8", "duckdb": "19a1a80ffc06169f32694de8", "generic": "19a1a80ffc06169f32694de8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "19a1a80ffc06169f32694de8", +"motherduck": "19a1a80ffc06169f32694de8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ab1f5f46e69284778bd66d58", "postgresql": "ab1f5f46e69284778bd66d58", "snowflake": "19a1a80ffc06169f32694de8", @@ -23485,8 +23485,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1eb7fb4e8cf75773c5573562", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1eb7fb4e8cf75773c5573562", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -23502,8 +23502,8 @@ "databricks": "de89632e277dbc8c97115419", "duckdb": "de89632e277dbc8c97115419", "generic": "de89632e277dbc8c97115419", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "de89632e277dbc8c97115419", +"motherduck": "de89632e277dbc8c97115419", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5abf4ec799a2ec32fb10da7e", "postgresql": "5abf4ec799a2ec32fb10da7e", "snowflake": "4b356542044f1267fcfc487a", @@ -23519,8 +23519,8 @@ "databricks": "2444c03418f9f1b2f23f2ec8", "duckdb": "2444c03418f9f1b2f23f2ec8", "generic": "2444c03418f9f1b2f23f2ec8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2444c03418f9f1b2f23f2ec8", +"motherduck": "2444c03418f9f1b2f23f2ec8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2444c03418f9f1b2f23f2ec8", "postgresql": "2444c03418f9f1b2f23f2ec8", "snowflake": "2444c03418f9f1b2f23f2ec8", @@ -23536,8 +23536,8 @@ "databricks": "d2c97d221599d038d9875873", "duckdb": "f7fca17b74cc50ae3de8c826", "generic": "d2c97d221599d038d9875873", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d2c97d221599d038d9875873", +"motherduck": "f7fca17b74cc50ae3de8c826", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "55155766823b6a3a5e882fc2", "postgresql": "55155766823b6a3a5e882fc2", "snowflake": "d2c97d221599d038d9875873", @@ -23553,8 +23553,8 @@ "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fe23594c781e79cbb0c8927a", +"motherduck": "fe23594c781e79cbb0c8927a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8a3f094f18fb4047f0febf59", "postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", @@ -23570,8 +23570,8 @@ "databricks": "b561884132dbe7e42b719110", "duckdb": "b561884132dbe7e42b719110", "generic": "b561884132dbe7e42b719110", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b561884132dbe7e42b719110", +"motherduck": "b561884132dbe7e42b719110", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5aa51d84ac5e6c7a9306f348", "postgresql": "5aa51d84ac5e6c7a9306f348", "snowflake": "b561884132dbe7e42b719110", @@ -23587,8 +23587,8 @@ "databricks": "f8219c960720043f0662a603", "duckdb": "35a9c5c1419bff507cf65ae2", "generic": "f8219c960720043f0662a603", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f8219c960720043f0662a603", +"motherduck": "35a9c5c1419bff507cf65ae2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "35a9c5c1419bff507cf65ae2", "postgresql": "35a9c5c1419bff507cf65ae2", "snowflake": "f8219c960720043f0662a603", @@ -23604,8 +23604,8 @@ "databricks": "e57ac1541fb51b362cd5b470", "duckdb": "396b2ce32efb5365b393c230", "generic": "e57ac1541fb51b362cd5b470", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e57ac1541fb51b362cd5b470", +"motherduck": "396b2ce32efb5365b393c230", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "396b2ce32efb5365b393c230", "postgresql": "396b2ce32efb5365b393c230", "snowflake": "4b356542044f1267fcfc487a", @@ -23621,8 +23621,8 @@ "databricks": "f16105cbc61d547e6c08efc8", "duckdb": "f16105cbc61d547e6c08efc8", "generic": "f16105cbc61d547e6c08efc8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f16105cbc61d547e6c08efc8", +"motherduck": "f16105cbc61d547e6c08efc8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f16105cbc61d547e6c08efc8", "postgresql": "f16105cbc61d547e6c08efc8", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -23638,8 +23638,8 @@ "databricks": "f6140be64a327a15b42b923d", "duckdb": "f6140be64a327a15b42b923d", "generic": "f6140be64a327a15b42b923d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f6140be64a327a15b42b923d", +"motherduck": "f6140be64a327a15b42b923d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f6140be64a327a15b42b923d", "postgresql": "f6140be64a327a15b42b923d", "snowflake": "f6140be64a327a15b42b923d", @@ -23655,8 +23655,8 @@ "databricks": "d61c83ef8bfdf043fa672cb5", "duckdb": "d61c83ef8bfdf043fa672cb5", "generic": "d61c83ef8bfdf043fa672cb5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d61c83ef8bfdf043fa672cb5", +"motherduck": "d61c83ef8bfdf043fa672cb5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d61c83ef8bfdf043fa672cb5", "postgresql": "d61c83ef8bfdf043fa672cb5", "snowflake": "97a20344d811fb77c7690863", @@ -23672,8 +23672,8 @@ "databricks": "25cdfa949946952336077d37", "duckdb": "25cdfa949946952336077d37", "generic": "25cdfa949946952336077d37", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "25cdfa949946952336077d37", +"motherduck": "25cdfa949946952336077d37", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "25cdfa949946952336077d37", "postgresql": "25cdfa949946952336077d37", "snowflake": "25cdfa949946952336077d37", @@ -23689,8 +23689,8 @@ "databricks": "6d7ab232e65fe8d85c7169ae", "duckdb": "6d7ab232e65fe8d85c7169ae", "generic": "6d7ab232e65fe8d85c7169ae", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6d7ab232e65fe8d85c7169ae", +"motherduck": "6d7ab232e65fe8d85c7169ae", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a6659d96bbfa5da75e69e22d", "postgresql": "a6659d96bbfa5da75e69e22d", "snowflake": "6d7ab232e65fe8d85c7169ae", @@ -23706,8 +23706,8 @@ "databricks": "1509a96170cb94e0a99bbf00", "duckdb": "13bc61bc9a039c8b798c5322", "generic": "1509a96170cb94e0a99bbf00", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "13bc61bc9a039c8b798c5322", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "13bc61bc9a039c8b798c5322", "postgresql": "13bc61bc9a039c8b798c5322", "snowflake": "d5cc04b4579263e52cd194bb", @@ -23723,8 +23723,8 @@ "databricks": "d6de1c22b8b95cc863db57ba", "duckdb": "d6de1c22b8b95cc863db57ba", "generic": "d6de1c22b8b95cc863db57ba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d6de1c22b8b95cc863db57ba", +"motherduck": "d6de1c22b8b95cc863db57ba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d6de1c22b8b95cc863db57ba", "postgresql": "d6de1c22b8b95cc863db57ba", "snowflake": "d6de1c22b8b95cc863db57ba", @@ -23740,8 +23740,8 @@ "databricks": "2c6b371b030464f525066893", "duckdb": "2c6b371b030464f525066893", "generic": "2c6b371b030464f525066893", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2c6b371b030464f525066893", +"motherduck": "2c6b371b030464f525066893", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3feb479faeec777c2ade8afd", "postgresql": "3feb479faeec777c2ade8afd", "snowflake": "2c6b371b030464f525066893", @@ -23757,8 +23757,8 @@ "databricks": "46ec3e19e05a6cb29854ccac", "duckdb": "35e216b6921bf843994f2eea", "generic": "46ec3e19e05a6cb29854ccac", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "46ec3e19e05a6cb29854ccac", +"motherduck": "35e216b6921bf843994f2eea", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "46ec3e19e05a6cb29854ccac", "postgresql": "46ec3e19e05a6cb29854ccac", "snowflake": "46ec3e19e05a6cb29854ccac", @@ -23774,8 +23774,8 @@ "databricks": "08736485547ba339930697e9", "duckdb": "08736485547ba339930697e9", "generic": "08736485547ba339930697e9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "08736485547ba339930697e9", +"motherduck": "08736485547ba339930697e9", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "eecd452b4f5f3f4133d0bd0a", "postgresql": "eecd452b4f5f3f4133d0bd0a", "snowflake": "08736485547ba339930697e9", @@ -23791,8 +23791,8 @@ "databricks": "2fcd11f2fdbe6c4498c4833e", "duckdb": "2fcd11f2fdbe6c4498c4833e", "generic": "2fcd11f2fdbe6c4498c4833e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2fcd11f2fdbe6c4498c4833e", +"motherduck": "2fcd11f2fdbe6c4498c4833e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3255b7052c624f34aab6633f", "postgresql": "3255b7052c624f34aab6633f", "snowflake": "2fcd11f2fdbe6c4498c4833e", @@ -23808,8 +23808,8 @@ "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fe23594c781e79cbb0c8927a", +"motherduck": "fe23594c781e79cbb0c8927a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8a3f094f18fb4047f0febf59", "postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", @@ -23825,8 +23825,8 @@ "databricks": "c844c96e1e3ffc72d5f90df9", "duckdb": "8a3f094f18fb4047f0febf59", "generic": "c844c96e1e3ffc72d5f90df9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "28eb0d6de9724959ce5ba43e", +"motherduck": "8a3f094f18fb4047f0febf59", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c844c96e1e3ffc72d5f90df9", "postgresql": "c844c96e1e3ffc72d5f90df9", "snowflake": "c844c96e1e3ffc72d5f90df9", @@ -23842,8 +23842,8 @@ "databricks": "5c28019dce48a74708c90a76", "duckdb": "5c28019dce48a74708c90a76", "generic": "5c28019dce48a74708c90a76", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "5c28019dce48a74708c90a76", +"motherduck": "5c28019dce48a74708c90a76", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5c28019dce48a74708c90a76", "postgresql": "5c28019dce48a74708c90a76", "snowflake": "a5656d70e208254d1d3e9b4f", @@ -23859,8 +23859,8 @@ "databricks": "2fcd11f2fdbe6c4498c4833e", "duckdb": "2fcd11f2fdbe6c4498c4833e", "generic": "2fcd11f2fdbe6c4498c4833e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2fcd11f2fdbe6c4498c4833e", +"motherduck": "2fcd11f2fdbe6c4498c4833e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3255b7052c624f34aab6633f", "postgresql": "3255b7052c624f34aab6633f", "snowflake": "2fcd11f2fdbe6c4498c4833e", @@ -23876,8 +23876,8 @@ "databricks": "aab0389e2b5470136660bf8a", "duckdb": "aab0389e2b5470136660bf8a", "generic": "aab0389e2b5470136660bf8a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "aab0389e2b5470136660bf8a", +"motherduck": "aab0389e2b5470136660bf8a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "aab0389e2b5470136660bf8a", "postgresql": "aab0389e2b5470136660bf8a", "snowflake": "97a20344d811fb77c7690863", @@ -23893,8 +23893,8 @@ "databricks": "fa78cfa46a78783d069e2fc5", "duckdb": "6f4372a332076048e56d5076", "generic": "fa78cfa46a78783d069e2fc5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fa78cfa46a78783d069e2fc5", +"motherduck": "6f4372a332076048e56d5076", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "6f4372a332076048e56d5076", "postgresql": "6f4372a332076048e56d5076", "snowflake": "daeeb0f00b3ee4211b8da00f", @@ -23910,8 +23910,8 @@ "databricks": "7a00586f9fdb1d97256bfff8", "duckdb": "7a00586f9fdb1d97256bfff8", "generic": "7a00586f9fdb1d97256bfff8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "7a00586f9fdb1d97256bfff8", +"motherduck": "7a00586f9fdb1d97256bfff8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7a00586f9fdb1d97256bfff8", "postgresql": "7a00586f9fdb1d97256bfff8", "snowflake": "7a00586f9fdb1d97256bfff8", @@ -23927,8 +23927,8 @@ "databricks": "2bfa352725f9d8a91250f75e", "duckdb": "2bfa352725f9d8a91250f75e", "generic": "2bfa352725f9d8a91250f75e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2bfa352725f9d8a91250f75e", +"motherduck": "2bfa352725f9d8a91250f75e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2bfa352725f9d8a91250f75e", "postgresql": "2bfa352725f9d8a91250f75e", "snowflake": "2bfa352725f9d8a91250f75e", @@ -23944,8 +23944,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1eb7fb4e8cf75773c5573562", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1eb7fb4e8cf75773c5573562", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -23961,8 +23961,8 @@ "databricks": "2bfa352725f9d8a91250f75e", "duckdb": "2bfa352725f9d8a91250f75e", "generic": "2bfa352725f9d8a91250f75e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2bfa352725f9d8a91250f75e", +"motherduck": "2bfa352725f9d8a91250f75e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2bfa352725f9d8a91250f75e", "postgresql": "2bfa352725f9d8a91250f75e", "snowflake": "2bfa352725f9d8a91250f75e", @@ -23978,8 +23978,8 @@ "databricks": "aab0389e2b5470136660bf8a", "duckdb": "aab0389e2b5470136660bf8a", "generic": "aab0389e2b5470136660bf8a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "aab0389e2b5470136660bf8a", +"motherduck": "aab0389e2b5470136660bf8a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f16105cbc61d547e6c08efc8", "postgresql": "f16105cbc61d547e6c08efc8", "snowflake": "97a20344d811fb77c7690863", @@ -23995,8 +23995,8 @@ "databricks": "62368fc15cefd5807807b209", "duckdb": "62368fc15cefd5807807b209", "generic": "62368fc15cefd5807807b209", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "62368fc15cefd5807807b209", +"motherduck": "62368fc15cefd5807807b209", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "62368fc15cefd5807807b209", "postgresql": "62368fc15cefd5807807b209", "snowflake": "62368fc15cefd5807807b209", @@ -24012,8 +24012,8 @@ "databricks": "38b2ec94bf02be40007ce525", "duckdb": "38b2ec94bf02be40007ce525", "generic": "38b2ec94bf02be40007ce525", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "38b2ec94bf02be40007ce525", +"motherduck": "38b2ec94bf02be40007ce525", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "38b2ec94bf02be40007ce525", "postgresql": "38b2ec94bf02be40007ce525", "snowflake": "38b2ec94bf02be40007ce525", @@ -24029,8 +24029,8 @@ "databricks": "3a4d8bca5124769db23a43df", "duckdb": "01e3dfc024c37a8d62c7f810", "generic": "3a4d8bca5124769db23a43df", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "01e3dfc024c37a8d62c7f810", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "01e3dfc024c37a8d62c7f810", "postgresql": "01e3dfc024c37a8d62c7f810", "snowflake": "db03b5ccf153c75e43846a6a", @@ -24046,8 +24046,8 @@ "databricks": "0703bf71a54942bdaa28406e", "duckdb": "0703bf71a54942bdaa28406e", "generic": "0703bf71a54942bdaa28406e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0703bf71a54942bdaa28406e", +"motherduck": "0703bf71a54942bdaa28406e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0703bf71a54942bdaa28406e", "postgresql": "0703bf71a54942bdaa28406e", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -24063,8 +24063,8 @@ "databricks": "ef2689646fd9e772e27d2727", "duckdb": "ef2689646fd9e772e27d2727", "generic": "ef2689646fd9e772e27d2727", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ef2689646fd9e772e27d2727", +"motherduck": "ef2689646fd9e772e27d2727", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ef2689646fd9e772e27d2727", "postgresql": "ef2689646fd9e772e27d2727", "snowflake": "ef2689646fd9e772e27d2727", @@ -24080,8 +24080,8 @@ "databricks": "61c3e35de716257a33271a64", "duckdb": "c43574432afc90bcba6944b2", "generic": "61c3e35de716257a33271a64", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "61c3e35de716257a33271a64", +"motherduck": "c43574432afc90bcba6944b2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "61c3e35de716257a33271a64", "postgresql": "61c3e35de716257a33271a64", "snowflake": "5b6535982c7d4576e34ee7cb", @@ -24097,8 +24097,8 @@ "databricks": "3dbbe73ee4310c73030e8110", "duckdb": "3dbbe73ee4310c73030e8110", "generic": "3dbbe73ee4310c73030e8110", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3dbbe73ee4310c73030e8110", +"motherduck": "3dbbe73ee4310c73030e8110", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3dbbe73ee4310c73030e8110", "postgresql": "3dbbe73ee4310c73030e8110", "snowflake": "f43dac38f34a1c0618a3f812", @@ -24114,8 +24114,8 @@ "databricks": "b46dfa976d5ba04f95bea32b", "duckdb": "b46dfa976d5ba04f95bea32b", "generic": "b46dfa976d5ba04f95bea32b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b46dfa976d5ba04f95bea32b", +"motherduck": "b46dfa976d5ba04f95bea32b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "34dcdfe3e3afe6140e1353e0", "postgresql": "34dcdfe3e3afe6140e1353e0", "snowflake": "b46dfa976d5ba04f95bea32b", @@ -24131,8 +24131,8 @@ "databricks": "031c4b14dc8b97fdb38e28b3", "duckdb": "bc9875824f2c0eb90fcb5e51", "generic": "0289c868616e51dc348bba36", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0289c868616e51dc348bba36", +"motherduck": "bc9875824f2c0eb90fcb5e51", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "fe2c1943dc029130eda3f066", "postgresql": "fe2c1943dc029130eda3f066", "snowflake": "ffc35af553194c8ccffc9058", @@ -24148,8 +24148,8 @@ "databricks": "c844c96e1e3ffc72d5f90df9", "duckdb": "8a3f094f18fb4047f0febf59", "generic": "c844c96e1e3ffc72d5f90df9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "28eb0d6de9724959ce5ba43e", +"motherduck": "8a3f094f18fb4047f0febf59", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c844c96e1e3ffc72d5f90df9", "postgresql": "c844c96e1e3ffc72d5f90df9", "snowflake": "c844c96e1e3ffc72d5f90df9", @@ -24165,8 +24165,8 @@ "databricks": "3dbbe73ee4310c73030e8110", "duckdb": "3dbbe73ee4310c73030e8110", "generic": "3dbbe73ee4310c73030e8110", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3dbbe73ee4310c73030e8110", +"motherduck": "3dbbe73ee4310c73030e8110", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3dbbe73ee4310c73030e8110", "postgresql": "3dbbe73ee4310c73030e8110", "snowflake": "f43dac38f34a1c0618a3f812", @@ -24182,8 +24182,8 @@ "databricks": "04d8552335eed867485d59b8", "duckdb": "8f29d402cac8d3b922d1a4e7", "generic": "04d8552335eed867485d59b8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "8f29d402cac8d3b922d1a4e7", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8f29d402cac8d3b922d1a4e7", "postgresql": "8f29d402cac8d3b922d1a4e7", "snowflake": "c60d34b521887e82716fd787", @@ -24199,8 +24199,8 @@ "databricks": "d53793d8f8d21c36a117dedb", "duckdb": "d53793d8f8d21c36a117dedb", "generic": "d53793d8f8d21c36a117dedb", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d53793d8f8d21c36a117dedb", +"motherduck": "d53793d8f8d21c36a117dedb", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d53793d8f8d21c36a117dedb", "postgresql": "d53793d8f8d21c36a117dedb", "snowflake": "d53793d8f8d21c36a117dedb", @@ -24216,8 +24216,8 @@ "databricks": "251aba3fef503ce64556c701", "duckdb": "251aba3fef503ce64556c701", "generic": "251aba3fef503ce64556c701", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "251aba3fef503ce64556c701", +"motherduck": "251aba3fef503ce64556c701", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b7d6fc051a3c1ea6deb29ff4", "postgresql": "b7d6fc051a3c1ea6deb29ff4", "snowflake": "251aba3fef503ce64556c701", @@ -24233,8 +24233,8 @@ "databricks": "df19c722959a4259621a1e09", "duckdb": "df19c722959a4259621a1e09", "generic": "df19c722959a4259621a1e09", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "df19c722959a4259621a1e09", +"motherduck": "df19c722959a4259621a1e09", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1d7dd2736322bc33dae77252", "postgresql": "1d7dd2736322bc33dae77252", "snowflake": "df19c722959a4259621a1e09", @@ -24250,8 +24250,8 @@ "databricks": "2bfa352725f9d8a91250f75e", "duckdb": "2bfa352725f9d8a91250f75e", "generic": "2bfa352725f9d8a91250f75e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2bfa352725f9d8a91250f75e", +"motherduck": "2bfa352725f9d8a91250f75e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2bfa352725f9d8a91250f75e", "postgresql": "2bfa352725f9d8a91250f75e", "snowflake": "2bfa352725f9d8a91250f75e", @@ -24267,8 +24267,8 @@ "databricks": "4662a1758442ce44b830269f", "duckdb": "4662a1758442ce44b830269f", "generic": "4662a1758442ce44b830269f", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4662a1758442ce44b830269f", +"motherduck": "4662a1758442ce44b830269f", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "4662a1758442ce44b830269f", "postgresql": "4662a1758442ce44b830269f", "snowflake": "4662a1758442ce44b830269f", @@ -24284,8 +24284,8 @@ "databricks": "73975a560983dfcdc361f145", "duckdb": "73975a560983dfcdc361f145", "generic": "73975a560983dfcdc361f145", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "73975a560983dfcdc361f145", +"motherduck": "73975a560983dfcdc361f145", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "73975a560983dfcdc361f145", "postgresql": "73975a560983dfcdc361f145", "snowflake": "73975a560983dfcdc361f145", @@ -24301,8 +24301,8 @@ "databricks": "c844c96e1e3ffc72d5f90df9", "duckdb": "8a3f094f18fb4047f0febf59", "generic": "c844c96e1e3ffc72d5f90df9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "28eb0d6de9724959ce5ba43e", +"motherduck": "8a3f094f18fb4047f0febf59", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c844c96e1e3ffc72d5f90df9", "postgresql": "c844c96e1e3ffc72d5f90df9", "snowflake": "c844c96e1e3ffc72d5f90df9", @@ -24318,8 +24318,8 @@ "databricks": "fedd65248cdf63b24b8c1567", "duckdb": "fedd65248cdf63b24b8c1567", "generic": "fedd65248cdf63b24b8c1567", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fedd65248cdf63b24b8c1567", +"motherduck": "fedd65248cdf63b24b8c1567", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "fedd65248cdf63b24b8c1567", "postgresql": "fedd65248cdf63b24b8c1567", "snowflake": "fedd65248cdf63b24b8c1567", @@ -24335,8 +24335,8 @@ "databricks": "e6ffdece2977be55da73b59a", "duckdb": "c50a316d091c821866e4bfba", "generic": "e6ffdece2977be55da73b59a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e6ffdece2977be55da73b59a", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -24352,8 +24352,8 @@ "databricks": "d79c3ac70093cb4cb531e71c", "duckdb": "d79c3ac70093cb4cb531e71c", "generic": "d79c3ac70093cb4cb531e71c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d79c3ac70093cb4cb531e71c", +"motherduck": "d79c3ac70093cb4cb531e71c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d79c3ac70093cb4cb531e71c", "postgresql": "d79c3ac70093cb4cb531e71c", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -24369,8 +24369,8 @@ "databricks": "3673b4fbbd8a7fed7120ec2b", "duckdb": "3673b4fbbd8a7fed7120ec2b", "generic": "3673b4fbbd8a7fed7120ec2b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3673b4fbbd8a7fed7120ec2b", +"motherduck": "3673b4fbbd8a7fed7120ec2b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9aff7fcc7d550e1a8d105471", "postgresql": "9aff7fcc7d550e1a8d105471", "snowflake": "3673b4fbbd8a7fed7120ec2b", @@ -24386,8 +24386,8 @@ "databricks": "fe1b4290ba0c2395d949643d", "duckdb": "d2e8f9f7288b371199b1652e", "generic": "fe1b4290ba0c2395d949643d", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fe1b4290ba0c2395d949643d", +"motherduck": "d2e8f9f7288b371199b1652e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e0367d4e3fcf904cd5657220", "postgresql": "e0367d4e3fcf904cd5657220", "snowflake": "c326a07d4a8adffde91e55cd", @@ -24403,8 +24403,8 @@ "databricks": "1e75e5227bea0ae294ac7af1", "duckdb": "19bdd92ea0cc5fb028657510", "generic": "b8321619b794db091c9e96a1", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b8321619b794db091c9e96a1", +"motherduck": "19bdd92ea0cc5fb028657510", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b8321619b794db091c9e96a1", "postgresql": "b8321619b794db091c9e96a1", "snowflake": "b8321619b794db091c9e96a1", @@ -24420,8 +24420,8 @@ "databricks": "77ffdb80029bd6e2cca768ad", "duckdb": "77ffdb80029bd6e2cca768ad", "generic": "77ffdb80029bd6e2cca768ad", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "77ffdb80029bd6e2cca768ad", +"motherduck": "77ffdb80029bd6e2cca768ad", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "77ffdb80029bd6e2cca768ad", "postgresql": "77ffdb80029bd6e2cca768ad", "snowflake": "77ffdb80029bd6e2cca768ad", @@ -24437,8 +24437,8 @@ "databricks": "b69a57be16780e0a5e4acadb", "duckdb": "b69a57be16780e0a5e4acadb", "generic": "b69a57be16780e0a5e4acadb", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b69a57be16780e0a5e4acadb", +"motherduck": "b69a57be16780e0a5e4acadb", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a7d1c1767c3d4e900c7d3cb4", "postgresql": "a7d1c1767c3d4e900c7d3cb4", "snowflake": "b69a57be16780e0a5e4acadb", @@ -24454,8 +24454,8 @@ "databricks": "c50a316d091c821866e4bfba", "duckdb": "c50a316d091c821866e4bfba", "generic": "c50a316d091c821866e4bfba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -24471,8 +24471,8 @@ "databricks": "f1625f38cf20f9627bc717a9", "duckdb": "f1625f38cf20f9627bc717a9", "generic": "f1625f38cf20f9627bc717a9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "f1625f38cf20f9627bc717a9", +"motherduck": "f1625f38cf20f9627bc717a9", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f1625f38cf20f9627bc717a9", "postgresql": "f1625f38cf20f9627bc717a9", "snowflake": "f1625f38cf20f9627bc717a9", @@ -24488,8 +24488,8 @@ "databricks": "e998cd8f0f74af0526b75ff0", "duckdb": "e998cd8f0f74af0526b75ff0", "generic": "e998cd8f0f74af0526b75ff0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e998cd8f0f74af0526b75ff0", +"motherduck": "e998cd8f0f74af0526b75ff0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e998cd8f0f74af0526b75ff0", "postgresql": "e998cd8f0f74af0526b75ff0", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -24505,8 +24505,8 @@ "databricks": "ebe2513b6cb4001e42cc464b", "duckdb": "fa45b7e4a9ef3b5a9c5781af", "generic": "ebe2513b6cb4001e42cc464b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ebe2513b6cb4001e42cc464b", +"motherduck": "fa45b7e4a9ef3b5a9c5781af", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "fa45b7e4a9ef3b5a9c5781af", "postgresql": "fa45b7e4a9ef3b5a9c5781af", "snowflake": "fa45b7e4a9ef3b5a9c5781af", @@ -24522,8 +24522,8 @@ "databricks": "4b356542044f1267fcfc487a", "duckdb": "5abf4ec799a2ec32fb10da7e", "generic": "4b356542044f1267fcfc487a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "4b356542044f1267fcfc487a", +"motherduck": "5abf4ec799a2ec32fb10da7e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a5656d70e208254d1d3e9b4f", "postgresql": "a5656d70e208254d1d3e9b4f", "snowflake": "4b356542044f1267fcfc487a", @@ -24539,8 +24539,8 @@ "databricks": "79278352c17c9de0f5a28af5", "duckdb": "1ebac487198b6c90b027faab", "generic": "79278352c17c9de0f5a28af5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "79278352c17c9de0f5a28af5", +"motherduck": "1ebac487198b6c90b027faab", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "79278352c17c9de0f5a28af5", "postgresql": "79278352c17c9de0f5a28af5", "snowflake": "97a20344d811fb77c7690863", @@ -24556,8 +24556,8 @@ "databricks": "70f3d045595fbd43219efb36", "duckdb": "1fe8a2d5a41383bd8e502808", "generic": "70f3d045595fbd43219efb36", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "70f3d045595fbd43219efb36", +"motherduck": "1fe8a2d5a41383bd8e502808", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3c17ab67cd8c034fc4911ef2", "postgresql": "3c17ab67cd8c034fc4911ef2", "snowflake": "1fe8a2d5a41383bd8e502808", @@ -24573,8 +24573,8 @@ "databricks": "c50a316d091c821866e4bfba", "duckdb": "c50a316d091c821866e4bfba", "generic": "c50a316d091c821866e4bfba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -24590,8 +24590,8 @@ "databricks": "035bb58fd2c7dca585a5899c", "duckdb": "035bb58fd2c7dca585a5899c", "generic": "035bb58fd2c7dca585a5899c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "035bb58fd2c7dca585a5899c", +"motherduck": "035bb58fd2c7dca585a5899c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "035bb58fd2c7dca585a5899c", "postgresql": "035bb58fd2c7dca585a5899c", "snowflake": "035bb58fd2c7dca585a5899c", @@ -24607,8 +24607,8 @@ "databricks": "d4b89e61a66aeb076db92954", "duckdb": "7515c0db413239044adef879", "generic": "d4b89e61a66aeb076db92954", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d4b89e61a66aeb076db92954", +"motherduck": "7515c0db413239044adef879", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "7515c0db413239044adef879", "postgresql": "7515c0db413239044adef879", "snowflake": "67b5874a2c569226c490211f", @@ -24624,8 +24624,8 @@ "databricks": "62368fc15cefd5807807b209", "duckdb": "a3b6b25265be330ec52a3322", "generic": "62368fc15cefd5807807b209", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "62368fc15cefd5807807b209", +"motherduck": "a3b6b25265be330ec52a3322", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "62368fc15cefd5807807b209", "postgresql": "62368fc15cefd5807807b209", "snowflake": "919b7ec6a7b49d8527b784cd", @@ -24641,8 +24641,8 @@ "databricks": "d61c83ef8bfdf043fa672cb5", "duckdb": "d61c83ef8bfdf043fa672cb5", "generic": "d61c83ef8bfdf043fa672cb5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d61c83ef8bfdf043fa672cb5", +"motherduck": "d61c83ef8bfdf043fa672cb5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d61c83ef8bfdf043fa672cb5", "postgresql": "d61c83ef8bfdf043fa672cb5", "snowflake": "97a20344d811fb77c7690863", @@ -24658,8 +24658,8 @@ "databricks": "268b9b7c7862c9de6569286a", "duckdb": "268b9b7c7862c9de6569286a", "generic": "268b9b7c7862c9de6569286a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "268b9b7c7862c9de6569286a", +"motherduck": "268b9b7c7862c9de6569286a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "268b9b7c7862c9de6569286a", "postgresql": "268b9b7c7862c9de6569286a", "snowflake": "268b9b7c7862c9de6569286a", @@ -24675,8 +24675,8 @@ "databricks": "b137c998bdaa2555d912e8fa", "duckdb": "27f973a6b2da27a79f5c0074", "generic": "b137c998bdaa2555d912e8fa", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b137c998bdaa2555d912e8fa", +"motherduck": "27f973a6b2da27a79f5c0074", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "b137c998bdaa2555d912e8fa", "postgresql": "b137c998bdaa2555d912e8fa", "snowflake": "b137c998bdaa2555d912e8fa", @@ -24692,8 +24692,8 @@ "databricks": "8e94482f81cd02e976359898", "duckdb": "efd6c6e5ae853495e89afda4", "generic": "8e94482f81cd02e976359898", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "8e94482f81cd02e976359898", +"motherduck": "efd6c6e5ae853495e89afda4", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "efd6c6e5ae853495e89afda4", "postgresql": "efd6c6e5ae853495e89afda4", "snowflake": "311749caf0bd53fee7b23fa1", @@ -24709,8 +24709,8 @@ "databricks": "d5b8a68e96a7848f2aa7b2d6", "duckdb": "d5b8a68e96a7848f2aa7b2d6", "generic": "d5b8a68e96a7848f2aa7b2d6", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d5b8a68e96a7848f2aa7b2d6", +"motherduck": "d5b8a68e96a7848f2aa7b2d6", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d5b8a68e96a7848f2aa7b2d6", "postgresql": "d5b8a68e96a7848f2aa7b2d6", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -24726,8 +24726,8 @@ "databricks": "1fa913156c869f5373066b2b", "duckdb": "1fa913156c869f5373066b2b", "generic": "1fa913156c869f5373066b2b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1fa913156c869f5373066b2b", +"motherduck": "1fa913156c869f5373066b2b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1fa913156c869f5373066b2b", "postgresql": "1fa913156c869f5373066b2b", "snowflake": "1fa913156c869f5373066b2b", @@ -24743,8 +24743,8 @@ "databricks": "3673b4fbbd8a7fed7120ec2b", "duckdb": "3673b4fbbd8a7fed7120ec2b", "generic": "3673b4fbbd8a7fed7120ec2b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "3673b4fbbd8a7fed7120ec2b", +"motherduck": "3673b4fbbd8a7fed7120ec2b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9aff7fcc7d550e1a8d105471", "postgresql": "9aff7fcc7d550e1a8d105471", "snowflake": "3673b4fbbd8a7fed7120ec2b", @@ -24760,8 +24760,8 @@ "databricks": "7770c7aba92f1bba835ef3f8", "duckdb": "7770c7aba92f1bba835ef3f8", "generic": "7770c7aba92f1bba835ef3f8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "7770c7aba92f1bba835ef3f8", +"motherduck": "7770c7aba92f1bba835ef3f8", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "55de7125c9ef4832b853c094", "postgresql": "55de7125c9ef4832b853c094", "snowflake": "7770c7aba92f1bba835ef3f8", @@ -24777,8 +24777,8 @@ "databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", "generic": "fb4b6f5101f973d16ea8d1f3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fb4b6f5101f973d16ea8d1f3", +"motherduck": "70fb4728c3adcf6dbe4975d5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", @@ -24794,8 +24794,8 @@ "databricks": "e57ac1541fb51b362cd5b470", "duckdb": "e57ac1541fb51b362cd5b470", "generic": "e57ac1541fb51b362cd5b470", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "e57ac1541fb51b362cd5b470", +"motherduck": "e57ac1541fb51b362cd5b470", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "e57ac1541fb51b362cd5b470", "postgresql": "e57ac1541fb51b362cd5b470", "snowflake": "4b356542044f1267fcfc487a", @@ -24811,8 +24811,8 @@ "databricks": "b1961f9482bbd04d61ba1c52", "duckdb": "4cce9f29790ed5c86ebce81d", "generic": "00e3060d99b1d939ec9f3616", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "00e3060d99b1d939ec9f3616", +"motherduck": "4cce9f29790ed5c86ebce81d", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "6e5449b80c5f377b124fba88", "postgresql": "6e5449b80c5f377b124fba88", "snowflake": "cfa49b8de6f3939e2701f38d", @@ -24828,8 +24828,8 @@ "databricks": "6282d36888b8e0aadda09779", "duckdb": "431ce1fed4f6ce1e6c2a6479", "generic": "6282d36888b8e0aadda09779", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "431ce1fed4f6ce1e6c2a6479", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "431ce1fed4f6ce1e6c2a6479", "postgresql": "431ce1fed4f6ce1e6c2a6479", "snowflake": "4ea9204c46e0ec4e3a94f672", @@ -24845,8 +24845,8 @@ "databricks": "9917d6c8c04307304904313b", "duckdb": "9917d6c8c04307304904313b", "generic": "9917d6c8c04307304904313b", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9917d6c8c04307304904313b", +"motherduck": "9917d6c8c04307304904313b", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9917d6c8c04307304904313b", "postgresql": "9917d6c8c04307304904313b", "snowflake": "9917d6c8c04307304904313b", @@ -24862,8 +24862,8 @@ "databricks": "c50a316d091c821866e4bfba", "duckdb": "c50a316d091c821866e4bfba", "generic": "c50a316d091c821866e4bfba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -24879,8 +24879,8 @@ "databricks": "0712c33a016f7dd6a37ee011", "duckdb": "0712c33a016f7dd6a37ee011", "generic": "0712c33a016f7dd6a37ee011", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0712c33a016f7dd6a37ee011", +"motherduck": "0712c33a016f7dd6a37ee011", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0712c33a016f7dd6a37ee011", "postgresql": "0712c33a016f7dd6a37ee011", "snowflake": "0712c33a016f7dd6a37ee011", @@ -24896,8 +24896,8 @@ "databricks": "fe23594c781e79cbb0c8927a", "duckdb": "fe23594c781e79cbb0c8927a", "generic": "fe23594c781e79cbb0c8927a", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fe23594c781e79cbb0c8927a", +"motherduck": "fe23594c781e79cbb0c8927a", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "8a3f094f18fb4047f0febf59", "postgresql": "8a3f094f18fb4047f0febf59", "snowflake": "38b2ec94bf02be40007ce525", @@ -24913,8 +24913,8 @@ "databricks": "22bed6e6de77311e5f24d414", "duckdb": "909ab87c62786e3181c86ba6", "generic": "22bed6e6de77311e5f24d414", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "22bed6e6de77311e5f24d414", +"motherduck": "909ab87c62786e3181c86ba6", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "22bed6e6de77311e5f24d414", "postgresql": "22bed6e6de77311e5f24d414", "snowflake": "05188197e056965f895710b0", @@ -24930,8 +24930,8 @@ "databricks": "ee47e1424cb01deb0cf51760", "duckdb": "ee47e1424cb01deb0cf51760", "generic": "ee47e1424cb01deb0cf51760", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ee47e1424cb01deb0cf51760", +"motherduck": "ee47e1424cb01deb0cf51760", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3c1521654088dd54d0dc9ead", "postgresql": "3c1521654088dd54d0dc9ead", "snowflake": "ee47e1424cb01deb0cf51760", @@ -24947,8 +24947,8 @@ "databricks": "49fdedaa5356fcccaa6f2f38", "duckdb": "49fdedaa5356fcccaa6f2f38", "generic": "49fdedaa5356fcccaa6f2f38", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "49fdedaa5356fcccaa6f2f38", +"motherduck": "49fdedaa5356fcccaa6f2f38", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "cf14fd6412673db81925b3a0", "postgresql": "cf14fd6412673db81925b3a0", "snowflake": "49fdedaa5356fcccaa6f2f38", @@ -24964,8 +24964,8 @@ "databricks": "fb4b6f5101f973d16ea8d1f3", "duckdb": "70fb4728c3adcf6dbe4975d5", "generic": "fb4b6f5101f973d16ea8d1f3", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fb4b6f5101f973d16ea8d1f3", +"motherduck": "70fb4728c3adcf6dbe4975d5", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "70fb4728c3adcf6dbe4975d5", "postgresql": "70fb4728c3adcf6dbe4975d5", "snowflake": "70fb4728c3adcf6dbe4975d5", @@ -24981,8 +24981,8 @@ "databricks": "1e75e5227bea0ae294ac7af1", "duckdb": "19bdd92ea0cc5fb028657510", "generic": "b8321619b794db091c9e96a1", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "b8321619b794db091c9e96a1", +"motherduck": "19bdd92ea0cc5fb028657510", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "bce26544873d6ec9191c9a86", "postgresql": "bce26544873d6ec9191c9a86", "snowflake": "b8321619b794db091c9e96a1", @@ -24998,8 +24998,8 @@ "databricks": "46e6767a9ba2cc5fbfdc02d0", "duckdb": "9f79ba9960d200f49192fa87", "generic": "46e6767a9ba2cc5fbfdc02d0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "6c03a3450ea4742ee7396e3c", +"motherduck": "9f79ba9960d200f49192fa87", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "9f79ba9960d200f49192fa87", "postgresql": "9f79ba9960d200f49192fa87", "snowflake": "72c40a5d469c974c1952c3c2", @@ -25015,8 +25015,8 @@ "databricks": "c50a316d091c821866e4bfba", "duckdb": "c50a316d091c821866e4bfba", "generic": "c50a316d091c821866e4bfba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -25032,8 +25032,8 @@ "databricks": "d61c83ef8bfdf043fa672cb5", "duckdb": "305aaf0b95cee629dd46380c", "generic": "d61c83ef8bfdf043fa672cb5", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "d61c83ef8bfdf043fa672cb5", +"motherduck": "305aaf0b95cee629dd46380c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "d61c83ef8bfdf043fa672cb5", "postgresql": "d61c83ef8bfdf043fa672cb5", "snowflake": "97a20344d811fb77c7690863", @@ -25049,8 +25049,8 @@ "databricks": "0dc0164f07551711f23943eb", "duckdb": "0dc0164f07551711f23943eb", "generic": "0dc0164f07551711f23943eb", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0dc0164f07551711f23943eb", +"motherduck": "0dc0164f07551711f23943eb", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0dc0164f07551711f23943eb", "postgresql": "0dc0164f07551711f23943eb", "snowflake": "0dc0164f07551711f23943eb", @@ -25066,8 +25066,8 @@ "databricks": "a428302e4848ffe877284c84", "duckdb": "a428302e4848ffe877284c84", "generic": "a428302e4848ffe877284c84", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a428302e4848ffe877284c84", +"motherduck": "a428302e4848ffe877284c84", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a428302e4848ffe877284c84", "postgresql": "a428302e4848ffe877284c84", "snowflake": "652ff8b21f638eac8fdc46c5", @@ -25083,8 +25083,8 @@ "databricks": "0f799a95a4f5ef502897f02c", "duckdb": "0f799a95a4f5ef502897f02c", "generic": "0f799a95a4f5ef502897f02c", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0f799a95a4f5ef502897f02c", +"motherduck": "0f799a95a4f5ef502897f02c", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0f799a95a4f5ef502897f02c", "postgresql": "0f799a95a4f5ef502897f02c", "snowflake": "0f799a95a4f5ef502897f02c", @@ -25100,8 +25100,8 @@ "databricks": "2bfa352725f9d8a91250f75e", "duckdb": "2bfa352725f9d8a91250f75e", "generic": "2bfa352725f9d8a91250f75e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2bfa352725f9d8a91250f75e", +"motherduck": "2bfa352725f9d8a91250f75e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "2bfa352725f9d8a91250f75e", "postgresql": "2bfa352725f9d8a91250f75e", "snowflake": "2bfa352725f9d8a91250f75e", @@ -25117,8 +25117,8 @@ "databricks": "bda0c90e573c6a01b55627e2", "duckdb": "bda0c90e573c6a01b55627e2", "generic": "bda0c90e573c6a01b55627e2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "bda0c90e573c6a01b55627e2", +"motherduck": "bda0c90e573c6a01b55627e2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "bda0c90e573c6a01b55627e2", "postgresql": "bda0c90e573c6a01b55627e2", "snowflake": "bda0c90e573c6a01b55627e2", @@ -25134,8 +25134,8 @@ "databricks": "2705f19942a4194a72576911", "duckdb": "2705f19942a4194a72576911", "generic": "2705f19942a4194a72576911", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "2705f19942a4194a72576911", +"motherduck": "2705f19942a4194a72576911", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "5226d7e2a62c7650a87d6e84", "postgresql": "5226d7e2a62c7650a87d6e84", "snowflake": "2705f19942a4194a72576911", @@ -25151,8 +25151,8 @@ "databricks": "558da38ec4d9d5d6c72f60cf", "duckdb": "558da38ec4d9d5d6c72f60cf", "generic": "558da38ec4d9d5d6c72f60cf", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "558da38ec4d9d5d6c72f60cf", +"motherduck": "558da38ec4d9d5d6c72f60cf", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "558da38ec4d9d5d6c72f60cf", "postgresql": "558da38ec4d9d5d6c72f60cf", "snowflake": "558da38ec4d9d5d6c72f60cf", @@ -25168,8 +25168,8 @@ "databricks": "9a66317fb8d040bf991502c0", "duckdb": "314ed506f24e92300a425780", "generic": "9a66317fb8d040bf991502c0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9a66317fb8d040bf991502c0", +"motherduck": "314ed506f24e92300a425780", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "314ed506f24e92300a425780", "postgresql": "314ed506f24e92300a425780", "snowflake": "314ed506f24e92300a425780", @@ -25185,8 +25185,8 @@ "databricks": "0b8ad171901cc2b7725617da", "duckdb": "0b8ad171901cc2b7725617da", "generic": "0b8ad171901cc2b7725617da", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0b8ad171901cc2b7725617da", +"motherduck": "0b8ad171901cc2b7725617da", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0b8ad171901cc2b7725617da", "postgresql": "0b8ad171901cc2b7725617da", "snowflake": "0b8ad171901cc2b7725617da", @@ -25202,8 +25202,8 @@ "databricks": "9a63ad6168418af23d9b6fd1", "duckdb": "363159f251e264604338e690", "generic": "9a63ad6168418af23d9b6fd1", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "9a63ad6168418af23d9b6fd1", +"motherduck": "363159f251e264604338e690", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f276e907770196606577c932", "postgresql": "f276e907770196606577c932", "snowflake": "363159f251e264604338e690", @@ -25219,8 +25219,8 @@ "databricks": "e3bc1b8bad22d8104417b00f", "duckdb": "1d8b945fd793ecdcacb90c21", "generic": "1d8b945fd793ecdcacb90c21", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1d8b945fd793ecdcacb90c21", +"motherduck": "1d8b945fd793ecdcacb90c21", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "3efe8bb57c98fcbe74096b3c", "postgresql": "3efe8bb57c98fcbe74096b3c", "snowflake": "1d8b945fd793ecdcacb90c21", @@ -25236,8 +25236,8 @@ "databricks": "52c81331cf7cf6b56395e239", "duckdb": "52c81331cf7cf6b56395e239", "generic": "52c81331cf7cf6b56395e239", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "52c81331cf7cf6b56395e239", +"motherduck": "52c81331cf7cf6b56395e239", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "52c81331cf7cf6b56395e239", "postgresql": "52c81331cf7cf6b56395e239", "snowflake": "52c81331cf7cf6b56395e239", @@ -25253,8 +25253,8 @@ "databricks": "ebc821a95b9286e22c2b26d0", "duckdb": "ebc821a95b9286e22c2b26d0", "generic": "ebc821a95b9286e22c2b26d0", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "ebc821a95b9286e22c2b26d0", +"motherduck": "ebc821a95b9286e22c2b26d0", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "ebc821a95b9286e22c2b26d0", "postgresql": "ebc821a95b9286e22c2b26d0", "snowflake": "ebc821a95b9286e22c2b26d0", @@ -25270,8 +25270,8 @@ "databricks": "85569b6cabaea71507846025", "duckdb": "85569b6cabaea71507846025", "generic": "85569b6cabaea71507846025", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "85569b6cabaea71507846025", +"motherduck": "85569b6cabaea71507846025", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "85569b6cabaea71507846025", "postgresql": "85569b6cabaea71507846025", "snowflake": "85569b6cabaea71507846025", @@ -25287,8 +25287,8 @@ "databricks": "7f7363467010768b3de8fe05", "duckdb": "7f7363467010768b3de8fe05", "generic": "7f7363467010768b3de8fe05", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "7f7363467010768b3de8fe05", +"motherduck": "7f7363467010768b3de8fe05", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "039da801fa79cb9eabce307d", "postgresql": "039da801fa79cb9eabce307d", "snowflake": "7f7363467010768b3de8fe05", @@ -25304,8 +25304,8 @@ "databricks": "cc5643d29bc94cb1374704e9", "duckdb": "cc5643d29bc94cb1374704e9", "generic": "cc5643d29bc94cb1374704e9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "cc5643d29bc94cb1374704e9", +"motherduck": "cc5643d29bc94cb1374704e9", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "cc5643d29bc94cb1374704e9", "postgresql": "cc5643d29bc94cb1374704e9", "snowflake": "cc5643d29bc94cb1374704e9", @@ -25321,8 +25321,8 @@ "databricks": "c50a316d091c821866e4bfba", "duckdb": "c50a316d091c821866e4bfba", "generic": "c50a316d091c821866e4bfba", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c50a316d091c821866e4bfba", +"motherduck": "c50a316d091c821866e4bfba", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "c50a316d091c821866e4bfba", "postgresql": "c50a316d091c821866e4bfba", "snowflake": "c50a316d091c821866e4bfba", @@ -25338,8 +25338,8 @@ "databricks": "c91ea1fc2032c6f196132d1e", "duckdb": "c91ea1fc2032c6f196132d1e", "generic": "c91ea1fc2032c6f196132d1e", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "c91ea1fc2032c6f196132d1e", +"motherduck": "c91ea1fc2032c6f196132d1e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "652ff8b21f638eac8fdc46c5", "postgresql": "652ff8b21f638eac8fdc46c5", "snowflake": "c91ea1fc2032c6f196132d1e", @@ -25355,8 +25355,8 @@ "databricks": "87496ad15619ba3d233601ad", "duckdb": "87496ad15619ba3d233601ad", "generic": "87496ad15619ba3d233601ad", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "87496ad15619ba3d233601ad", +"motherduck": "87496ad15619ba3d233601ad", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "87496ad15619ba3d233601ad", "postgresql": "87496ad15619ba3d233601ad", "snowflake": "87496ad15619ba3d233601ad", @@ -25372,8 +25372,8 @@ "databricks": "a33d60d9b009878ab623c1d8", "duckdb": "23f70cb2333740d79361d52e", "generic": "a33d60d9b009878ab623c1d8", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a33d60d9b009878ab623c1d8", +"motherduck": "23f70cb2333740d79361d52e", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "f276e907770196606577c932", "postgresql": "f276e907770196606577c932", "snowflake": "23f70cb2333740d79361d52e", @@ -25389,8 +25389,8 @@ "databricks": "1eb7fb4e8cf75773c5573562", "duckdb": "1eb7fb4e8cf75773c5573562", "generic": "1eb7fb4e8cf75773c5573562", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "1eb7fb4e8cf75773c5573562", +"motherduck": "1eb7fb4e8cf75773c5573562", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "1eb7fb4e8cf75773c5573562", "postgresql": "1eb7fb4e8cf75773c5573562", "snowflake": "97a20344d811fb77c7690863", @@ -25406,8 +25406,8 @@ "databricks": "300e2b67af892b463d85f9cd", "duckdb": "300e2b67af892b463d85f9cd", "generic": "300e2b67af892b463d85f9cd", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "300e2b67af892b463d85f9cd", +"motherduck": "300e2b67af892b463d85f9cd", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "300e2b67af892b463d85f9cd", "postgresql": "300e2b67af892b463d85f9cd", "snowflake": "300e2b67af892b463d85f9cd", @@ -25423,8 +25423,8 @@ "databricks": "669084d1ea704ae5b8ee05c2", "duckdb": "669084d1ea704ae5b8ee05c2", "generic": "669084d1ea704ae5b8ee05c2", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "669084d1ea704ae5b8ee05c2", +"motherduck": "669084d1ea704ae5b8ee05c2", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "93c1b5b5960e26479baf88d0", "postgresql": "93c1b5b5960e26479baf88d0", "snowflake": "669084d1ea704ae5b8ee05c2", @@ -25440,8 +25440,8 @@ "databricks": "08736485547ba339930697e9", "duckdb": "08736485547ba339930697e9", "generic": "08736485547ba339930697e9", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "08736485547ba339930697e9", +"motherduck": "08736485547ba339930697e9", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "eecd452b4f5f3f4133d0bd0a", "postgresql": "eecd452b4f5f3f4133d0bd0a", "snowflake": "08736485547ba339930697e9", @@ -25457,8 +25457,8 @@ "databricks": "0c4610f182f18bf41d306e01", "duckdb": "0c4610f182f18bf41d306e01", "generic": "0c4610f182f18bf41d306e01", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "0c4610f182f18bf41d306e01", +"motherduck": "0c4610f182f18bf41d306e01", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "0c4610f182f18bf41d306e01", "postgresql": "0c4610f182f18bf41d306e01", "snowflake": "0c4610f182f18bf41d306e01", @@ -25474,8 +25474,8 @@ "databricks": "fad6caec1e65529a6edc32df", "duckdb": "fad6caec1e65529a6edc32df", "generic": "fad6caec1e65529a6edc32df", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "fad6caec1e65529a6edc32df", +"motherduck": "fad6caec1e65529a6edc32df", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "fad6caec1e65529a6edc32df", "postgresql": "fad6caec1e65529a6edc32df", "snowflake": "fad6caec1e65529a6edc32df", @@ -25491,8 +25491,8 @@ "databricks": "a428302e4848ffe877284c84", "duckdb": "a428302e4848ffe877284c84", "generic": "a428302e4848ffe877284c84", -"motherduck": "901c5133ed47d4769286e860", -"mysql": "a428302e4848ffe877284c84", +"motherduck": "a428302e4848ffe877284c84", +"mysql": "9565c6d3005d7730ba35e40b", "postgres": "a428302e4848ffe877284c84", "postgresql": "a428302e4848ffe877284c84", "snowflake": "652ff8b21f638eac8fdc46c5", diff --git a/tests/integration/src/sqlbuild/adapter/type_system/test_native_type_normalization.py b/tests/integration/src/sqlbuild/adapter/type_system/test_native_type_normalization.py index 81c037456e..bbd2847af9 100644 --- a/tests/integration/src/sqlbuild/adapter/type_system/test_native_type_normalization.py +++ b/tests/integration/src/sqlbuild/adapter/type_system/test_native_type_normalization.py @@ -213,9 +213,9 @@ def test_given_generated_types_when_normalizing_then_every_known_dialect_answers expected_parse_error_logged=True, ), TypeNormalizationTestCase( - description="a dialect outside the old native build", + description="MotherDuck reads types as DuckDB", type_sql="INT", - dialect="mysql", + dialect="motherduck", expected_type=NormalizedType(normalized_name="INT", family=TypeFamily.INTEGER), expected_parse_error_logged=False, ), @@ -244,9 +244,9 @@ def test_given_type_when_normalizing_then_python_shape_is_returned( "test_case", [ UnknownDialectTypeTestCase( - description="a dialect name SQLBuild does not know", - dialect="motherduck", - expected_error="ValueError: Unknown dialect: motherduck", + description="a dialect no SQLBuild adapter ships", + dialect="mysql", + expected_error="ValueError: Unknown dialect: mysql", ), ], ids=lambda case: case.description, diff --git a/tests/integration/src/sqlbuild/compiler/contracts/test_native_model_contracts.py b/tests/integration/src/sqlbuild/compiler/contracts/test_native_model_contracts.py index f7e8c93063..3d5658116a 100644 --- a/tests/integration/src/sqlbuild/compiler/contracts/test_native_model_contracts.py +++ b/tests/integration/src/sqlbuild/compiler/contracts/test_native_model_contracts.py @@ -128,9 +128,9 @@ def test_given_generated_contracts_when_validating_then_every_contract_family_is ), ), FormerlyDeferredContractTestCase( - description="a dialect outside the old native build", + description="a shipped dialect without a native type parser", declared_type="INTEGER", - dialect="mysql", + dialect="tsql", expected_diagnostics=( ( "K002", @@ -184,9 +184,9 @@ def test_given_input_python_once_answered_when_validating_then_native_diagnostic "test_case", [ UnknownDialectContractTestCase( - description="a dialect Polyglot does not know raises the wheel's error", - dialect="motherduck", - expected_error="Unknown dialect: motherduck", + description="a dialect no adapter ships raises the wheel's error", + dialect="mysql", + expected_error="Unknown dialect: mysql", ), ], ids=lambda case: case.description, diff --git a/tests/integration/src/sqlbuild/compiler/semantic_checks/test_native_metadata_checks.py b/tests/integration/src/sqlbuild/compiler/semantic_checks/test_native_metadata_checks.py index de7023ba50..21389df895 100644 --- a/tests/integration/src/sqlbuild/compiler/semantic_checks/test_native_metadata_checks.py +++ b/tests/integration/src/sqlbuild/compiler/semantic_checks/test_native_metadata_checks.py @@ -56,7 +56,7 @@ seed=20261017, count=4, model_count=10, - dialects=("duckdb", "mysql", "tsql", None, "nonsense"), + dialects=("duckdb", "clickhouse", "tsql", None, "nonsense"), non_ascii=True, golden_prefix="metadata_formerly_deferred", expected_minimum_native=12, diff --git a/tests/integration/src/sqlbuild/compiler/semantic_checks/test_native_semantic_checks.py b/tests/integration/src/sqlbuild/compiler/semantic_checks/test_native_semantic_checks.py index 1f2c4f7dcb..d7d5d1d6dd 100644 --- a/tests/integration/src/sqlbuild/compiler/semantic_checks/test_native_semantic_checks.py +++ b/tests/integration/src/sqlbuild/compiler/semantic_checks/test_native_semantic_checks.py @@ -132,7 +132,7 @@ def test_given_generated_failing_projects_when_completing_natively_then_matches_ seed=20261015, count=2, non_ascii=False, - dialects=("mysql", "tsql", "databricks", "sqlite", "oracle", "trino"), + dialects=("tsql", "databricks", "sqlite", "clickhouse"), expected_outcomes=frozenset({"completed"}), ), FormerlyDeferredSemanticTestCase( From e0638cd48ef2917d0d1788cf7f4d22f5ba29f562 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 08:27:38 +0000 Subject: [PATCH 029/106] refactor(rules): read the shared-parser dialect from one lookup --- .../sql_tree/_helpers/rules_tree_parser.rs | 16 +++----------- .../src/sql_tree/main/mod.rs | 1 + .../src/sql_tree/main/shared_dialect.rs | 22 +++++++++++++++++++ .../src/query_facts/_helpers/facts.rs | 21 +++--------------- .../src/query_facts/constants.rs | 8 ------- .../src/query_facts/main/facts_available.rs | 4 +++- 6 files changed, 32 insertions(+), 40 deletions(-) create mode 100644 crates/sqlbuild-analysis/src/sql_tree/main/shared_dialect.rs diff --git a/crates/sqlbuild-analysis/src/sql_tree/_helpers/rules_tree_parser.rs b/crates/sqlbuild-analysis/src/sql_tree/_helpers/rules_tree_parser.rs index b330c0a39c..db8eb603d9 100644 --- a/crates/sqlbuild-analysis/src/sql_tree/_helpers/rules_tree_parser.rs +++ b/crates/sqlbuild-analysis/src/sql_tree/_helpers/rules_tree_parser.rs @@ -1,10 +1,7 @@ //! `rules_tree_parser`: the parser that reads a dialect's SQL for rules. -use sqlquack_syntax::Dialect; - -use crate::sql_tree::constants::{ - DUCKDB_DIALECTS, POSTGRES_DIALECTS, SNOWFLAKE_DIALECT, SQLITE_DIALECT, -}; +use crate::sql_tree::constants::POSTGRES_DIALECTS; +use crate::sql_tree::main::shared_dialect::shared_dialect; use crate::sql_tree::models::TreeParser; /// The parser for `dialect`, or `None`; add a dialect (one line) only once its parser passes the depth probe with an empty allowlist. @@ -12,12 +9,5 @@ pub(crate) fn rules_tree_parser(dialect: &str) -> Option { if POSTGRES_DIALECTS.contains(&dialect) { return Some(TreeParser::Postgres); } - if DUCKDB_DIALECTS.contains(&dialect) { - return Some(TreeParser::Shared(Dialect::DuckDB)); - } - match dialect { - SNOWFLAKE_DIALECT => Some(TreeParser::Shared(Dialect::Snowflake)), - SQLITE_DIALECT => Some(TreeParser::Shared(Dialect::SQLite)), - _ => None, - } + shared_dialect(dialect).map(TreeParser::Shared) } diff --git a/crates/sqlbuild-analysis/src/sql_tree/main/mod.rs b/crates/sqlbuild-analysis/src/sql_tree/main/mod.rs index 0c71a2f7d5..924d435a79 100644 --- a/crates/sqlbuild-analysis/src/sql_tree/main/mod.rs +++ b/crates/sqlbuild-analysis/src/sql_tree/main/mod.rs @@ -1,3 +1,4 @@ //! Entry points. +pub mod shared_dialect; pub mod sql_tree_nodes; diff --git a/crates/sqlbuild-analysis/src/sql_tree/main/shared_dialect.rs b/crates/sqlbuild-analysis/src/sql_tree/main/shared_dialect.rs new file mode 100644 index 0000000000..5a7e95518e --- /dev/null +++ b/crates/sqlbuild-analysis/src/sql_tree/main/shared_dialect.rs @@ -0,0 +1,22 @@ +//! `shared_dialect`: the grammar of SQLQuack's shared parser that reads a dialect's SQL. + +use sqlquack_syntax::Dialect; + +use crate::sql_tree::constants::{ + DUCKDB_DIALECTS, POSTGRES_DIALECTS, SNOWFLAKE_DIALECT, SQLITE_DIALECT, +}; + +/// The shared-parser grammar for `dialect`, or `None` when the shared parser reads none of it. +pub fn shared_dialect(dialect: &str) -> Option { + if DUCKDB_DIALECTS.contains(&dialect) { + return Some(Dialect::DuckDB); + } + if POSTGRES_DIALECTS.contains(&dialect) { + return Some(Dialect::PostgreSQL); + } + match dialect { + SNOWFLAKE_DIALECT => Some(Dialect::Snowflake), + SQLITE_DIALECT => Some(Dialect::SQLite), + _ => None, + } +} diff --git a/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs b/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs index 959cf65122..3b468b18cb 100644 --- a/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs +++ b/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs @@ -1,14 +1,14 @@ use crate::query_facts::_helpers::shape::stub_call; use crate::query_facts::_helpers::tokens::{ComparisonOperator, QueryTokens, SelectTokens}; use crate::query_facts::constants::{ - DUCKDB_DIALECTS, MAX_AST_DEPTH, MAX_FUNCTION_CALL_DEPTH, POSTGRES_DIALECTS, - SNOWFLAKE_DATE_PART_ARGUMENTS, SNOWFLAKE_DIALECT, SQLITE_DIALECT, TABLE_FUNCTION_STUB, + MAX_AST_DEPTH, MAX_FUNCTION_CALL_DEPTH, SNOWFLAKE_DATE_PART_ARGUMENTS, TABLE_FUNCTION_STUB, TRIPLE_QUOTE_LENGTH, }; use crate::query_facts::models::{ CallKind, ColumnFact, ComparisonFacts, ComparisonSpan, CteFacts, ExpressionItem, ItemFacts, ModelShape, ParsedFixture, QueryFacts, RelationFacts, SelectFacts, SetFacts, SourceText, }; +use sqlbuild_analysis::sql_tree::main::shared_dialect::shared_dialect; use sqlquack_syntax::expressions::names::models::{Identifier, Literal, TableRef}; use sqlquack_syntax::expressions::queries::models::{Select, With}; use sqlquack_syntax::{ @@ -18,23 +18,8 @@ use std::cell::{Cell, RefCell}; use std::collections::{HashMap, HashSet}; use std::ops::Range; -/// The SQLQuack grammar that reads `dialect_name` for built-in rules, or `None` when none does. -pub(crate) fn facts_dialect(dialect_name: &str) -> Option { - if DUCKDB_DIALECTS.contains(&dialect_name) { - return Some(Dialect::DuckDB); - } - if POSTGRES_DIALECTS.contains(&dialect_name) { - return Some(Dialect::PostgreSQL); - } - match dialect_name { - SNOWFLAKE_DIALECT => Some(Dialect::Snowflake), - SQLITE_DIALECT => Some(Dialect::SQLite), - _ => None, - } -} - fn parse_query(sql: &str, dialect_name: &str) -> Result<(Expression, Dialect), String> { - let dialect = facts_dialect(dialect_name) + let dialect = shared_dialect(dialect_name) .ok_or_else(|| format!("no SQL parser for dialect {dialect_name}"))?; let options = ParseOptions { complexity_guard: Some(ComplexityGuardOptions { diff --git a/crates/sqlbuild-rules/src/query_facts/constants.rs b/crates/sqlbuild-rules/src/query_facts/constants.rs index a473ddfef1..2ff3bfcad4 100644 --- a/crates/sqlbuild-rules/src/query_facts/constants.rs +++ b/crates/sqlbuild-rules/src/query_facts/constants.rs @@ -12,14 +12,6 @@ pub(crate) const UDF_CALL: &str = "__udf"; pub(crate) const TABLE_FUNCTION_CALL: &str = "__table_fn"; pub(crate) const MIN_CAST_TOKENS: usize = 2; pub(crate) const TRIPLE_QUOTE_LENGTH: usize = 3; -/// Dialect names read with the shared DuckDB grammar; `generic` matches the fingerprint tokenizer. -pub(crate) const DUCKDB_DIALECTS: [&str; 3] = ["duckdb", "motherduck", "generic"]; -/// Dialect names read with the shared PostgreSQL grammar. -pub(crate) const POSTGRES_DIALECTS: [&str; 2] = ["postgres", "postgresql"]; -/// The Snowflake dialect name. -pub(crate) const SNOWFLAKE_DIALECT: &str = "snowflake"; -/// The SQLite dialect name. -pub(crate) const SQLITE_DIALECT: &str = "sqlite"; /// Snowflake calls with a date-part argument, as (name, argument count, date-part index). pub(crate) const SNOWFLAKE_DATE_PART_ARGUMENTS: [(&str, usize, usize); 5] = [ ("DATEDIFF", 3, 0), diff --git a/crates/sqlbuild-rules/src/query_facts/main/facts_available.rs b/crates/sqlbuild-rules/src/query_facts/main/facts_available.rs index 3fc8193c5f..d4e29aac88 100644 --- a/crates/sqlbuild-rules/src/query_facts/main/facts_available.rs +++ b/crates/sqlbuild-rules/src/query_facts/main/facts_available.rs @@ -1,6 +1,8 @@ //! `facts_available`: whether SQLBuild parses a dialect's SQL for built-in rules. +use sqlbuild_analysis::sql_tree::main::shared_dialect::shared_dialect; + /// Whether SQLBuild parses `dialect`'s SQL for the built-in rules that read query facts. pub(crate) fn facts_available(dialect: &str) -> bool { - crate::query_facts::_helpers::facts::facts_dialect(dialect).is_some() + shared_dialect(dialect).is_some() } From fea889c3ff7bc26e1628c28254be69fb7b7cec6c Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 09:06:30 +0000 Subject: [PATCH 030/106] chore(deps): bump sqlquack to 19c51fa2 sqlquack main 19c51fa2 carries the Postgres parser work and the train that followed a9dde6a0. sqlquack-syntax-postgres now depends on stacker, which re-resolves a few Windows-only windows-sys edges in the lock. --- Cargo.lock | 13 +++++++------ Cargo.toml | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b09abffe20..4edc9a920b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4091,7 +4091,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4499,7 +4499,7 @@ dependencies = [ [[package]] name = "sqlquack-syntax" version = "0.2.0" -source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=a9dde6a00b144c6306425cfa84516ad816d8c49c#a9dde6a00b144c6306425cfa84516ad816d8c49c" +source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=19c51fa29ab01dbef9dddd18e8bfc406ed8e65f5#19c51fa29ab01dbef9dddd18e8bfc406ed8e65f5" dependencies = [ "serde", "serde_json", @@ -4512,7 +4512,7 @@ dependencies = [ [[package]] name = "sqlquack-syntax-derive" version = "0.2.0" -source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=a9dde6a00b144c6306425cfa84516ad816d8c49c#a9dde6a00b144c6306425cfa84516ad816d8c49c" +source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=19c51fa29ab01dbef9dddd18e8bfc406ed8e65f5#19c51fa29ab01dbef9dddd18e8bfc406ed8e65f5" dependencies = [ "proc-macro2", "quote", @@ -4522,9 +4522,10 @@ dependencies = [ [[package]] name = "sqlquack-syntax-postgres" version = "0.2.0" -source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=a9dde6a00b144c6306425cfa84516ad816d8c49c#a9dde6a00b144c6306425cfa84516ad816d8c49c" +source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=19c51fa29ab01dbef9dddd18e8bfc406ed8e65f5#19c51fa29ab01dbef9dddd18e8bfc406ed8e65f5" dependencies = [ "sqlquack-syntax", + "stacker", ] [[package]] @@ -4618,7 +4619,7 @@ dependencies = [ "cfg-if", "libc", "psm", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5054,7 +5055,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 65a30d1f3a..9e8e7cfd75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -177,8 +177,8 @@ sqlbuild-state = { path = "crates/sqlbuild-state" } # SQL lint Rules, quality Rules and the formatter. sqlclean = { git = "https://github.com/sqlstack-io/sqlclean.git", rev = "cf79656d247277a30bc81ef39b6dee420e721221" } # The lossless SQL tokenizer behind query fingerprints. -sqlquack-syntax = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "a9dde6a00b144c6306425cfa84516ad816d8c49c" } -sqlquack-syntax-postgres = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "a9dde6a00b144c6306425cfa84516ad816d8c49c" } +sqlquack-syntax = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "19c51fa29ab01dbef9dddd18e8bfc406ed8e65f5" } +sqlquack-syntax-postgres = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "19c51fa29ab01dbef9dddd18e8bfc406ed8e65f5" } stacker = "0.1" syn = { version = "2", features = ["full", "visit", "extra-traits"] } subtle = "2" From 183b210c838bd90e7bbfa0783ff2e5c88c88d909 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 09:17:32 +0000 Subject: [PATCH 031/106] build(deps): pin SQLQuack before its Snowflake grammar checks SQLQuack's next commit refuses a bare call in a Snowflake FROM clause, which rejects the __ref and __source placeholders that compiled model SQL keeps, so custom-rule syntax trees fail on Snowflake models. This rev keeps the BigQuery parser the warehouse type parsers need. --- Cargo.lock | 26 +++++++++++++------------- Cargo.toml | 6 +++--- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4d81a5009e..93e9016bfc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1319,7 +1319,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2174,7 +2174,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cde48f36530cbd91b65f050101e6c056d6eb743cea2a8f0f19beef69c6742ff8" dependencies = [ "scopeguard", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3199,7 +3199,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3637,7 +3637,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4091,7 +4091,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4500,7 +4500,7 @@ dependencies = [ [[package]] name = "sqlquack-syntax" version = "0.2.0" -source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=0ed31b9ce7c5644dde6dd8fd9b4a143458bb86c6#0ed31b9ce7c5644dde6dd8fd9b4a143458bb86c6" +source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=98eefae657b57566a091e39f7cde992bee45eb6f#98eefae657b57566a091e39f7cde992bee45eb6f" dependencies = [ "serde", "serde_json", @@ -4513,7 +4513,7 @@ dependencies = [ [[package]] name = "sqlquack-syntax-bigquery" version = "0.2.0" -source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=0ed31b9ce7c5644dde6dd8fd9b4a143458bb86c6#0ed31b9ce7c5644dde6dd8fd9b4a143458bb86c6" +source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=98eefae657b57566a091e39f7cde992bee45eb6f#98eefae657b57566a091e39f7cde992bee45eb6f" dependencies = [ "sqlquack-syntax", ] @@ -4521,7 +4521,7 @@ dependencies = [ [[package]] name = "sqlquack-syntax-derive" version = "0.2.0" -source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=0ed31b9ce7c5644dde6dd8fd9b4a143458bb86c6#0ed31b9ce7c5644dde6dd8fd9b4a143458bb86c6" +source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=98eefae657b57566a091e39f7cde992bee45eb6f#98eefae657b57566a091e39f7cde992bee45eb6f" dependencies = [ "proc-macro2", "quote", @@ -4531,7 +4531,7 @@ dependencies = [ [[package]] name = "sqlquack-syntax-postgres" version = "0.2.0" -source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=0ed31b9ce7c5644dde6dd8fd9b4a143458bb86c6#0ed31b9ce7c5644dde6dd8fd9b4a143458bb86c6" +source = "git+https://github.com/sqlstack-io/sqlquack.git?rev=98eefae657b57566a091e39f7cde992bee45eb6f#98eefae657b57566a091e39f7cde992bee45eb6f" dependencies = [ "sqlquack-syntax", ] @@ -4627,7 +4627,7 @@ dependencies = [ "cfg-if", "libc", "psm", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4749,7 +4749,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5063,7 +5063,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5396,7 +5396,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index cb12ecd72f..9a7f79dcfd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -177,9 +177,9 @@ sqlbuild-state = { path = "crates/sqlbuild-state" } # SQL lint Rules, quality Rules and the formatter. sqlclean = { git = "https://github.com/sqlstack-io/sqlclean.git", rev = "cf79656d247277a30bc81ef39b6dee420e721221" } # SQLQuack's lossless tokenizer behind query fingerprints and its parsers for warehouse types. -sqlquack-syntax = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "0ed31b9ce7c5644dde6dd8fd9b4a143458bb86c6" } -sqlquack-syntax-bigquery = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "0ed31b9ce7c5644dde6dd8fd9b4a143458bb86c6" } -sqlquack-syntax-postgres = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "0ed31b9ce7c5644dde6dd8fd9b4a143458bb86c6" } +sqlquack-syntax = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "98eefae657b57566a091e39f7cde992bee45eb6f" } +sqlquack-syntax-bigquery = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "98eefae657b57566a091e39f7cde992bee45eb6f" } +sqlquack-syntax-postgres = { git = "https://github.com/sqlstack-io/sqlquack.git", rev = "98eefae657b57566a091e39f7cde992bee45eb6f" } stacker = "0.1" syn = { version = "2", features = ["full", "visit", "extra-traits"] } subtle = "2" From 7d1ef9a8958badd04c437c0d656adf478d5f8052 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 09:32:05 +0000 Subject: [PATCH 032/106] fix(rules): read relation markers as tables in custom-rule syntax trees Compiled query_sql keeps relation markers such as __ref("orders"). Snowflake does not accept a bare function call in FROM, so newer SQLQuack parsers refuse every Snowflake model that references another. A shared helper in sqlbuild-analysis (relation_markers::markers_as_names) now writes each marker call in code as a short indexed name, padded so all later text keeps its line and column, and returns the calls by index. sql_tree reads SQL through it for every dialect. The rules crate's dialect lexical syntax moves to sqlbuild-sqltext so both use one mapping. --- crates/sqlbuild-analysis/src/lib.rs | 1 + .../src/relation_markers/_helpers/mod.rs | 1 + .../src/relation_markers/_helpers/pattern.rs | 15 ++++ .../src/relation_markers/constants.rs | 9 +++ .../relation_markers/main/markers_as_names.rs | 60 ++++++++++++++++ .../src/relation_markers/main/mod.rs | 1 + .../src/relation_markers/mod.rs | 9 +++ .../src/relation_markers/tests.rs | 4 ++ .../tests/test_markers_as_names.rs | 72 +++++++++++++++++++ .../src/relation_markers/tests/test_types.rs | 10 +++ .../src/relation_markers/types.rs | 4 ++ .../src/sql_tree/main/sql_tree_nodes.rs | 10 ++- .../src/sql_tree/tests/test_sql_tree.rs | 19 +++++ .../src/query_facts/_helpers/shape.rs | 43 +---------- .../sql_scan/main/dialect_lexical_syntax.rs | 43 +++++++++++ .../sqlbuild-sqltext/src/sql_scan/main/mod.rs | 1 + .../cli/commands/main/rules/_test_types.py | 2 + .../cli/commands/main/rules/helpers.py | 6 +- .../main/rules/test_rule_positions.py | 25 ++++++- 19 files changed, 287 insertions(+), 48 deletions(-) create mode 100644 crates/sqlbuild-analysis/src/relation_markers/_helpers/mod.rs create mode 100644 crates/sqlbuild-analysis/src/relation_markers/_helpers/pattern.rs create mode 100644 crates/sqlbuild-analysis/src/relation_markers/constants.rs create mode 100644 crates/sqlbuild-analysis/src/relation_markers/main/markers_as_names.rs create mode 100644 crates/sqlbuild-analysis/src/relation_markers/main/mod.rs create mode 100644 crates/sqlbuild-analysis/src/relation_markers/mod.rs create mode 100644 crates/sqlbuild-analysis/src/relation_markers/tests.rs create mode 100644 crates/sqlbuild-analysis/src/relation_markers/tests/test_markers_as_names.rs create mode 100644 crates/sqlbuild-analysis/src/relation_markers/tests/test_types.rs create mode 100644 crates/sqlbuild-analysis/src/relation_markers/types.rs create mode 100644 crates/sqlbuild-sqltext/src/sql_scan/main/dialect_lexical_syntax.rs diff --git a/crates/sqlbuild-analysis/src/lib.rs b/crates/sqlbuild-analysis/src/lib.rs index 5ad16bf8b6..e8d6a2920b 100644 --- a/crates/sqlbuild-analysis/src/lib.rs +++ b/crates/sqlbuild-analysis/src/lib.rs @@ -37,6 +37,7 @@ pub mod python_plan; pub mod python_selection; pub mod python_values; pub mod query_analysis; +pub mod relation_markers; pub mod relation_names; pub mod required_columns; pub mod resource_diagnostics; diff --git a/crates/sqlbuild-analysis/src/relation_markers/_helpers/mod.rs b/crates/sqlbuild-analysis/src/relation_markers/_helpers/mod.rs new file mode 100644 index 0000000000..b13857eb1c --- /dev/null +++ b/crates/sqlbuild-analysis/src/relation_markers/_helpers/mod.rs @@ -0,0 +1 @@ +pub(crate) mod pattern; diff --git a/crates/sqlbuild-analysis/src/relation_markers/_helpers/pattern.rs b/crates/sqlbuild-analysis/src/relation_markers/_helpers/pattern.rs new file mode 100644 index 0000000000..4fa72a78b6 --- /dev/null +++ b/crates/sqlbuild-analysis/src/relation_markers/_helpers/pattern.rs @@ -0,0 +1,15 @@ +//! The compiled relation marker call pattern. + +use std::sync::LazyLock; + +use regex::Regex; + +use crate::relation_markers::constants::RELATION_MARKER_PATTERN; + +static RELATION_MARKER: LazyLock> = + LazyLock::new(|| Regex::new(RELATION_MARKER_PATTERN).map_err(|error| error.to_string())); + +/// The compiled relation marker call pattern, or its compile error. +pub(crate) fn relation_marker_pattern() -> Result<&'static Regex, String> { + RELATION_MARKER.as_ref().map_err(Clone::clone) +} diff --git a/crates/sqlbuild-analysis/src/relation_markers/constants.rs b/crates/sqlbuild-analysis/src/relation_markers/constants.rs new file mode 100644 index 0000000000..4fef92b98e --- /dev/null +++ b/crates/sqlbuild-analysis/src/relation_markers/constants.rs @@ -0,0 +1,9 @@ +//! The relation marker call pattern and the names marker calls stand as. + +/// The prefix of the name a marker call stands as while parsed; the call's index follows it. +pub const MARKER_NAME_PREFIX: &str = "__sqb_"; +/// Any relation marker call: `__name("x")`, or `__name("a", "b")` as `__dbt_ref` writes it. +pub(crate) const RELATION_MARKER_PATTERN: &str = + r#"(?i)\b(__[a-z_]+)\(\s*"([^"]+)"\s*(?:,\s*"([^"]+)"\s*)?\)"#; +/// The marker whose two names read as one, joined by `__`. +pub(crate) const DBT_REF_FUNCTION: &str = "__dbt_ref"; diff --git a/crates/sqlbuild-analysis/src/relation_markers/main/markers_as_names.rs b/crates/sqlbuild-analysis/src/relation_markers/main/markers_as_names.rs new file mode 100644 index 0000000000..9330c94939 --- /dev/null +++ b/crates/sqlbuild-analysis/src/relation_markers/main/markers_as_names.rs @@ -0,0 +1,60 @@ +//! `markers_as_names`: relation marker calls in code as plain names that keep every position. + +use regex::Regex; +use sqlbuild_sqltext::sql_scan::main::dialect_non_code_ranges::dialect_non_code_ranges; +use sqlbuild_sqltext::sql_scan::models::LexicalSyntax; + +use crate::relation_markers::_helpers::pattern::relation_marker_pattern; +use crate::relation_markers::constants::{DBT_REF_FUNCTION, MARKER_NAME_PREFIX}; +use crate::relation_markers::types::MarkerCall; + +/// `sql` with each marker call in code as `__sqb_`, padded so later text keeps its line and column, and the calls by index. +pub fn markers_as_names( + sql: &str, + syntax: &LexicalSyntax, +) -> Result<(String, Vec), String> { + let pattern: &Regex = relation_marker_pattern()?; + let mut protected: Option> = None; + let mut named: String = String::with_capacity(sql.len()); + let mut calls: Vec = Vec::new(); + let mut cursor: usize = 0; + for captures in pattern.captures_iter(sql) { + let (Some(full), Some(function), Some(first)) = + (captures.get(0), captures.get(1), captures.get(2)) + else { + continue; + }; + let ranges = protected.get_or_insert_with(|| dialect_non_code_ranges(sql, syntax)); + let following: usize = ranges.partition_point(|(start, _)| *start <= full.start()); + if following > 0 && full.start() < ranges[following - 1].1 { + continue; + } + let function_name: String = function.as_str().to_ascii_lowercase(); + let referenced_name: String = match (captures.get(3), function_name == DBT_REF_FUNCTION) { + (None, _) => first.as_str().to_owned(), + (Some(second), true) => format!("{}__{}", first.as_str(), second.as_str()), + (Some(_), false) => continue, + }; + named.push_str(&sql[cursor..full.start()]); + named.push_str(&padded_name( + &format!("{MARKER_NAME_PREFIX}{}", calls.len()), + full.as_str(), + )); + cursor = full.end(); + calls.push((function_name, referenced_name)); + } + named.push_str(&sql[cursor..]); + Ok((named, calls)) +} + +/// `name`, then the call's line breaks, then spaces as wide as the call's last line. +fn padded_name(name: &str, call: &str) -> String { + let breaks: usize = call.matches('\n').count(); + let last_line: &str = call.rsplit('\n').next().unwrap_or(call); + let width: usize = if breaks == 0 { + call.chars().count().saturating_sub(name.chars().count()) + } else { + last_line.chars().count() + }; + format!("{name}{}{}", "\n".repeat(breaks), " ".repeat(width)) +} diff --git a/crates/sqlbuild-analysis/src/relation_markers/main/mod.rs b/crates/sqlbuild-analysis/src/relation_markers/main/mod.rs new file mode 100644 index 0000000000..32d7644fcb --- /dev/null +++ b/crates/sqlbuild-analysis/src/relation_markers/main/mod.rs @@ -0,0 +1 @@ +pub mod markers_as_names; diff --git a/crates/sqlbuild-analysis/src/relation_markers/mod.rs b/crates/sqlbuild-analysis/src/relation_markers/mod.rs new file mode 100644 index 0000000000..af27b6661d --- /dev/null +++ b/crates/sqlbuild-analysis/src/relation_markers/mod.rs @@ -0,0 +1,9 @@ +//! Relation marker calls, such as `__ref("orders")`, read as plain relation names by strict parsers. + +mod _helpers; +pub mod constants; +pub mod main; +pub mod types; + +#[cfg(test)] +mod tests; diff --git a/crates/sqlbuild-analysis/src/relation_markers/tests.rs b/crates/sqlbuild-analysis/src/relation_markers/tests.rs new file mode 100644 index 0000000000..c7c4334980 --- /dev/null +++ b/crates/sqlbuild-analysis/src/relation_markers/tests.rs @@ -0,0 +1,4 @@ +#[path = "tests/test_markers_as_names.rs"] +mod test_markers_as_names; +#[path = "tests/test_types.rs"] +mod test_types; diff --git a/crates/sqlbuild-analysis/src/relation_markers/tests/test_markers_as_names.rs b/crates/sqlbuild-analysis/src/relation_markers/tests/test_markers_as_names.rs new file mode 100644 index 0000000000..6df1acbc7a --- /dev/null +++ b/crates/sqlbuild-analysis/src/relation_markers/tests/test_markers_as_names.rs @@ -0,0 +1,72 @@ +use sqlbuild_sqltext::sql_scan::main::dialect_lexical_syntax::dialect_lexical_syntax; + +use crate::relation_markers::main::markers_as_names::markers_as_names; +use crate::relation_markers::tests::test_types::MarkersAsNamesTestCase; + +#[test] +fn given_marker_calls_when_reading_them_as_names_then_code_calls_become_names_and_positions_hold() { + let test_cases = [ + MarkersAsNamesTestCase { + description: "each call becomes its indexed name, padded to the call's width", + dialect: "snowflake", + sql: r#"SELECT * FROM __ref("orders") o JOIN __source("raw") r ON TRUE"#, + expected_sql: format!( + "SELECT * FROM __sqb_0{} o JOIN __sqb_1{} r ON TRUE", + " ".repeat(8), + " ".repeat(8) + ), + expected_calls: &[("__ref", "orders"), ("__source", "raw")], + }, + MarkersAsNamesTestCase { + description: "calls in quoted text and comments stay; an upper-case call folds", + dialect: "snowflake", + sql: "SELECT '__ref(\"a\")' AS q -- __ref(\"b\")\nFROM __REF(\"Orders\") // __ref(\"c\")", + expected_sql: format!( + "SELECT '__ref(\"a\")' AS q -- __ref(\"b\")\nFROM __sqb_0{} // __ref(\"c\")", + " ".repeat(8) + ), + expected_calls: &[("__ref", "Orders")], + }, + MarkersAsNamesTestCase { + description: "a call over three lines keeps its line breaks and its last line's width", + dialect: "duckdb", + sql: "FROM __ref(\n \"orders\"\n) AS o", + expected_sql: "FROM __sqb_0\n\n AS o".to_owned(), + expected_calls: &[("__ref", "orders")], + }, + MarkersAsNamesTestCase { + description: "two dbt names join; a non-ASCII name pads by characters, not bytes", + dialect: "postgres", + sql: r#"FROM __dbt_ref("shop", "customers") d JOIN __ref("café") c ON TRUE"#, + expected_sql: format!( + "FROM __sqb_0{} d JOIN __sqb_1{} c ON TRUE", + " ".repeat(23), + " ".repeat(6) + ), + expected_calls: &[("__dbt_ref", "shop__customers"), ("__ref", "café")], + }, + MarkersAsNamesTestCase { + description: "calls with a second name, or a non-literal argument, stay calls", + dialect: "duckdb", + sql: r#"FROM __ref("a", "b") JOIN __table_fn("orders", 2) ON TRUE"#, + expected_sql: r#"FROM __ref("a", "b") JOIN __table_fn("orders", 2) ON TRUE"#.to_owned(), + expected_calls: &[], + }, + ]; + for test_case in test_cases { + let (named, calls) = + markers_as_names(test_case.sql, &dialect_lexical_syntax(test_case.dialect)) + .expect(test_case.description); + let expected_calls: Vec<(String, String)> = test_case + .expected_calls + .iter() + .map(|(function, name)| ((*function).to_owned(), (*name).to_owned())) + .collect(); + assert_eq!( + (named, calls), + (test_case.expected_sql, expected_calls), + "{}", + test_case.description + ); + } +} diff --git a/crates/sqlbuild-analysis/src/relation_markers/tests/test_types.rs b/crates/sqlbuild-analysis/src/relation_markers/tests/test_types.rs new file mode 100644 index 0000000000..b92a6d74fa --- /dev/null +++ b/crates/sqlbuild-analysis/src/relation_markers/tests/test_types.rs @@ -0,0 +1,10 @@ +//! Test case types for relation markers read as names. + +/// SQL in one dialect, the SQL its markers read as, and the calls they stand for. +pub(crate) struct MarkersAsNamesTestCase { + pub(crate) description: &'static str, + pub(crate) dialect: &'static str, + pub(crate) sql: &'static str, + pub(crate) expected_sql: String, + pub(crate) expected_calls: &'static [(&'static str, &'static str)], +} diff --git a/crates/sqlbuild-analysis/src/relation_markers/types.rs b/crates/sqlbuild-analysis/src/relation_markers/types.rs new file mode 100644 index 0000000000..9b90a22d2a --- /dev/null +++ b/crates/sqlbuild-analysis/src/relation_markers/types.rs @@ -0,0 +1,4 @@ +//! Shapes of relation marker calls. + +/// A marker call's lower-case function, such as `__ref`, and the name it references. +pub type MarkerCall = (String, String); diff --git a/crates/sqlbuild-analysis/src/sql_tree/main/sql_tree_nodes.rs b/crates/sqlbuild-analysis/src/sql_tree/main/sql_tree_nodes.rs index de5e7a8b94..daad7f356f 100644 --- a/crates/sqlbuild-analysis/src/sql_tree/main/sql_tree_nodes.rs +++ b/crates/sqlbuild-analysis/src/sql_tree/main/sql_tree_nodes.rs @@ -1,7 +1,9 @@ //! `sql_tree_nodes`: a rule's SQL syntax tree as nodes in pre-order. +use sqlbuild_sqltext::sql_scan::main::dialect_lexical_syntax::dialect_lexical_syntax; use sqlquack_syntax::Expression; +use crate::relation_markers::main::markers_as_names::markers_as_names; use crate::sql_tree::_helpers::parse::parse; use crate::sql_tree::_helpers::positions::line_starts; use crate::sql_tree::_helpers::rules_tree_parser::rules_tree_parser; @@ -11,7 +13,7 @@ use crate::sql_tree::errors::SqlTreeError; use crate::sql_tree::models::SqlTreeNode; use crate::worker_stack::main::run_on_worker_stack::run_on_worker_stack; -/// Every node of `sql` parsed in `dialect`, in pre-order, joins and CTEs owning their expressions. +/// Every node of `sql` parsed in `dialect`, in pre-order, joins and CTEs owning their expressions; relation markers read as tables. pub fn sql_tree_nodes(sql: &str, dialect: &str) -> Result, SqlTreeError> { run_on_worker_stack(TREE_WORKER_STACK_BYTES, "the parser panicked", || { tree_nodes(sql, dialect) @@ -22,6 +24,8 @@ pub fn sql_tree_nodes(sql: &str, dialect: &str) -> Result, SqlT fn tree_nodes(sql: &str, dialect: &str) -> Result, SqlTreeError> { let parser = rules_tree_parser(dialect) .ok_or_else(|| SqlTreeError::UnavailableDialect(dialect.to_owned()))?; - let statements: Vec = parse(sql, parser)?; - walk(&statements, &line_starts(sql)) + let (named, _) = markers_as_names(sql, &dialect_lexical_syntax(dialect)) + .map_err(SqlTreeError::Unanalysable)?; + let statements: Vec = parse(&named, parser)?; + walk(&statements, &line_starts(&named)) } diff --git a/crates/sqlbuild-analysis/src/sql_tree/tests/test_sql_tree.rs b/crates/sqlbuild-analysis/src/sql_tree/tests/test_sql_tree.rs index e1715b2eee..5a0629d703 100644 --- a/crates/sqlbuild-analysis/src/sql_tree/tests/test_sql_tree.rs +++ b/crates/sqlbuild-analysis/src/sql_tree/tests/test_sql_tree.rs @@ -6,6 +6,8 @@ use crate::sql_tree::tests::test_types::{TreeFailureTestCase, TreeShapeTestCase} const CTE_AND_JOIN_SQL: &str = "WITH x AS (SELECT 1 AS id)\nSELECT *\n FROM x JOIN (SELECT 2 AS id) s ON x.id = s.id"; +const SNOWFLAKE_MARKER_SQL: &str = "WITH o AS (SELECT * FROM __ref(\"orders\"))\nSELECT o.*, c.name\nFROM o JOIN __ref(\"customers\") c ON o.id = c.id JOIN (SELECT 2 AS id) s ON s.id = o.id\nWHERE o.id IN (SELECT id FROM __source(\"raw_returns\"))"; + #[test] fn given_sql_when_building_a_rule_tree_then_clauses_own_their_queries_at_their_source_positions() { let test_cases = [ @@ -48,6 +50,23 @@ fn given_sql_when_building_a_rule_tree_then_clauses_own_their_queries_at_their_s ("subquery", 2, 3, Some("alias")), ], }, + TreeShapeTestCase { + description: "Snowflake reads relation markers as tables; nodes after them keep their columns", + dialect: "snowflake", + sql: SNOWFLAKE_MARKER_SQL, + expected_nodes: vec![ + ("select", 1, 1, None), + ("cte", 1, 6, Some("select")), + ("select", 1, 12, Some("cte")), + ("star", 1, 12, Some("select")), + ("star", 2, 8, Some("select")), + ("join", 3, 8, Some("select")), + ("join", 3, 49, Some("select")), + ("select", 3, 54, Some("subquery")), + ("subquery", 3, 54, Some("join")), + ("select", 4, 7, Some("in")), + ], + }, TreeShapeTestCase { description: "a comma-separated FROM item after an explicit join is not a join", dialect: "duckdb", diff --git a/crates/sqlbuild-rules/src/query_facts/_helpers/shape.rs b/crates/sqlbuild-rules/src/query_facts/_helpers/shape.rs index 0f2b3ea81d..f9fae3f4b4 100644 --- a/crates/sqlbuild-rules/src/query_facts/_helpers/shape.rs +++ b/crates/sqlbuild-rules/src/query_facts/_helpers/shape.rs @@ -5,6 +5,7 @@ use crate::query_facts::constants::{ use crate::query_facts::models::{CallKind, ModelCall, ModelShape}; use crate::rules::main::compact_sql::compact_sql; use sqlbuild_analysis::semantic_validation::main::normalize_mapped::normalize_mapped; +use sqlbuild_sqltext::sql_scan::main::dialect_lexical_syntax::dialect_lexical_syntax; use sqlbuild_sqltext::sql_scan::main::dialect_matching_paren::dialect_matching_paren; use sqlbuild_sqltext::sql_scan::main::dialect_non_code_end::dialect_non_code_end; use sqlbuild_sqltext::sql_scan::main::quote_policy::quote_policy; @@ -15,7 +16,7 @@ pub(crate) fn model_shape(sql: &str, dialect: &str) -> Result = normalized @@ -77,46 +78,6 @@ fn is_word_byte(byte: u8) -> bool { byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'$' } -/// The comment and quoted-text syntax the rules parser tokenized for `dialect`. -fn lexical_syntax(dialect: &str) -> LexicalSyntax { - let strings = |quotes: &[&str]| quotes.iter().map(|quote| (*quote).to_owned()).collect(); - let lines = |prefixes: &[&str]| prefixes.iter().map(|prefix| (*prefix).to_owned()).collect(); - match dialect.to_ascii_lowercase().as_str() { - "bigquery" => LexicalSyntax { - backslash_escape_quotes: strings(&["'", "\""]), - raw_string_prefix: true, - triple_quoted_strings: true, - line_comment_prefixes: lines(&["--", "#"]), - ..LexicalSyntax::default() - }, - "snowflake" => LexicalSyntax { - backslash_escape_quotes: strings(&["'"]), - line_comment_prefixes: lines(&["--", "//"]), - ..LexicalSyntax::default() - }, - "clickhouse" => LexicalSyntax { - backslash_escape_quotes: strings(&["'"]), - line_comment_prefixes: lines(&["--", "#", "//"]), - ..LexicalSyntax::default() - }, - "databricks" | "duckdb" => LexicalSyntax { - line_comment_prefixes: lines(&["--"]), - ..LexicalSyntax::default() - }, - "mssql" | "sqlserver" | "tsql" => LexicalSyntax { - nested_block_comments: true, - line_comment_prefixes: lines(&["--"]), - ..LexicalSyntax::default() - }, - _ => LexicalSyntax { - escape_string_prefix: true, - nested_block_comments: true, - line_comment_prefixes: lines(&["--"]), - ..LexicalSyntax::default() - }, - } -} - fn is_quote_start(byte: u8) -> bool { matches!(byte, b'\'' | b'"' | b'`' | b'$') } diff --git a/crates/sqlbuild-sqltext/src/sql_scan/main/dialect_lexical_syntax.rs b/crates/sqlbuild-sqltext/src/sql_scan/main/dialect_lexical_syntax.rs new file mode 100644 index 0000000000..ff94e94b32 --- /dev/null +++ b/crates/sqlbuild-sqltext/src/sql_scan/main/dialect_lexical_syntax.rs @@ -0,0 +1,43 @@ +//! `dialect_lexical_syntax`: the comment and quoted-text syntax of a dialect's SQL, by dialect name. + +use crate::sql_scan::models::LexicalSyntax; + +/// The comment and quoted-text syntax that SQLQuack tokenizes for `dialect`. +pub fn dialect_lexical_syntax(dialect: &str) -> LexicalSyntax { + let strings = |quotes: &[&str]| quotes.iter().map(|quote| (*quote).to_owned()).collect(); + let lines = |prefixes: &[&str]| prefixes.iter().map(|prefix| (*prefix).to_owned()).collect(); + match dialect.to_ascii_lowercase().as_str() { + "bigquery" => LexicalSyntax { + backslash_escape_quotes: strings(&["'", "\""]), + raw_string_prefix: true, + triple_quoted_strings: true, + line_comment_prefixes: lines(&["--", "#"]), + ..LexicalSyntax::default() + }, + "snowflake" => LexicalSyntax { + backslash_escape_quotes: strings(&["'"]), + line_comment_prefixes: lines(&["--", "//"]), + ..LexicalSyntax::default() + }, + "clickhouse" => LexicalSyntax { + backslash_escape_quotes: strings(&["'"]), + line_comment_prefixes: lines(&["--", "#", "//"]), + ..LexicalSyntax::default() + }, + "databricks" | "duckdb" => LexicalSyntax { + line_comment_prefixes: lines(&["--"]), + ..LexicalSyntax::default() + }, + "mssql" | "sqlserver" | "tsql" => LexicalSyntax { + nested_block_comments: true, + line_comment_prefixes: lines(&["--"]), + ..LexicalSyntax::default() + }, + _ => LexicalSyntax { + escape_string_prefix: true, + nested_block_comments: true, + line_comment_prefixes: lines(&["--"]), + ..LexicalSyntax::default() + }, + } +} diff --git a/crates/sqlbuild-sqltext/src/sql_scan/main/mod.rs b/crates/sqlbuild-sqltext/src/sql_scan/main/mod.rs index 4735e4b172..3cc4afefde 100644 --- a/crates/sqlbuild-sqltext/src/sql_scan/main/mod.rs +++ b/crates/sqlbuild-sqltext/src/sql_scan/main/mod.rs @@ -1,4 +1,5 @@ pub mod comment_end; +pub mod dialect_lexical_syntax; pub mod dialect_matching_paren; pub mod dialect_non_code_end; pub mod dialect_non_code_ranges; diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/rules/_test_types.py b/tests/e2e/src/sqlbuild/cli/commands/main/rules/_test_types.py index f4859f7693..2694060a82 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/rules/_test_types.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/rules/_test_types.py @@ -384,3 +384,5 @@ class SyntaxTreeRuleE2ETestCase: description: str model_sql: str expected_messages: tuple[str, ...] + adapter: str = "duckdb" + other_models: dict[str, str] = field(default_factory=dict) diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/rules/helpers.py b/tests/e2e/src/sqlbuild/cli/commands/main/rules/helpers.py index 42c1bcbcbc..7dc6388739 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/rules/helpers.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/rules/helpers.py @@ -73,11 +73,13 @@ def write_custom_rule_project( selected_rules: tuple[str, ...], files: tuple[tuple[str, str], ...], configuration: str = "", + adapter: str = "duckdb", ) -> None: - """Write a DuckDB project whose selected custom Rules live in the given files.""" + """Write a project, DuckDB by default, whose selected custom Rules live in the given files.""" (project_dir / "sqlbuild_project.toml").write_text( - f'name = "orders"\nadapter = "duckdb"\n\n[rules]\nselect = {json.dumps(selected_rules)}\n' + f'name = "orders"\nadapter = "{adapter}"\n\n' + f"[rules]\nselect = {json.dumps(selected_rules)}\n" "\n[rules.thresholds]\nmin_custom_rule_test_cases = 0\n" + configuration, encoding="utf-8", ) diff --git a/tests/e2e/src/sqlbuild/cli/commands/main/rules/test_rule_positions.py b/tests/e2e/src/sqlbuild/cli/commands/main/rules/test_rule_positions.py index 62f0510870..788b5698e2 100644 --- a/tests/e2e/src/sqlbuild/cli/commands/main/rules/test_rule_positions.py +++ b/tests/e2e/src/sqlbuild/cli/commands/main/rules/test_rule_positions.py @@ -140,7 +140,24 @@ def test_given_model_query_layout_when_running_rules_then_findings_use_file_posi "Star/3:o.*,", "Subquery/3:(SEL", ), - ) + ), + SyntaxTreeRuleE2ETestCase( + description="Snowflake reads relation markers as tables at their source positions", + adapter="snowflake", + model_sql=( + 'MODEL (description "Orders", database analytics, schema analytics);\n' + 'WITH c AS (SELECT * FROM __ref("customers"))\n' + 'SELECT o.*, c.name FROM __ref("raw_orders") o JOIN c ON o.id = c.id\n' + ), + other_models={ + name: ( + f'MODEL (description "{name}", database analytics, schema analytics);\n' + "SELECT 1 AS id, 'a' AS name\n" + ) + for name in ("customers", "raw_orders") + }, + expected_messages=("CTE/2:c AS", "Join/2:JOIN", "Star/2:SELE", "Star/2:o.*,"), + ), ], ids=lambda case: case.description, ) @@ -150,7 +167,11 @@ def test_given_custom_rule_over_the_syntax_tree_when_compiling_then_nodes_point_ write_custom_rule_project( project_dir=tmp_path, selected_rules=("XSQBRTREE001",), - files=(("rules/tree.py", _TREE_RULE),), + files=( + ("rules/tree.py", _TREE_RULE), + *((f"models/{name}.sql", sql) for name, sql in test_case.other_models.items()), + ), + adapter=test_case.adapter, ) (tmp_path / "models" / "orders.sql").write_text(test_case.model_sql, encoding="utf-8") From a1e6b0f36711c79e12ee5d5756a7c2973d8d7469 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 10:17:13 +0000 Subject: [PATCH 033/106] build(deps): keep Windows windows-sys edges at main's resolution Adding stacker through the SQLQuack crates re-resolved errno, rustix, tempfile, junction, quinn-udp and winapi-util to older windows-sys releases. On Windows the extension build then compiled windows-sys 0.52 with features the sqb build lacks. The lockfile keeps main's edges, so both builds share one windows-sys again. --- Cargo.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 52e1132cb2..5957511d7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1319,7 +1319,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2174,7 +2174,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cde48f36530cbd91b65f050101e6c056d6eb743cea2a8f0f19beef69c6742ff8" dependencies = [ "scopeguard", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3199,7 +3199,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3637,7 +3637,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4752,7 +4752,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5399,7 +5399,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] From 9d016217569ea53c726003a333c005fc6ca00078 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 10:36:43 +0000 Subject: [PATCH 034/106] docs(types): keep the ClickHouse type parameter doc comment to one line --- .../sqlbuild-analysis/src/type_system/_helpers/type_reader.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/sqlbuild-analysis/src/type_system/_helpers/type_reader.rs b/crates/sqlbuild-analysis/src/type_system/_helpers/type_reader.rs index d98af083c5..b95d3108bf 100644 --- a/crates/sqlbuild-analysis/src/type_system/_helpers/type_reader.rs +++ b/crates/sqlbuild-analysis/src/type_system/_helpers/type_reader.rs @@ -414,8 +414,7 @@ impl Reader<'_> { Ok(json!({"data_type": "custom", "name": format!("{name}({})", parameters.join(", "))})) } - /// One ClickHouse type parameter as written: a nested type, as in `Nullable(DateTime64(3))`, - /// a named tuple field, or an enum member and its value. + /// One ClickHouse type parameter as written: a nested type, a named tuple field or an enum member. fn clickhouse_parameter(&mut self, name: &str) -> Result { let mut text: String = String::new(); let mut depth: usize = 0; From 1e4aa20ade249d29e443e0f6fb8da13d27ef4e96 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 10:46:22 +0000 Subject: [PATCH 035/106] feat(ui): wire the lineage and changes pages to the live control API Add a person read of a target's published snapshots with the subjects each one changed, behind the same listing authorisation as jobs. The Changes page reads it, and the Lineage page builds its graph and inspector from the catalog listing, so neither page needs mock data. --- clients/typescript/src/schema.d.ts | 79 +++++++++++++++++ .../authentication/password_sessions.rs | 4 + .../api/_helpers/resources/person_reads.rs | 27 +++++- .../sqlbuild-orchestration/src/api/models.rs | 1 + .../snapshots/main/list_person_snapshots.rs | 84 +++++++++++++++++++ .../src/snapshots/main/mod.rs | 1 + .../src/snapshots/models.rs | 23 +++++ .../tests/api/test_person_sessions.rs | 1 + .../tests/api/test_reads.rs | 51 ++++++++++- fensu.toml | 1 - ui/src/lib/api/http-changes.test.ts | 61 ++++++++++++++ ui/src/lib/api/http-changes.ts | 65 ++++++++++++++ ui/src/lib/api/http-client.ts | 16 +++- ui/src/lib/api/types/build.ts | 8 +- ui/src/lib/nav.test.ts | 5 +- ui/src/lib/nav.ts | 2 +- ui/src/routes/changes/+page.svelte | 13 +-- ui/src/routes/changes/utils.ts | 10 ++- ui/src/routes/lineage/+page.svelte | 21 ++++- 19 files changed, 451 insertions(+), 22 deletions(-) create mode 100644 crates/sqlbuild-orchestration/src/snapshots/main/list_person_snapshots.rs create mode 100644 ui/src/lib/api/http-changes.test.ts create mode 100644 ui/src/lib/api/http-changes.ts diff --git a/clients/typescript/src/schema.d.ts b/clients/typescript/src/schema.d.ts index 117fb8c855..35c7f687f4 100644 --- a/clients/typescript/src/schema.d.ts +++ b/clients/typescript/src/schema.d.ts @@ -482,6 +482,22 @@ export interface paths { patch?: never; trace?: never; }; + "/api/control/locations/{location_id}/targets/{target}/snapshots": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["listPersonSnapshots"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/control/locations/{location_id}/targets/{target}/statistics": { parameters: { query?: never; @@ -1488,6 +1504,28 @@ export interface components { }; /** @enum {string} */ PlatformEventKind: "heartbeat" | "scheduler_no_leader" | "tick_overdue" | "calendar_horizon_low" | "location_broken" | "launch_failed"; + /** @description A changed subject, such as `model:orders` or `role:analyst`, without its content hashes. */ + PublishedChange: { + change_kind: string; + subject_key: string; + }; + /** @description One publication as people browse it: its commit, environment and changed definitions. */ + PublishedSnapshot: { + /** @description Added, changed and removed nodes and roles, in publication order. */ + changes: components["schemas"]["PublishedChange"][]; + /** @description Digest of the lock file, Python and `sqb` versions; a change means dependencies changed. */ + environment_hash: string; + /** Format: int64 */ + generation: number; + git_author?: string | null; + git_dirty: boolean; + git_revision?: string | null; + /** Format: date-time */ + published_at: string; + /** Format: uuid */ + snapshot_id: string; + sqb_version: string; + }; /** @description One invocation's records; the server owns its claim namespace, start time and version. */ RecallBatch: { invocation_id: string; @@ -3286,6 +3324,47 @@ export interface operations { }; }; }; + listPersonSnapshots: { + parameters: { + query?: { + /** @description Maximum recent publications; defaults to 20, capped at 100 */ + limit?: number; + }; + header?: never; + path: { + location_id: string; + target: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Publications newest first, each with its changed nodes and roles */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PublishedSnapshot"][]; + }; + }; + /** @description Session rejected */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + }; + }; readPersonStatistics: { parameters: { query: { diff --git a/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs b/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs index f40bb3d87d..5bce2d5b43 100644 --- a/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs +++ b/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs @@ -117,6 +117,10 @@ pub(crate) fn person_routes() -> Router { "/api/control/locations/{location_id}/targets/{target}/triggers/{trigger_id}/ticks", get(crate::api::_helpers::resources::person_reads::ticks), ) + .route( + "/api/control/locations/{location_id}/targets/{target}/snapshots", + get(crate::api::_helpers::resources::person_reads::snapshots), + ) .route( "/api/control/locations/{location_id}/targets/{target}/catalog", get(crate::api::_helpers::resources::person_operations::catalog), diff --git a/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_reads.rs b/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_reads.rs index 79fbb670ad..aa10e0c867 100644 --- a/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_reads.rs +++ b/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_reads.rs @@ -1,4 +1,4 @@ -//! Person reads of run nodes, catalog nodes, jobs, triggers and ticks; refusal precedes every lookup, so unknown objects look forbidden. +//! Person reads of run nodes, catalog nodes, jobs, triggers, ticks and publications; refusal precedes every lookup, so unknown objects look forbidden. use axum::extract::{Path, Query, State}; use axum::http::StatusCode; @@ -15,6 +15,8 @@ use crate::runs::main::list_person_run_nodes::list_person_run_nodes; use crate::runs::models::RunNodeRecord; use crate::scheduler::main::list_person_ticks::list_person_ticks; use crate::scheduler::models::TickRecord; +use crate::snapshots::main::list_person_snapshots::list_person_snapshots; +use crate::snapshots::models::PublishedSnapshot; use crate::store::models::ControlStore; use crate::warehouse_metadata::main::list_person_catalog_nodes::list_person_catalog_nodes; use crate::warehouse_metadata::models::CatalogNodeRecord; @@ -136,6 +138,29 @@ pub(crate) async fn ticks( .ok_or_else(|| not_found("trigger not found")) } +#[utoipa::path(get, path = "/api/control/locations/{location_id}/targets/{target}/snapshots", operation_id = "listPersonSnapshots", + params(("location_id" = Uuid, Path), ("target" = String, Path), + ("limit" = Option, Query, description = "Maximum recent publications; defaults to 20, capped at 100")), + responses((status = 200, body = Vec, description = "Publications newest first, each with its changed nodes and roles"), + (status = 401, description = "Session rejected"), + (status = 403, body = crate::api::models::ApiErrorBody)), security(("session" = []), ("cliSession" = [])))] +pub(crate) async fn snapshots( + (State(store), Extension(context)): (State, Extension), + Extension(default_role): Extension, + (Path((location_id, target)), Query(options)): (Path<(Uuid, String)>, Query), + Extension(AuthenticatedPerson(principal, _)): Extension, +) -> Result>, ApiError> { + Ok(Json( + list_person_snapshots( + &store, + (&context, principal), + ((location_id, &target), options.limit.unwrap_or(20)), + default_role, + ) + .await?, + )) +} + fn not_found(message: &str) -> ApiError { ApiError { status: StatusCode::NOT_FOUND, diff --git a/crates/sqlbuild-orchestration/src/api/models.rs b/crates/sqlbuild-orchestration/src/api/models.rs index 530893c084..9571345a46 100644 --- a/crates/sqlbuild-orchestration/src/api/models.rs +++ b/crates/sqlbuild-orchestration/src/api/models.rs @@ -25,6 +25,7 @@ use crate::api::_helpers::resources::{ person_reads::jobs, person_reads::triggers, person_reads::ticks, + person_reads::snapshots, person_operations::catalog, person_operations::statistics, person_operations::fingerprint, diff --git a/crates/sqlbuild-orchestration/src/snapshots/main/list_person_snapshots.rs b/crates/sqlbuild-orchestration/src/snapshots/main/list_person_snapshots.rs new file mode 100644 index 0000000000..eaf79c0e86 --- /dev/null +++ b/crates/sqlbuild-orchestration/src/snapshots/main/list_person_snapshots.rs @@ -0,0 +1,84 @@ +//! A person browses a target's publications only with view on every current node, like its jobs. + +use std::collections::BTreeMap; + +use chrono::{DateTime, Utc}; +use uuid::Uuid; + +use crate::authorization::main::authorize_listing::authorize_listing; +use crate::credentials::models::{AuditContext, DefaultRole}; +use crate::snapshots::models::{PublishedChange, PublishedSnapshot}; +use crate::store::errors::ControlError; +use crate::store::main::{begin::begin, commit::commit, decode_row::decode_row, query::query}; +use crate::store::models::{ControlStore, Parameter, TransactionMode}; +use crate::store::types::Row; + +/// Newest first, at most `limit` (capped at 100) publications with their changes. +pub async fn list_person_snapshots( + store: &ControlStore, + (context, credentials): (&AuditContext, Uuid), + ((location_id, target), limit): ((Uuid, &str), u32), + default_role: DefaultRole, +) -> Result, ControlError> { + let (transaction, _) = authorize_listing( + begin(store, TransactionMode::Write).await?, + (context, credentials), + (location_id, target), + default_role, + ) + .await?; + let scope = vec![ + Parameter::Id(location_id), + Parameter::Text(target.into()), + Parameter::Integer(i64::from(limit.min(100))), + ]; + let (transaction, published) = query(transaction, + "SELECT snapshot_id, generation, git_revision, CASE WHEN git_dirty THEN 1 ELSE 0 END AS git_dirty, git_author, environment_hash, sqb_version, published_at \ + FROM sqb_snapshots WHERE location_id=$1 AND target=$2 ORDER BY generation DESC LIMIT $3", + scope.clone()).await?; + let (transaction, changed) = query(transaction, + "SELECT c.snapshot_id, c.change_kind, c.subject_key FROM sqb_snapshot_changes c \ + WHERE c.snapshot_id IN (SELECT s.snapshot_id FROM sqb_snapshots s WHERE s.location_id=$1 AND s.target=$2 \ + ORDER BY s.generation DESC LIMIT $3) ORDER BY c.snapshot_id, c.change_seq", + scope).await?; + commit(transaction).await?; + let mut changes: BTreeMap> = BTreeMap::new(); + for row in changed { + let snapshot_id = id_of(&row)?; + changes + .entry(snapshot_id) + .or_default() + .push(decode_row(row)?); + } + published + .into_iter() + .map(|row| { + let mut snapshot = snapshot_of(row)?; + snapshot.changes = changes.remove(&snapshot.snapshot_id).unwrap_or_default(); + Ok(snapshot) + }) + .collect() +} + +fn snapshot_of(row: Row) -> Result { + let snapshot_id = id_of(&row)?; + let text = |key: &str| row[key].as_str().map(String::from); + let published_at: DateTime = serde_json::from_value(row["published_at"].clone()) + .map_err(|error| ControlError::Invalid(format!("stored publication time: {error}")))?; + Ok(PublishedSnapshot { + snapshot_id, + generation: row["generation"].as_i64().unwrap_or_default(), + git_revision: text("git_revision"), + git_dirty: row["git_dirty"].as_i64() == Some(1), + git_author: text("git_author"), + environment_hash: text("environment_hash").unwrap_or_default(), + sqb_version: text("sqb_version").unwrap_or_default(), + published_at, + changes: Vec::new(), + }) +} + +fn id_of(row: &Row) -> Result { + serde_json::from_value(row["snapshot_id"].clone()) + .map_err(|error| ControlError::Invalid(format!("stored snapshot id: {error}"))) +} diff --git a/crates/sqlbuild-orchestration/src/snapshots/main/mod.rs b/crates/sqlbuild-orchestration/src/snapshots/main/mod.rs index 342c47fd5a..8c05cf753a 100644 --- a/crates/sqlbuild-orchestration/src/snapshots/main/mod.rs +++ b/crates/sqlbuild-orchestration/src/snapshots/main/mod.rs @@ -2,6 +2,7 @@ pub(super) mod _publish_snapshot; pub mod current_snapshot; pub mod current_snapshot_record; pub(crate) mod job_write_set; +pub mod list_person_snapshots; pub mod next_remote_generation; pub(crate) mod prune_snapshots; pub(crate) mod publish_development_snapshot; diff --git a/crates/sqlbuild-orchestration/src/snapshots/models.rs b/crates/sqlbuild-orchestration/src/snapshots/models.rs index 4fef7aee74..0428d221c6 100644 --- a/crates/sqlbuild-orchestration/src/snapshots/models.rs +++ b/crates/sqlbuild-orchestration/src/snapshots/models.rs @@ -122,3 +122,26 @@ impl Default for SnapshotRetention { } } } + +/// One publication as people browse it: its commit, environment and changed definitions. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, utoipa::ToSchema)] +pub struct PublishedSnapshot { + pub snapshot_id: Uuid, + pub generation: i64, + pub git_revision: Option, + pub git_dirty: bool, + pub git_author: Option, + /// Digest of the lock file, Python and `sqb` versions; a change means dependencies changed. + pub environment_hash: String, + pub sqb_version: String, + pub published_at: DateTime, + /// Added, changed and removed nodes and roles, in publication order. + pub changes: Vec, +} + +/// A changed subject, such as `model:orders` or `role:analyst`, without its content hashes. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] +pub struct PublishedChange { + pub change_kind: String, + pub subject_key: String, +} diff --git a/crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs b/crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs index f2d7d6a691..cc253368c7 100644 --- a/crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs +++ b/crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs @@ -288,6 +288,7 @@ fn given_browser_session_roles_when_work_is_requested_then_scope_revocation_and_ format!("/api/control/locations/{location}/targets/development/nodes"), format!("/api/control/locations/{location}/targets/development/jobs"), format!("/api/control/locations/{location}/targets/development/triggers"), + format!("/api/control/locations/{location}/targets/development/snapshots"), format!( "/api/control/locations/{location}/targets/development/triggers/{}/ticks", trigger.trigger_id diff --git a/crates/sqlbuild-orchestration/tests/api/test_reads.rs b/crates/sqlbuild-orchestration/tests/api/test_reads.rs index e3adc23031..2f3db1edf0 100644 --- a/crates/sqlbuild-orchestration/tests/api/test_reads.rs +++ b/crates/sqlbuild-orchestration/tests/api/test_reads.rs @@ -1,12 +1,14 @@ //! Real HTTP reads keep both location and target scope, including guessed nested resource IDs. +use crate::authorization::main::bootstrap_user::bootstrap_user; use crate::scheduler::tests::contracts::ticks::helpers::calendar_text; +use crate::snapshots::main::list_person_snapshots::list_person_snapshots; use std::collections::BTreeMap; use std::time::Duration; use crate::credentials::main::tokens::issue_token::issue_token; use crate::credentials::main::tokens::revoke_token::revoke_token; -use crate::credentials::models::{AuditContext, CredentialScope}; +use crate::credentials::models::{AuditContext, CredentialScope, DefaultRole}; use crate::declarations::main::change_trigger::change_trigger; use crate::declarations::main::list_triggers::list_triggers; use crate::declarations::models::{TriggerChange, TriggerChangeRequest}; @@ -323,6 +325,53 @@ fn given_published_locations_and_scheduled_work_when_read_through_http_then_reso ); assert_eq!(ticks[0]["outcome"], "requested"); assert_eq!(ticks[0]["scheduled_for"], calendar_text(2)); + let administrator = bootstrap_user(store, &audit, "orders-admin") + .await + .expect("administrator"); + let mut extra = input.clone(); + extra.generation += 1; + extra.nodes.remove("products"); + extra.git_dirty = !input.git_dirty; + publish_snapshot(store, &extra) + .await + .expect("second publication"); + input.generation = extra.generation; + let history = list_person_snapshots( + store, + (&audit, administrator), + ((location_id, "development"), 20), + DefaultRole::None, + ) + .await + .expect("person snapshots"); + assert_eq!( + history.iter().map(|s| s.generation).collect::>(), + [extra.generation, published.generation], + "newest first, this target only" + ); + assert_eq!( + json!(history[0].changes), + json!([{"change_kind": "removed", "subject_key": "products"}]), + "only the subjects that changed" + ); + assert!(!history[1].changes.is_empty()); + assert_eq!( + (history[0].git_dirty, history[1].git_dirty), + (extra.git_dirty, input.git_dirty), + "the working-tree flag survives each store's boolean type" + ); + assert_eq!( + list_person_snapshots( + store, + (&audit, administrator), + ((location_id, "development"), 1), + DefaultRole::None, + ) + .await + .expect("bounded snapshots") + .len(), + 1 + ); for path in [&run_path, &event_path, &tick_path, &invocation_path] { assert_eq!( read_json(&client, (&format!("{path}?limit=1"), &token.bearer)) diff --git a/fensu.toml b/fensu.toml index bee7f18a71..7c1f8e7b53 100644 --- a/fensu.toml +++ b/fensu.toml @@ -1637,7 +1637,6 @@ allowed = [ "sqb_platform_sink_deliveries.delivered_at=Audit history read through audit exports: when an event reached the platform sink.", "sqb_security_audit_deliveries.delivered_at=Audit history read through audit exports: when an event reached a security audit sink.", "sqb_run_requests.evaluation_id=Reader comes with the operations UI: a sensor's run requests show the evaluation that made them.", - "sqb_snapshots.git_author=Reader comes with the operations UI: the change feed shows who published each snapshot.", "sqb_write_claims=Read through its primary-key conflict: an insert that conflicts means another run already holds the node.", "sqb_warehouse_microbatches.execution_run_id=Reader comes with the UI epic (D132), which filters on it: the run that executed the batch.", "sqb_warehouse_microbatches.record_type=Reader comes with the UI epic (D132), which filters on it: the batch state.", diff --git a/ui/src/lib/api/http-changes.test.ts b/ui/src/lib/api/http-changes.test.ts new file mode 100644 index 0000000000..164b1a45ff --- /dev/null +++ b/ui/src/lib/api/http-changes.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from 'vitest'; +import type { components } from '$control/schema.d.ts'; +import { toSnapshotChanges } from './http-changes'; +import type { Run } from './types/operate'; + +type W = components['schemas']; + +function snapshot(id: string, generation: number, environment: string, changes: [string, string][]): W['PublishedSnapshot'] { + return { + snapshot_id: id, + generation, + git_revision: generation === 2 ? 'a1b2c3d' : null, + git_dirty: generation === 2, + git_author: generation === 2 ? 'Avery Example' : null, + environment_hash: environment, + sqb_version: '0.200.0', + published_at: `2026-11-0${generation}T09:00:00Z`, + changes: changes.map(([change_kind, subject_key]) => ({ change_kind, subject_key })) + }; +} + +function run(id: string, snapshotId: string, queuedAt: string, parentRunId: string | null = null): Run { + return { id, snapshotId, parentRunId, jobKey: 'orders', kind: 'job', status: 'success', startedAt: queuedAt, queuedAt } as Run; +} + +describe('change feed mapping', () => { + const newest = snapshot('s-2', 2, 'env-bbbbbbbbbbbbbbbb', [ + ['removed', 'model:stock'], + ['changed', 'seed:regions'], + ['added', 'role:analyst'] + ]); + const oldest = snapshot('s-1', 1, 'env-aaaaaaaaaaaaaaaa', [['added', 'model:orders']]); + // Runs are listed newest first, as the server returns them. + const runs = [run('r-late', 's-2', '2026-11-02T11:00:00Z'), run('r-child', 's-2', '2026-11-02T10:30:00Z', 'r-early'), run('r-early', 's-2', '2026-11-02T10:00:00Z'), run('r-old', 's-1', '2026-11-01T10:00:00Z')]; + const [second, first] = toSnapshotChanges([newest, oldest], runs); + + it('splits roles from nodes and keeps each change kind', () => { + expect(second.nodes.map((n) => [n.op, n.key, n.kind])).toEqual([ + ['removed', 'model:stock', null], + ['changed', 'seed:regions', 'seed'] + ]); + expect(second.roles.map((r) => [r.op, r.role])).toEqual([['added', 'analyst']]); + }); + + it('reports an environment change only against the previous publication', () => { + expect(second.environment).toMatchObject({ before: 'env-aaaaaaaa', after: 'env-bbbbbbbb' }); + expect(second.previousGeneration).toBe(1); + expect(first.environment).toBeNull(); + expect(first.previousGeneration).toBeNull(); + }); + + it("lists each deploy's own top-level runs, oldest first", () => { + expect(second.followingRuns.map((r) => r.id)).toEqual(['r-early', 'r-late']); + expect(first.followingRuns.map((r) => r.id)).toEqual(['r-old']); + }); + + it('keeps missing git facts empty instead of inventing them', () => { + expect([second.gitRevision, second.gitAuthor, second.gitDirty]).toEqual(['a1b2c3d', 'Avery Example', true]); + expect([first.gitRevision, first.gitAuthor, first.publishedBy]).toEqual([null, null, null]); + }); +}); diff --git a/ui/src/lib/api/http-changes.ts b/ui/src/lib/api/http-changes.ts new file mode 100644 index 0000000000..518a43ed28 --- /dev/null +++ b/ui/src/lib/api/http-changes.ts @@ -0,0 +1,65 @@ +import type { components } from '$control/schema.d.ts'; +import type { ChangeOp, NodeChange, RoleChange, SnapshotChange } from './types/build'; +import type { NodeKind, Run } from './types/operate'; + +type Wire = components['schemas']; + +/** How many runs of each deploy the feed lists. */ +const FOLLOWING_RUNS = 8; + +const NODE_KINDS: Record = { seed: 'seed', source: 'source' }; + +function opOf(kind: string): ChangeOp { + return kind === 'added' || kind === 'removed' ? kind : 'changed'; +} + +function nodeChange(change: Wire['PublishedChange']): NodeChange { + const prefix = change.subject_key.split(':', 1)[0]; + return { + key: change.subject_key, + kind: NODE_KINDS[prefix] ?? null, + op: opOf(change.change_kind), + formerKey: null, + compiledOnly: false, + detail: '', + columns: [] + }; +} + +function roleChange(change: Wire['PublishedChange']): RoleChange { + return { role: change.subject_key.slice('role:'.length), op: opOf(change.change_kind), detail: '', coverage: null }; +} + +/** + * Publications newest first, as the change feed shows them. The server records which nodes and + * roles changed, not job, trigger, column or cost changes, so those stay empty. Each deploy's runs + * are the listed runs pinned to its snapshot, oldest first. + */ +export function toSnapshotChanges(wire: Wire['PublishedSnapshot'][], runs: Run[]): SnapshotChange[] { + return wire.map((snapshot, index): SnapshotChange => { + const previous = wire[index + 1] ?? null; + const environmentChanged = previous !== null && previous.environment_hash !== snapshot.environment_hash; + return { + snapshotId: snapshot.snapshot_id, + generation: snapshot.generation, + previousGeneration: previous?.generation ?? null, + gitRevision: snapshot.git_revision ?? null, + gitDirty: snapshot.git_dirty, + gitAuthor: snapshot.git_author ?? null, + publishedAt: snapshot.published_at, + publishedBy: null, + nodes: snapshot.changes.filter((c) => !c.subject_key.startsWith('role:')).map(nodeChange), + orchestration: [], + roles: snapshot.changes.filter((c) => c.subject_key.startsWith('role:')).map(roleChange), + environment: environmentChanged + ? { before: previous.environment_hash.slice(0, 12), after: snapshot.environment_hash.slice(0, 12), detail: 'Dependencies or the sqb version changed' } + : null, + followingRuns: runs + .filter((r) => r.snapshotId === snapshot.snapshot_id && !r.parentRunId) + .reverse() + .slice(0, FOLLOWING_RUNS) + .map(({ id, jobKey, kind, status, startedAt, queuedAt }) => ({ id, jobKey, kind, status, startedAt, queuedAt })), + costChanges: null + }; + }); +} diff --git a/ui/src/lib/api/http-client.ts b/ui/src/lib/api/http-client.ts index 70a1df67b9..03baee8ca5 100644 --- a/ui/src/lib/api/http-client.ts +++ b/ui/src/lib/api/http-client.ts @@ -3,11 +3,12 @@ import type { components, paths } from '$control/schema.d.ts'; import { ApiError, type ApiClient } from './client'; import { checkDetailOf, toCheck, toCheckEvidence, toCheckSql } from './http-checks'; import { toCatalogNodes } from './http-catalog'; +import { toSnapshotChanges } from './http-changes'; import { jobDetailOf, toJob, toTick, toTrigger, triggerDetailOf } from './http-jobs'; import { RUN_WINDOW, runDetailOf, runRowPage, runsMatching, runTabCountsOf, toRunNode, toRuns } from './http-runs'; import type { ChangeSubscription, ChangedChannel, ChangedSignal, ConnectionState } from './signals'; import type { AuthMode, Bootstrap, Capability, Location, Scope } from './types'; -import type { CatalogNode, Check, CheckDetail, CheckEvidence, CheckSql } from './types/build'; +import type { CatalogNode, Check, CheckDetail, CheckEvidence, CheckSql, SnapshotChange } from './types/build'; import type { Job, JobDetail, RerunMode, Run, RunDetail, RunListQuery, RunRowPage, RunRowQuery, RunTabCounts, Trigger, TriggerDetail, TriggerState } from './types/operate'; type Wire = components['schemas']; @@ -36,7 +37,8 @@ const SUPPORTED: ReadonlySet = new Set([ 'getCheck', 'getCheckSql', 'getCheckEvidence', - 'listCatalogNodes' + 'listCatalogNodes', + 'listSnapshotChanges' ]); /** A UI location is one server location and target; its id carries both. */ @@ -244,6 +246,16 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt if (!data) throw new ApiError(errorText(error, response), response.status); return toCatalogNodes(data); }, + async listSnapshotChanges(scope: Scope, query: { limit?: number } = {}): Promise { + const { locationId, target } = parseLocationKey(scope.locationId); + const [snapshots, runs] = await Promise.all([ + http.GET('/api/control/locations/{location_id}/targets/{target}/snapshots', { params: { path: { location_id: locationId, target }, query: { limit: query.limit } } }), + recentRuns(scope) + ]); + if (snapshots.response.status === 401) onUnauthorized(); + if (!snapshots.data) throw new ApiError(errorText(snapshots.error, snapshots.response), snapshots.response.status); + return toSnapshotChanges(snapshots.data, runs); + }, listChecks: async (scope: Scope): Promise => (await checkResults(scope)).map(toCheck), getCheck: async (scope: Scope, id: string): Promise => checkDetailOf(await checkResults(scope, id)), async getCheckSql(scope: Scope, checkId: string): Promise { diff --git a/ui/src/lib/api/types/build.ts b/ui/src/lib/api/types/build.ts index 6bc9353ffb..2eb5eb15b8 100644 --- a/ui/src/lib/api/types/build.ts +++ b/ui/src/lib/api/types/build.ts @@ -315,11 +315,13 @@ export type SnapshotChange = { snapshotId: string; generation: number; previousGeneration: number | null; - gitRevision: string; + /** Null when the project was published from outside a git checkout. */ + gitRevision: string | null; gitDirty: boolean; - gitAuthor: string; + gitAuthor: string | null; publishedAt: Iso; - publishedBy: Principal; + /** Null when the server does not record who published. */ + publishedBy: Principal | null; nodes: NodeChange[]; orchestration: OrchestrationChange[]; roles: RoleChange[]; diff --git a/ui/src/lib/nav.test.ts b/ui/src/lib/nav.test.ts index 8cdc115678..f0eaa47e62 100644 --- a/ui/src/lib/nav.test.ts +++ b/ui/src/lib/nav.test.ts @@ -16,8 +16,9 @@ describe('visibleNav', () => { ]); }); - it('keeps lineage hidden until node detail is served, even when the catalog list is', () => { - expect(labels(serving('listCatalogNodes'), ['view'])).toEqual([['Build', ['Catalog']]]); + it('shows lineage with the catalog list, since the graph needs no node detail', () => { + expect(labels(serving('listCatalogNodes'), ['view'])).toEqual([['Build', ['Lineage', 'Catalog']]]); + expect(labels(serving('getCatalogNode'), ['view'])).toEqual([]); }); it('hides admin pages from people without an admin capability, even when the server serves them', () => { diff --git a/ui/src/lib/nav.ts b/ui/src/lib/nav.ts index 269b0c967e..789191fae9 100644 --- a/ui/src/lib/nav.ts +++ b/ui/src/lib/nav.ts @@ -26,7 +26,7 @@ export const NAV: readonly NavGroup[] = [ { section: 'Build', items: [ - { label: 'Lineage', href: '/lineage', icon: GitBranchIcon, loads: 'getCatalogNode' }, + { label: 'Lineage', href: '/lineage', icon: GitBranchIcon, loads: 'listCatalogNodes' }, { label: 'Catalog', href: '/catalog', icon: DatabaseIcon, loads: 'listCatalogNodes', badge: { value: (c) => c.catalogNodes, tone: 'muted' } }, { label: 'Checks', href: '/checks', icon: CheckCircleIcon, loads: 'listChecks', badge: { value: (c) => c.failingChecks, tone: 'warning' } }, { label: 'Changes', href: '/changes', icon: GitCompareIcon, loads: 'listSnapshotChanges' } diff --git a/ui/src/routes/changes/+page.svelte b/ui/src/routes/changes/+page.svelte index dbb27390dd..ba98226c03 100644 --- a/ui/src/routes/changes/+page.svelte +++ b/ui/src/routes/changes/+page.svelte @@ -12,7 +12,7 @@ import { runStatusLabel, runStatusTone } from '$lib/status'; import { changeOpLabel, changeOpTone, formatCredits } from '$lib/build-area/labels'; import { summarizeChange } from '$lib/build-area/changes'; - import { orchestrationHref, percentChange } from './utils'; + import { nodeHref, orchestrationHref, percentChange } from './utils'; type Area = 'all' | 'nodes' | 'orchestration' | 'roles' | 'environment'; @@ -32,7 +32,7 @@ if (!hasArea(c, area)) return false; const q = query.trim().toLowerCase(); if (!q) return true; - return [c.gitRevision, c.gitAuthor, String(c.generation), ...c.nodes.map((n) => n.key), ...c.nodes.map((n) => n.formerKey ?? ''), ...c.orchestration.map((o) => o.key), ...c.roles.map((r) => r.role)].some((s) => s.toLowerCase().includes(q)); + return [c.gitRevision ?? '', c.gitAuthor ?? '', String(c.generation), ...c.nodes.map((n) => n.key), ...c.nodes.map((n) => n.formerKey ?? ''), ...c.orchestration.map((o) => o.key), ...c.roles.map((r) => r.role)].some((s) => s.toLowerCase().includes(q)); }) ); const areaOptions: { value: Area; label: string }[] = [ @@ -62,12 +62,12 @@
Version {c.generation} - {c.gitRevision}{c.gitDirty ? ' · with uncommitted changes' : ''} + {#if c.gitRevision}{c.gitRevision}{c.gitDirty ? ' · with uncommitted changes' : ''}{/if} {summarizeChange(c)}
- {c.gitAuthor} - deployed by {c.publishedBy.displayName} · {formatAgo(c.publishedAt, app.now)} + {#if c.gitAuthor}{c.gitAuthor}{/if} + {c.publishedBy ? `deployed by ${c.publishedBy.displayName} · ` : 'published '}{formatAgo(c.publishedAt, app.now)}
@@ -76,9 +76,10 @@

Nodes {c.nodes.length}

{#each c.nodes as n (n.key + n.op)} + {@const href = nodeHref(n, app.client.supports('getCatalogNode'))}
{changeOpLabel[n.op]} - {#if n.op === 'removed'}{n.key}{:else}{n.key}{/if} + {#if href}{n.key}{:else}{n.key}{/if} {n.detail}{#if n.compiledOnly}compiled only{/if}
{#each n.columns as col (col.name)} diff --git a/ui/src/routes/changes/utils.ts b/ui/src/routes/changes/utils.ts index 145a2de178..ae85baf1c3 100644 --- a/ui/src/routes/changes/utils.ts +++ b/ui/src/routes/changes/utils.ts @@ -1,6 +1,6 @@ // Pure helpers for the Changes page. -import type { OrchestrationChange } from '$lib/api'; +import type { NodeChange, OrchestrationChange } from '$lib/api'; export function orchestrationHref(change: OrchestrationChange): string | null { if (change.op === 'removed') return null; @@ -11,3 +11,11 @@ export function orchestrationHref(change: OrchestrationChange): string | null { export function percentChange(before: number, after: number): number { return before ? Math.round(((after - before) / before) * 100) : 0; } + +/** A changed node's page: its catalog detail, or the lineage graph when the server has no detail read. */ +export function nodeHref(change: NodeChange, catalogDetail: boolean): string | null { + if (change.op === 'removed') return null; + if (catalogDetail) return `/catalog/${change.key}${change.op === 'changed' ? '?tab=drift' : ''}`; + const graphNode = change.kind !== null || change.key.startsWith('model:') || !change.key.includes(':'); + return graphNode ? `/lineage?select=${encodeURIComponent(change.key)}` : null; +} diff --git a/ui/src/routes/lineage/+page.svelte b/ui/src/routes/lineage/+page.svelte index 83f904fb3f..3bb38719e1 100644 --- a/ui/src/routes/lineage/+page.svelte +++ b/ui/src/routes/lineage/+page.svelte @@ -243,11 +243,14 @@ const primary = $derived(selectedNodes[0] ?? nodes.find((n) => n.id === scaleStartId)); const primarySlug = $derived(primary?.data.name ?? ''); // Inspector detail for the primary selection; the scale-test graph is synthetic, so it has none. + // Without a detail read the inspector shows what the catalog listing reports. + const detailSupported = app.client.supports('getCatalogNode'); const detail = createResource( - (client, scope) => (scaleMode || !primarySlug ? Promise.resolve(null) : client.getCatalogNode(scope, String(primarySlug))), + (client, scope) => (scaleMode || !primarySlug || !detailSupported ? Promise.resolve(null) : client.getCatalogNode(scope, String(primarySlug))), ['runs', 'checks', 'snapshot', 'node_status'] ); const d = $derived(detail.data && detail.data.key === primarySlug ? detail.data : null); + const listed = $derived(detailSupported ? null : (catalogByKey.get(String(primarySlug)) ?? null)); const isSource = $derived(d?.kind === 'source'); const hasSql = $derived(!!d?.sql); @@ -455,7 +458,7 @@ - (runOpen = true)}>▷ Execute{runSelection.length ? ` ${runSelection.length}` : ''} + {#if app.client.supports('launchAdhocRun')} (runOpen = true)}>▷ Execute{runSelection.length ? ` ${runSelection.length}` : ''}{/if} @@ -632,6 +635,16 @@

A synthetic graph for layout and rendering at scale; nodes have no catalog detail.

+ {:else if listed} + +
+
Kind
{kindLine(listed)}
+ {#if listed.relation}
Relation
{relationName(listed.relation)}
{/if} + {#if listed.kind !== 'source'}
Last built
{#if listed.lastBuild}{:else}No build recorded{/if}
{/if} + {#if listed.tags.length}
Tags
{listed.tags.join(', ')}
{/if} + {#if listed.path}
File
{listed.path}
{/if} +
+
{:else if !d}

{detail.error ?? 'Loading…'}

{:else if isSource} @@ -675,9 +688,9 @@ {/if} - {#if d?.description} + {#if (d ?? listed)?.description} -

{d.description}

+

{(d ?? listed)?.description}

{/if} From 07937044a05da1e84640e1eafd0cb737f1bce052 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 10:58:29 +0000 Subject: [PATCH 036/106] feat(server): page the security audit newest first with actor names Person reads of the security audit accept an exclusive upper cursor that returns the newest entries first, and name each principal actor. Exports keep their payload unchanged. --- clients/typescript/src/schema.d.ts | 4 + .../src/api/_helpers/resources/audit.rs | 8 +- .../sqlbuild-orchestration/src/api/models.rs | 1 + .../src/authorization/_helpers/mod.rs | 1 + .../authorization/_helpers/principal_names.rs | 29 ++++++ .../authorization/main/read_audit_events.rs | 17 +++- .../authorization/main/read_security_audit.rs | 9 +- .../src/credentials/models.rs | 3 + .../src/webhooks/_helpers/events.rs | 3 +- .../tests/credentials/test_audit.rs | 91 ++++++++++++++----- 10 files changed, 134 insertions(+), 32 deletions(-) create mode 100644 crates/sqlbuild-orchestration/src/authorization/_helpers/principal_names.rs diff --git a/clients/typescript/src/schema.d.ts b/clients/typescript/src/schema.d.ts index 35c7f687f4..acc45842d1 100644 --- a/clients/typescript/src/schema.d.ts +++ b/clients/typescript/src/schema.d.ts @@ -1802,6 +1802,8 @@ export interface components { SecurityAuditEvent: { action: string; actor: string; + /** @description The person's name when `actor` is a principal; person reads set it, exports leave it out. */ + actor_name?: string | null; after_json: unknown; before_json: unknown; /** Format: uuid */ @@ -2540,6 +2542,8 @@ export interface operations { query?: { /** @description Exclusive storage-order cursor */ after?: number; + /** @description Exclusive upper cursor; when set, the page is newest first */ + before?: number; limit?: number; }; header?: never; diff --git a/crates/sqlbuild-orchestration/src/api/_helpers/resources/audit.rs b/crates/sqlbuild-orchestration/src/api/_helpers/resources/audit.rs index 2ec377e95e..1cf398f92d 100644 --- a/crates/sqlbuild-orchestration/src/api/_helpers/resources/audit.rs +++ b/crates/sqlbuild-orchestration/src/api/_helpers/resources/audit.rs @@ -11,7 +11,8 @@ use crate::credentials::models::{AuditContext, SecurityAuditEvent}; use crate::store::models::ControlStore; #[utoipa::path(get, path = "/api/control/audit", operation_id = "readSecurityAudit", - params(("after" = Option, Query, description = "Exclusive storage-order cursor"), ("limit" = Option, Query)), + params(("after" = Option, Query, description = "Exclusive storage-order cursor"), + ("before" = Option, Query, description = "Exclusive upper cursor; when set, the page is newest first"), ("limit" = Option, Query)), responses((status = 200, body = Vec), (status = 400, body = crate::api::models::ApiErrorBody), (status = 401, description = "Session rejected"), (status = 403, body = crate::api::models::ApiErrorBody)), security(("session" = []), ("cliSession" = [])))] @@ -24,7 +25,10 @@ pub(crate) async fn read( read_security_audit( &store, (&context, principal), - (options.after.unwrap_or(0), options.limit.unwrap_or(100)), + ( + (options.after.unwrap_or(0), options.before), + options.limit.unwrap_or(100), + ), ) .await?, )) diff --git a/crates/sqlbuild-orchestration/src/api/models.rs b/crates/sqlbuild-orchestration/src/api/models.rs index 9571345a46..1a639b5176 100644 --- a/crates/sqlbuild-orchestration/src/api/models.rs +++ b/crates/sqlbuild-orchestration/src/api/models.rs @@ -87,6 +87,7 @@ pub(crate) struct ReadQuery { #[serde(deny_unknown_fields)] pub(crate) struct ReadAuditQuery { pub(crate) after: Option, + pub(crate) before: Option, pub(crate) limit: Option, } diff --git a/crates/sqlbuild-orchestration/src/authorization/_helpers/mod.rs b/crates/sqlbuild-orchestration/src/authorization/_helpers/mod.rs index 831d2d7b1e..fd4b1031c4 100644 --- a/crates/sqlbuild-orchestration/src/authorization/_helpers/mod.rs +++ b/crates/sqlbuild-orchestration/src/authorization/_helpers/mod.rs @@ -3,3 +3,4 @@ pub(crate) mod administrator_roles; pub(crate) mod administrators; pub(crate) mod custom_roles; pub(crate) mod person_grants; +pub(crate) mod principal_names; diff --git a/crates/sqlbuild-orchestration/src/authorization/_helpers/principal_names.rs b/crates/sqlbuild-orchestration/src/authorization/_helpers/principal_names.rs new file mode 100644 index 0000000000..5e6605f309 --- /dev/null +++ b/crates/sqlbuild-orchestration/src/authorization/_helpers/principal_names.rs @@ -0,0 +1,29 @@ +//! Audit actors name principals by ID; people read them by name. + +use std::collections::BTreeMap; + +use crate::store::errors::ControlError; +use crate::store::main::query::query; +use crate::store::models::ControlTransaction; + +/// Each principal's name, keyed by its audit actor text, `principal:`. +pub(crate) async fn principal_names( + transaction: ControlTransaction, +) -> Result<(ControlTransaction, BTreeMap), ControlError> { + let (transaction, rows) = query( + transaction, + "SELECT principal_id, name FROM sqb_principals", + Vec::new(), + ) + .await?; + let names: BTreeMap = rows + .iter() + .filter_map(|row| { + Some(( + format!("principal:{}", row["principal_id"].as_str()?), + row["name"].as_str()?.to_owned(), + )) + }) + .collect(); + Ok((transaction, names)) +} diff --git a/crates/sqlbuild-orchestration/src/authorization/main/read_audit_events.rs b/crates/sqlbuild-orchestration/src/authorization/main/read_audit_events.rs index 8791f4349e..1d1281a142 100644 --- a/crates/sqlbuild-orchestration/src/authorization/main/read_audit_events.rs +++ b/crates/sqlbuild-orchestration/src/authorization/main/read_audit_events.rs @@ -7,17 +7,26 @@ use crate::store::models::{ControlTransaction, Parameter}; pub(crate) async fn read_audit_events( transaction: ControlTransaction, - (after, limit): (i64, u32), + ((after, before), limit): ((i64, Option), u32), ) -> Result<(ControlTransaction, Vec), ControlError> { if after < 0 || !(1..=100).contains(&limit) { return Err(ControlError::Invalid( "audit pages require a nonnegative storage cursor and a limit between 1 and 100".into(), )); } + let (window, order) = match before { + Some(_) => ("storage_id>$1 AND storage_id<$3", "DESC"), + None => ("storage_id>$1", "ASC"), + }; + let mut parameters = vec![ + Parameter::Integer(after), + Parameter::Integer(i64::from(limit)), + ]; + parameters.extend(before.map(Parameter::Integer)); let (transaction, rows) = query(transaction, - "SELECT storage_id,event_id,occurred_at,actor,action,granting_role,request_id,ip_address,scope_json,before_json,after_json \ - FROM sqb_security_audit WHERE storage_id>$1 ORDER BY storage_id LIMIT $2", - vec![Parameter::Integer(after), Parameter::Integer(i64::from(limit))]).await?; + &format!("SELECT storage_id,event_id,occurred_at,actor,action,granting_role,request_id,ip_address,scope_json,before_json,after_json \ + FROM sqb_security_audit WHERE {window} ORDER BY storage_id {order} LIMIT $2"), + parameters).await?; Ok(( transaction, rows.into_iter().map(decode_row).collect::>()?, diff --git a/crates/sqlbuild-orchestration/src/authorization/main/read_security_audit.rs b/crates/sqlbuild-orchestration/src/authorization/main/read_security_audit.rs index 81df5bd0ff..9e1a768c48 100644 --- a/crates/sqlbuild-orchestration/src/authorization/main/read_security_audit.rs +++ b/crates/sqlbuild-orchestration/src/authorization/main/read_security_audit.rs @@ -2,6 +2,7 @@ use uuid::Uuid; +use crate::authorization::_helpers::principal_names::principal_names; use crate::authorization::main::begin_authorized::begin_authorized; use crate::authorization::main::read_audit_events::read_audit_events; use crate::credentials::models::{ @@ -14,7 +15,7 @@ use crate::store::models::ControlStore; pub async fn read_security_audit( store: &ControlStore, (context, credentials): (&AuditContext, Uuid), - (after, limit): (i64, u32), + (cursor, limit): ((i64, Option), u32), ) -> Result, ControlError> { let (transaction, _) = begin_authorized( store, @@ -23,7 +24,11 @@ pub async fn read_security_audit( DefaultRole::None, ) .await?; - let (transaction, events) = read_audit_events(transaction, (after, limit)).await?; + let (transaction, mut events) = read_audit_events(transaction, (cursor, limit)).await?; + let (transaction, names) = principal_names(transaction).await?; commit(transaction).await?; + for event in &mut events { + event.actor_name = names.get(&event.actor).cloned(); + } Ok(events) } diff --git a/crates/sqlbuild-orchestration/src/credentials/models.rs b/crates/sqlbuild-orchestration/src/credentials/models.rs index fc0a74344e..e9b2ac3978 100644 --- a/crates/sqlbuild-orchestration/src/credentials/models.rs +++ b/crates/sqlbuild-orchestration/src/credentials/models.rs @@ -39,6 +39,9 @@ pub struct SecurityAuditEvent { pub scope_json: serde_json::Value, pub before_json: serde_json::Value, pub after_json: serde_json::Value, + /// The person's name when `actor` is a principal; person reads set it, exports leave it out. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub actor_name: Option, } /// Installation configuration: rows past the horizon stay until every sink and retry lookup is done. diff --git a/crates/sqlbuild-orchestration/src/webhooks/_helpers/events.rs b/crates/sqlbuild-orchestration/src/webhooks/_helpers/events.rs index 50c830a7a8..07c97c1511 100644 --- a/crates/sqlbuild-orchestration/src/webhooks/_helpers/events.rs +++ b/crates/sqlbuild-orchestration/src/webhooks/_helpers/events.rs @@ -20,7 +20,8 @@ pub(crate) async fn read_events( )) } WebhookSource::SecurityAudit => { - let (transaction, events) = read_audit_events(transaction, (after, 100)).await?; + let (transaction, events) = + read_audit_events(transaction, ((after, None), 100)).await?; Ok(( transaction, events diff --git a/crates/sqlbuild-orchestration/tests/credentials/test_audit.rs b/crates/sqlbuild-orchestration/tests/credentials/test_audit.rs index 247721fd30..b101ac85c7 100644 --- a/crates/sqlbuild-orchestration/tests/credentials/test_audit.rs +++ b/crates/sqlbuild-orchestration/tests/credentials/test_audit.rs @@ -113,16 +113,39 @@ fn given_security_audit_pages_when_clock_and_authority_change_then_commit_order_ .expect("role assignment") ); assert!(matches!( - read_security_audit(&replica, (&context, ordinary_credentials), (0, 100)).await, + read_security_audit( + &replica, + (&context, ordinary_credentials), + ((0, None), 100) + ) + .await, Err(ControlError::Denied { .. }) )); } - let existing = read_security_audit(store, (&context, admin_credentials), (0, 100)) - .await - .expect("existing security facts"); - let upgraded = read_security_audit(&replica, (&context, admin_credentials), (0, 100)) - .await - .expect("replica audit"); + let existing = + read_security_audit(store, (&context, admin_credentials), ((0, None), 100)) + .await + .expect("existing security facts"); + assert_eq!( + existing + .iter() + .filter(|event| event.actor == format!("principal:{admin}")) + .map(|event| event.actor_name.as_deref()) + .collect::>(), + [Some("orders-admin")].into(), + "a principal actor is named" + ); + assert!( + existing + .iter() + .filter(|event| !event.actor.starts_with("principal:")) + .all(|event| event.actor_name.is_none()), + "other actors keep only their actor text" + ); + let upgraded = + read_security_audit(&replica, (&context, admin_credentials), ((0, None), 100)) + .await + .expect("replica audit"); assert_eq!( upgraded .iter() @@ -180,9 +203,10 @@ fn given_security_audit_pages_when_clock_and_authority_change_then_commit_order_ .expect("durable clock fact"); commit(transaction).await.expect("commit clock fact"); } - let first = read_security_audit(&replica, (&context, admin_credentials), (cursor, 2)) - .await - .expect("first bounded page"); + let first = + read_security_audit(&replica, (&context, admin_credentials), ((cursor, None), 2)) + .await + .expect("first bounded page"); assert_eq!( first.len(), test_case.expected_page_length, @@ -206,7 +230,7 @@ fn given_security_audit_pages_when_clock_and_authority_change_then_commit_order_ let last = read_security_audit( store, (&context, admin_credentials), - (first[1].storage_id, 2), + ((first[1].storage_id, None), 2), ) .await .expect("strict continuation"); @@ -214,14 +238,29 @@ fn given_security_audit_pages_when_clock_and_authority_change_then_commit_order_ last.iter().map(|event| event.event_id).collect::>(), ids[2..] ); + let newest = read_security_audit( + store, + (&context, admin_credentials), + ((cursor, Some(last[0].storage_id)), 100), + ) + .await + .expect("newest-first window"); + assert_eq!( + newest + .iter() + .map(|event| event.event_id) + .collect::>(), + [ids[1], ids[0]], + "both bounds are exclusive and the window reads newest first" + ); let cursor = last[0].storage_id; assert!( - read_security_audit(store, (&context, admin_credentials), (cursor, 100)) + read_security_audit(store, (&context, admin_credentials), ((cursor, None), 100)) .await .expect("empty continuation") .is_empty() ); - for bounds in [(-1, 1), (0, 0), (0, 101)] { + for bounds in [((-1, None), 1), ((0, None), 0), ((0, None), 101)] { assert!(matches!( read_security_audit(store, (&context, admin_credentials), bounds).await, Err(ControlError::Invalid(_)) @@ -243,17 +282,22 @@ fn given_security_audit_pages_when_clock_and_authority_change_then_commit_order_ ); remove_fault(store, &fault).await; assert!( - read_security_audit(&replica, (&context, admin_credentials), (cursor, 100)) - .await - .expect("failed audit did not publish") - .is_empty() + read_security_audit( + &replica, + (&context, admin_credentials), + ((cursor, None), 100) + ) + .await + .expect("failed audit did not publish") + .is_empty() ); create_principal(store, &context, "orders-rollback") .await .expect("failed principal did not persist"); - let after = read_security_audit(store, (&context, admin_credentials), (cursor, 100)) - .await - .expect("successful retry"); + let after = + read_security_audit(store, (&context, admin_credentials), ((cursor, None), 100)) + .await + .expect("successful retry"); assert_eq!(after.len(), 1); assert_eq!(after[0].storage_id, cursor + 1); let cursor = after[0].storage_id; @@ -266,7 +310,7 @@ fn given_security_audit_pages_when_clock_and_authority_change_then_commit_order_ right.expect("second concurrent append") ); let concurrent = - read_security_audit(store, (&context, admin_credentials), (cursor, 100)) + read_security_audit(store, (&context, admin_credentials), ((cursor, None), 100)) .await .expect("serialized writers"); assert_eq!( @@ -312,11 +356,12 @@ fn given_security_audit_pages_when_clock_and_authority_change_then_commit_order_ .expect("live role revocation") ); assert!(matches!( - read_security_audit(&replica, (&context, admin_credentials), (0, 100)).await, + read_security_audit(&replica, (&context, admin_credentials), ((0, None), 100)) + .await, Err(ControlError::Denied { .. }) )); assert!( - !read_security_audit(&replica, (&context, ordinary_credentials), (0, 100)) + !read_security_audit(&replica, (&context, ordinary_credentials), ((0, None), 100)) .await .expect("replacement administrator reads audit") .is_empty() From eb7c8803e8b6362fda40680555358d43108286f6 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 11:03:55 +0000 Subject: [PATCH 037/106] feat(ui): wire the audit log to the security audit read The audit log reads the newest security facts first, lists a denial under the action it refused with its decision ID and reason, and names principal actors. Its actor and action filters come from the entries, and links to the person page and access checker stay hidden until the people read exists. --- ui/src/lib/api/http-audit.test.ts | 57 ++++++++++++++++++++ ui/src/lib/api/http-audit.ts | 66 +++++++++++++++++++++++ ui/src/lib/api/http-client.ts | 24 ++++++++- ui/src/lib/api/types/platform.ts | 3 +- ui/src/lib/platform/permission-catalog.ts | 31 ++++++++--- ui/src/routes/admin/audit/+page.svelte | 35 ++++++++---- 6 files changed, 199 insertions(+), 17 deletions(-) create mode 100644 ui/src/lib/api/http-audit.test.ts create mode 100644 ui/src/lib/api/http-audit.ts diff --git a/ui/src/lib/api/http-audit.test.ts b/ui/src/lib/api/http-audit.test.ts new file mode 100644 index 0000000000..7ebccefbf4 --- /dev/null +++ b/ui/src/lib/api/http-audit.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from 'vitest'; +import { auditMatches, toAuditEntry } from './http-audit'; + +const base = { + storage_id: 7, + event_id: 'e-1', + occurred_at: '2026-11-08T10:00:00Z', + request_id: 'r-1', + ip_address: '127.0.0.1', + before_json: null +}; + +describe('toAuditEntry', () => { + it('lists a denial under the refused action with its decision and reason', () => { + const entry = toAuditEntry({ + ...base, + actor: 'principal:p-2', + actor_name: 'orders-viewer', + action: 'authorization.denied', + granting_role: 'none', + scope_json: { action: 'run.cancel', location_id: 'loc-1', target: 'prod', nodes: [] }, + after_json: { decision_id: 'd-9', allowed: false, reason: 'missing permission' } + }); + expect(entry).toMatchObject({ + actor: { kind: 'user', name: 'orders-viewer' }, + action: 'run.cancel', + outcome: 'denied', + grantingRole: null, + decisionId: 'd-9', + summary: 'missing permission', + after: null, + scope: { locationId: 'loc-1:prod', subject: null } + }); + }); + + it('keeps an allowed change with its role, values and remaining scope', () => { + const entry = toAuditEntry({ + ...base, + actor: 'scheduler/s-1', + action: 'token.issued', + granting_role: 'scheduler', + scope_json: { kind: 'run', run_id: 'run-4' }, + after_json: { token_id: 't-1' } + }); + expect(entry).toMatchObject({ + actor: { kind: 'system', name: 'scheduler/s-1' }, + action: 'token.issued', + outcome: 'allowed', + grantingRole: 'scheduler', + decisionId: null, + after: '{"token_id":"t-1"}', + scope: { locationId: null, subject: 'run_id=run-4' } + }); + expect(auditMatches(entry, { outcome: 'denied' })).toBe(false); + expect(auditMatches(entry, { actor: 'scheduler/s-1', action: 'token.issued' })).toBe(true); + }); +}); diff --git a/ui/src/lib/api/http-audit.ts b/ui/src/lib/api/http-audit.ts new file mode 100644 index 0000000000..0000c6d0d3 --- /dev/null +++ b/ui/src/lib/api/http-audit.ts @@ -0,0 +1,66 @@ +import type { components } from '$control/schema.d.ts'; +import { locationKey } from './http-client'; +import type { AuditEntry, AuditQuery } from './types/platform'; + +type Wire = components['schemas']; + +const DENIED = 'authorization.denied'; + +function fieldsOf(value: unknown): Record { + return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record) : {}; +} + +function textOf(value: unknown): string | null { + return typeof value === 'string' && value !== '' ? value : null; +} + +function jsonText(value: unknown): string | null { + return value === null || value === undefined ? null : JSON.stringify(value); +} + +/** The scope's location in UI terms, and its remaining fields as the subject. */ +function scopeOf(scope: Record): AuditEntry['scope'] { + const { location_id: location, target, action: _action, kind: _kind, ...rest } = scope; + const locationId = textOf(location); + const subject = Object.entries(rest) + .filter(([, value]) => value !== null && !(Array.isArray(value) && value.length === 0)) + .map(([key, value]) => `${key}=${typeof value === 'string' ? value : JSON.stringify(value)}`) + .join(' '); + return { + locationId: locationId && textOf(target) ? locationKey(locationId, target as string) : locationId, + subject: subject || null + }; +} + +/** A denial is listed under the action it refused, with its decision ID and reason. */ +export function toAuditEntry(wire: Wire['SecurityAuditEvent']): AuditEntry { + const denied = wire.action === DENIED; + const scope = fieldsOf(wire.scope_json); + const after = fieldsOf(wire.after_json); + const name = wire.actor_name ?? wire.actor; + return { + id: wire.event_id, + at: wire.occurred_at, + actor: { kind: wire.actor_name ? 'user' : 'system', name, displayName: name }, + action: denied ? (textOf(scope.action) ?? DENIED) : wire.action, + outcome: denied ? 'denied' : 'allowed', + scope: scopeOf(scope), + grantingRole: denied ? null : wire.granting_role, + decisionId: denied ? textOf(after.decision_id) : null, + evidenceRead: wire.action === 'evidence.read', + summary: denied ? (textOf(after.reason) ?? '') : '', + before: jsonText(wire.before_json), + after: denied ? null : jsonText(wire.after_json), + ip: wire.ip_address ?? '', + requestId: wire.request_id + }; +} + +export function auditMatches(entry: AuditEntry, query: AuditQuery): boolean { + return ( + (!query.actor || entry.actor.name === query.actor) && + (!query.action || entry.action === query.action) && + (!query.outcome || entry.outcome === query.outcome) && + (!query.evidenceOnly || entry.evidenceRead) + ); +} diff --git a/ui/src/lib/api/http-client.ts b/ui/src/lib/api/http-client.ts index 03baee8ca5..401465a6b0 100644 --- a/ui/src/lib/api/http-client.ts +++ b/ui/src/lib/api/http-client.ts @@ -3,11 +3,13 @@ import type { components, paths } from '$control/schema.d.ts'; import { ApiError, type ApiClient } from './client'; import { checkDetailOf, toCheck, toCheckEvidence, toCheckSql } from './http-checks'; import { toCatalogNodes } from './http-catalog'; +import { auditMatches, toAuditEntry } from './http-audit'; import { toSnapshotChanges } from './http-changes'; import { jobDetailOf, toJob, toTick, toTrigger, triggerDetailOf } from './http-jobs'; import { RUN_WINDOW, runDetailOf, runRowPage, runsMatching, runTabCountsOf, toRunNode, toRuns } from './http-runs'; import type { ChangeSubscription, ChangedChannel, ChangedSignal, ConnectionState } from './signals'; import type { AuthMode, Bootstrap, Capability, Location, Scope } from './types'; +import type { AuditEntry, AuditQuery } from './types/platform'; import type { CatalogNode, Check, CheckDetail, CheckEvidence, CheckSql, SnapshotChange } from './types/build'; import type { Job, JobDetail, RerunMode, Run, RunDetail, RunListQuery, RunRowPage, RunRowQuery, RunTabCounts, Trigger, TriggerDetail, TriggerState } from './types/operate'; @@ -38,9 +40,14 @@ const SUPPORTED: ReadonlySet = new Set([ 'getCheckSql', 'getCheckEvidence', 'listCatalogNodes', - 'listSnapshotChanges' + 'listSnapshotChanges', + 'listAuditEntries' ]); +/** The server's largest audit page, and how many the audit log reads before it stops looking. */ +const AUDIT_PAGE = 100; +const AUDIT_PAGES = 10; + /** A UI location is one server location and target; its id carries both. */ export function locationKey(locationId: string, target: string): string { return `${locationId}:${target}`; @@ -256,6 +263,21 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt if (!snapshots.data) throw new ApiError(errorText(snapshots.error, snapshots.response), snapshots.response.status); return toSnapshotChanges(snapshots.data, runs); }, + /** Newest first, reading at most `AUDIT_PAGES` server pages to find `limit` matching entries. */ + async listAuditEntries(query: AuditQuery = {}): Promise { + const limit = query.limit ?? 100; + const found: AuditEntry[] = []; + let before = Number.MAX_SAFE_INTEGER; + for (let page = 0; page < AUDIT_PAGES && found.length < limit; page++) { + const { data, error, response } = await http.GET('/api/control/audit', { params: { query: { before, limit: AUDIT_PAGE } } }); + if (response.status === 401) onUnauthorized(); + if (!data) throw new ApiError(errorText(error, response), response.status); + found.push(...data.map(toAuditEntry).filter((entry) => auditMatches(entry, query))); + if (data.length < AUDIT_PAGE) break; + before = data[data.length - 1].storage_id; + } + return found.slice(0, limit); + }, listChecks: async (scope: Scope): Promise => (await checkResults(scope)).map(toCheck), getCheck: async (scope: Scope, id: string): Promise => checkDetailOf(await checkResults(scope, id)), async getCheckSql(scope: Scope, checkId: string): Promise { diff --git a/ui/src/lib/api/types/platform.ts b/ui/src/lib/api/types/platform.ts index 4c130b04f8..92defd33c6 100644 --- a/ui/src/lib/api/types/platform.ts +++ b/ui/src/lib/api/types/platform.ts @@ -395,7 +395,8 @@ export type AuditEntry = { id: string; at: Iso; actor: Principal; - action: Capability | 'session.login'; + /** A capability, or a recorded event such as `token.issued`; a denial carries the refused capability. */ + action: string; outcome: AuditOutcome; scope: AssignmentScope & { subject: string | null }; /** Role that granted the action, or for denials null. */ diff --git a/ui/src/lib/platform/permission-catalog.ts b/ui/src/lib/platform/permission-catalog.ts index 8686dd3bef..5c90e8c1c4 100644 --- a/ui/src/lib/platform/permission-catalog.ts +++ b/ui/src/lib/platform/permission-catalog.ts @@ -72,13 +72,32 @@ export function requiresText(action: Capability): string { return `Requires “${PERMISSIONS[action].label}” (${action})`; } -const AUDIT_ONLY: Record, string> = { - 'session.login': 'Signed in' +const AUDIT_ONLY: Record = { + 'session.login': 'Signed in', + 'admin.granted': 'Changed administrators', + 'authorization.denied': 'Denied', + 'cli.login.started': 'Started CLI sign-in', + 'cli.login.decided': 'Decided CLI sign-in', + 'cli.login.exchanged': 'Completed CLI sign-in', + 'evidence.read': 'Read sample rows', + 'job.run_requested': 'Requested a job run', + 'location.created': 'Created location', + 'oidc.flow.created': 'Started SSO sign-in', + 'oidc.authenticated': 'Signed in with SSO', + 'password.changed': 'Changed password', + 'principal.created': 'Added person', + 'role.assigned': 'Changed role', + 'run.cancel_requested': 'Requested cancel', + 'run.rerun_requested': 'Requested rerun', + 'session.created': 'Signed in', + 'session.revoked': 'Signed out', + 'sql.read': 'Read SQL', + 'token.issued': 'Issued token', + 'token.revoked': 'Revoked token', + 'trigger.changed': 'Changed trigger' }; /** Plain name of an audited action, including the audit-only ones. */ -export function auditActionLabel(action: AuditEntry['action']): string { - return action in PERMISSIONS ? PERMISSIONS[action as Capability].label : AUDIT_ONLY[action as keyof typeof AUDIT_ONLY]; +export function auditActionLabel(action: string): string { + return action in PERMISSIONS ? PERMISSIONS[action as Capability].label : (AUDIT_ONLY[action] ?? action); } - -export const AUDIT_ACTIONS: AuditEntry['action'][] = [...CAPABILITIES, 'session.login']; diff --git a/ui/src/routes/admin/audit/+page.svelte b/ui/src/routes/admin/audit/+page.svelte index fe37712c67..8b0539a81b 100644 --- a/ui/src/routes/admin/audit/+page.svelte +++ b/ui/src/routes/admin/audit/+page.svelte @@ -11,7 +11,7 @@ import CopyButton from '$lib/components/copy-button.svelte'; import type { AuditOutcome } from '$lib/api'; import { checkHref, personHref } from '$lib/platform/access'; - import { AUDIT_ACTIONS, auditActionLabel } from '$lib/platform/permission-catalog'; + import { auditActionLabel } from '$lib/platform/permission-catalog'; import { formatAgo, formatStamp } from '$lib/format/time'; import { formatAssignmentScope } from '$lib/platform/labels'; import { app } from '$lib/state/app.svelte'; @@ -30,8 +30,11 @@ const everyone = createResource((client) => (app.can('audit.view') ? client.listAuditEntries({ limit: 500 }) : Promise.resolve([]))); const actors = $derived([...new Set((everyone.data ?? []).map((e) => e.actor.name))].sort()); + const actions = $derived([...new Set((everyone.data ?? []).map((e) => e.action))].sort()); const rows = $derived(entries.data ?? []); const locations = $derived(app.bootstrap?.locations ?? []); + // The person page and the access checker both read people. + const linksPeople = $derived(app.client.supports('listAccounts')); @@ -55,7 +58,7 @@ - {#if e.actor.kind === 'user'}{e.actor.name}{:else}{e.actor.name}{/if} + {#if e.actor.kind === 'user' && linksPeople}{e.actor.name}{:else}{e.actor.name}{/if}
{e.ip}
{auditActionLabel(e.action)} {#if e.evidenceRead} read sample rows{/if} - + {formatAssignmentScope(e.scope, locations)} - {#if e.scope.subject}
{e.scope.subject}
{/if} + {#if e.scope.subject}
{e.scope.subject}
{/if} {#if e.decisionId}
- {e.decisionId} + {e.decisionId} - Check this + {#if linksPeople}Check this{/if}
{/if} {e.grantingRole ?? '—'} {e.summary} -
+
{#if e.before !== null || e.after !== null}{e.before ?? '∅'} → {e.after ?? '∅'} ·{/if} {e.requestId}
@@ -172,7 +175,21 @@ font-size: 11.5px; } .detail { - max-width: 420px; + max-width: 320px; + } + .where .clip { + max-width: 200px; + } + .decision .clip { + max-width: 110px; + } + .detail .clip { + max-width: 300px; + } + .clip { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .decision { display: flex; From 96ad1ba7bd17c1ded85226c9afa75f4470008411 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 11:22:19 +0000 Subject: [PATCH 038/106] feat(server): list people and role holdings for administrators Add GET /api/admin/people, which needs user.manage everywhere, and GET /api/admin/roles, which needs role.assign everywhere. People carry sign-in facts only: password presence, SSO groups, live session count and last sign-in. Role holdings list person assignments and SSO group mappings. --- .../administrator_lifecycle.json | 10 +- clients/typescript/src/schema.d.ts | 131 +++++++++++++++++- .../authentication/password_sessions.rs | 7 +- .../api/_helpers/resources/administrators.rs | 30 +++- .../sqlbuild-orchestration/src/api/models.rs | 2 + .../src/authorization/main/list_people.rs | 72 ++++++++++ .../authorization/main/list_role_holdings.rs | 51 +++++++ .../src/authorization/main/mod.rs | 2 + .../src/credentials/models.rs | 42 ++++++ .../tests/api/person_sessions/test_types.rs | 1 + .../tests/api/test_person_sessions.rs | 58 ++++++++ 11 files changed, 398 insertions(+), 8 deletions(-) create mode 100644 crates/sqlbuild-orchestration/src/authorization/main/list_people.rs create mode 100644 crates/sqlbuild-orchestration/src/authorization/main/list_role_holdings.rs diff --git a/benchmarks/duckshop/edge_cases/orchestration/administrator_lifecycle.json b/benchmarks/duckshop/edge_cases/orchestration/administrator_lifecycle.json index 8116485af0..da57815e82 100644 --- a/benchmarks/duckshop/edge_cases/orchestration/administrator_lifecycle.json +++ b/benchmarks/duckshop/edge_cases/orchestration/administrator_lifecycle.json @@ -12,11 +12,11 @@ {"role": "developer", "allowed": true} ], "session_roles": [ - {"role": null, "expected_metadata": 403, "expected_connection": 403, "expected_job": 403, "expected_sql": 403}, - {"role": "viewer", "expected_metadata": 200, "expected_connection": 403, "expected_job": 403, "expected_sql": 403}, - {"role": "operator", "expected_metadata": 200, "expected_connection": 403, "expected_job": 200, "expected_sql": 403}, - {"role": "developer", "expected_metadata": 200, "expected_connection": 200, "expected_job": 200, "expected_sql": 200}, - {"role": "admin", "expected_metadata": 200, "expected_connection": 200, "expected_job": 200, "expected_sql": 200} + {"role": null, "expected_metadata": 403, "expected_connection": 403, "expected_job": 403, "expected_sql": 403, "expected_admin": 403}, + {"role": "viewer", "expected_metadata": 200, "expected_connection": 403, "expected_job": 403, "expected_sql": 403, "expected_admin": 403}, + {"role": "operator", "expected_metadata": 200, "expected_connection": 403, "expected_job": 200, "expected_sql": 403, "expected_admin": 403}, + {"role": "developer", "expected_metadata": 200, "expected_connection": 200, "expected_job": 200, "expected_sql": 200, "expected_admin": 403}, + {"role": "admin", "expected_metadata": 200, "expected_connection": 200, "expected_job": 200, "expected_sql": 200, "expected_admin": 200} ], "job_settings": [ {"full_refresh": false}, diff --git a/clients/typescript/src/schema.d.ts b/clients/typescript/src/schema.d.ts index acc45842d1..1d25acfe23 100644 --- a/clients/typescript/src/schema.d.ts +++ b/clients/typescript/src/schema.d.ts @@ -1,5 +1,21 @@ // Generated from sqb server --openapi. Run npm run generate; do not edit. export interface paths { + "/api/admin/people": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["listPeople"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/admin/people/{principal_id}": { parameters: { query?: never; @@ -23,7 +39,7 @@ export interface paths { path?: never; cookie?: never; }; - get?: never; + get: operations["listRoleHoldings"]; put?: never; post: operations["assignBuiltinRole"]; delete?: never; @@ -1197,6 +1213,14 @@ export interface components { source_event_id: string; version_hash: string; }; + /** @description A role every member of an SSO group holds. */ + GroupRole: { + group_name: string; + /** Format: uuid */ + location_id?: string | null; + role_key: string; + target?: string | null; + }; HeartbeatResponse: { cancel_requested: boolean; }; @@ -1492,6 +1516,38 @@ export interface components { /** Format: uuid */ principal_id: string; }; + /** @description A role assigned to one person, everywhere or on one location and target. */ + PersonRole: { + /** Format: uuid */ + assignment_id: string; + /** Format: uuid */ + location_id?: string | null; + name: string; + /** Format: uuid */ + principal_id: string; + role_key: string; + target?: string | null; + }; + /** @description A person as administrators see them: how they sign in, their SSO groups and live sessions. */ + PersonSummary: { + /** + * Format: int64 + * @description Unrevoked, unexpired browser and CLI sessions. + */ + active_sessions: number; + /** Format: date-time */ + created_at: string; + /** Format: date-time */ + disabled_at?: string | null; + /** @description Groups the identity provider reported at the person's last SSO sign-in. */ + groups: string[]; + has_password: boolean; + /** Format: date-time */ + last_signed_in_at?: string | null; + name: string; + /** Format: uuid */ + principal_id: string; + }; /** @description Observed physical evidence is not a live warehouse-existence guarantee. */ PhysicalEvidence: { observation: components["schemas"]["CatalogObservation"]; @@ -1580,6 +1636,11 @@ export interface components { role: components["schemas"]["RoleKey"]; target?: string | null; }; + /** @description Who holds which role where: assigned to people here, or mapped to SSO groups in server config. */ + RoleHoldings: { + assignments: components["schemas"]["PersonRole"][]; + group_roles: components["schemas"]["GroupRole"][]; + }; /** @description A built-in role by name, or a custom role a location publishes. */ RoleKey: components["schemas"]["BuiltinRole"] | string; /** @description This is a server-owned instruction, not an arbitrary status supplied by a worker. */ @@ -2170,6 +2231,40 @@ export interface components { } export type $defs = Record; export interface operations { + listPeople: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PersonSummary"][]; + }; + }; + /** @description Session rejected */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + }; + }; changeAdministrator: { parameters: { query?: never; @@ -2214,6 +2309,40 @@ export interface operations { }; }; }; + listRoleHoldings: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RoleHoldings"]; + }; + }; + /** @description Session rejected */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + }; + }; assignBuiltinRole: { parameters: { query?: never; diff --git a/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs b/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs index 5bce2d5b43..f259c54582 100644 --- a/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs +++ b/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs @@ -65,9 +65,14 @@ pub(crate) fn person_routes() -> Router { "/api/admin/people/{principal_id}", post(crate::api::_helpers::resources::administrators::change), ) + .route( + "/api/admin/people", + get(crate::api::_helpers::resources::administrators::people), + ) .route( "/api/admin/roles", - post(crate::api::_helpers::resources::administrators::assign), + post(crate::api::_helpers::resources::administrators::assign) + .get(crate::api::_helpers::resources::administrators::holdings), ) .route( "/api/control/runs", diff --git a/crates/sqlbuild-orchestration/src/api/_helpers/resources/administrators.rs b/crates/sqlbuild-orchestration/src/api/_helpers/resources/administrators.rs index 89fbada6a9..4031c8df16 100644 --- a/crates/sqlbuild-orchestration/src/api/_helpers/resources/administrators.rs +++ b/crates/sqlbuild-orchestration/src/api/_helpers/resources/administrators.rs @@ -8,8 +8,12 @@ use crate::api::errors::ApiError; use crate::api::models::MutationResponse; use crate::authorization::main::assign_builtin_role::assign_builtin_role; use crate::authorization::main::change_administrator::change_administrator; +use crate::authorization::main::list_people::list_people; +use crate::authorization::main::list_role_holdings::list_role_holdings; use crate::credentials::models::AuthenticatedPerson; -use crate::credentials::models::{AdministratorChange, AuditContext, RoleAssignmentRequest}; +use crate::credentials::models::{ + AdministratorChange, AuditContext, PersonSummary, RoleAssignmentRequest, RoleHoldings, +}; use crate::store::models::ControlStore; #[utoipa::path(post, path = "/api/admin/people/{principal_id}", operation_id = "changeAdministrator", @@ -42,3 +46,27 @@ pub(crate) async fn assign( applied: assign_builtin_role(&store, (&context, principal), &request).await?, })) } + +#[utoipa::path(get, path = "/api/admin/people", operation_id = "listPeople", + responses((status = 200, body = Vec), (status = 401, description = "Session rejected"), + (status = 403, body = crate::api::models::ApiErrorBody)), security(("session" = []), ("cliSession" = [])))] +pub(crate) async fn people( + (State(store), Extension(context)): (State, Extension), + Extension(AuthenticatedPerson(principal, _)): Extension, +) -> Result>, ApiError> { + Ok(Json( + list_people(&store, (&context, principal), chrono::Utc::now()).await?, + )) +} + +#[utoipa::path(get, path = "/api/admin/roles", operation_id = "listRoleHoldings", + responses((status = 200, body = RoleHoldings), (status = 401, description = "Session rejected"), + (status = 403, body = crate::api::models::ApiErrorBody)), security(("session" = []), ("cliSession" = [])))] +pub(crate) async fn holdings( + (State(store), Extension(context)): (State, Extension), + Extension(AuthenticatedPerson(principal, _)): Extension, +) -> Result, ApiError> { + Ok(Json( + list_role_holdings(&store, (&context, principal)).await?, + )) +} diff --git a/crates/sqlbuild-orchestration/src/api/models.rs b/crates/sqlbuild-orchestration/src/api/models.rs index 1a639b5176..a80ef4bcec 100644 --- a/crates/sqlbuild-orchestration/src/api/models.rs +++ b/crates/sqlbuild-orchestration/src/api/models.rs @@ -14,6 +14,8 @@ use crate::api::_helpers::resources::{ #[openapi(paths( administrators::change, administrators::assign, + administrators::people, + administrators::holdings, audit::read, person_operations::request_job, person_operations::cancel_run, diff --git a/crates/sqlbuild-orchestration/src/authorization/main/list_people.rs b/crates/sqlbuild-orchestration/src/authorization/main/list_people.rs new file mode 100644 index 0000000000..db9dfd4f5a --- /dev/null +++ b/crates/sqlbuild-orchestration/src/authorization/main/list_people.rs @@ -0,0 +1,72 @@ +//! Administrators who manage people list every principal with sign-in facts, never secrets. + +use std::collections::BTreeMap; + +use chrono::{DateTime, Utc}; +use serde_json::json; +use uuid::Uuid; + +use crate::authorization::main::begin_authorized::begin_authorized; +use crate::credentials::models::{ + Action, AuditContext, DefaultRole, PermissionScope, PersonSummary, +}; +use crate::store::errors::ControlError; +use crate::store::main::{commit::commit, decode_row::decode_row, query::query}; +use crate::store::models::{ControlStore, Parameter}; +use crate::store::types::Row; + +/// Everyone by name, with sessions still live at `now`; requires `user.manage` everywhere. +pub async fn list_people( + store: &ControlStore, + (context, credentials): (&AuditContext, Uuid), + now: DateTime, +) -> Result, ControlError> { + let (transaction, _) = begin_authorized( + store, + (context, credentials), + &PermissionScope::action(Action::UserManage, None), + DefaultRole::None, + ) + .await?; + let (transaction, people) = query(transaction, + "SELECT p.principal_id, p.name, p.created_at, p.disabled_at, \ + CASE WHEN c.principal_id IS NULL THEN 0 ELSE 1 END AS has_password, \ + (SELECT COUNT(*) FROM sqb_sessions s WHERE s.principal_id=p.principal_id \ + AND s.revoked_at IS NULL AND s.expires_at>$1) AS active_sessions, \ + (SELECT MAX(s.created_at) FROM sqb_sessions s WHERE s.principal_id=p.principal_id) AS last_signed_in_at \ + FROM sqb_principals p LEFT JOIN sqb_password_credentials c ON c.principal_id=p.principal_id ORDER BY p.name", + vec![Parameter::Time(now)]).await?; + let (transaction, memberships) = query( + transaction, + "SELECT principal_id, group_name FROM sqb_person_groups ORDER BY group_name", + Vec::new(), + ) + .await?; + commit(transaction).await?; + let mut groups: BTreeMap> = BTreeMap::new(); + for row in &memberships { + if let (Some(id), Some(group)) = (row["principal_id"].as_str(), row["group_name"].as_str()) + { + groups.entry(id.into()).or_default().push(group.into()); + } + } + people + .into_iter() + .map(|row| person_of(row, &groups)) + .collect() +} + +fn person_of( + mut row: Row, + groups: &BTreeMap>, +) -> Result { + let member_of = row["principal_id"] + .as_str() + .and_then(|id| groups.get(id)) + .cloned() + .unwrap_or_default(); + let has_password = row["has_password"].as_i64() == Some(1); + row.insert("groups".into(), json!(member_of)); + row.insert("has_password".into(), json!(has_password)); + decode_row(row) +} diff --git a/crates/sqlbuild-orchestration/src/authorization/main/list_role_holdings.rs b/crates/sqlbuild-orchestration/src/authorization/main/list_role_holdings.rs new file mode 100644 index 0000000000..28207113a2 --- /dev/null +++ b/crates/sqlbuild-orchestration/src/authorization/main/list_role_holdings.rs @@ -0,0 +1,51 @@ +//! Administrators who assign roles see every assignment and SSO group mapping. + +use uuid::Uuid; + +use crate::authorization::main::begin_authorized::begin_authorized; +use crate::credentials::models::{ + Action, AuditContext, DefaultRole, PermissionScope, RoleHoldings, +}; +use crate::store::errors::ControlError; +use crate::store::main::{commit::commit, decode_row::decode_row, query::query}; +use crate::store::models::ControlStore; + +/// Assignments by person name, then group mappings by group; requires `role.assign` everywhere. +pub async fn list_role_holdings( + store: &ControlStore, + (context, credentials): (&AuditContext, Uuid), +) -> Result { + let (transaction, _) = begin_authorized( + store, + (context, credentials), + &PermissionScope::action(Action::RoleAssign, None), + DefaultRole::None, + ) + .await?; + let (transaction, assignments) = query( + transaction, + "SELECT a.assignment_id, a.principal_id, p.name, a.role_key, a.location_id, a.target \ + FROM sqb_role_assignments a JOIN sqb_principals p ON p.principal_id=a.principal_id \ + ORDER BY p.name, a.role_key, a.location_id, a.target", + Vec::new(), + ) + .await?; + let (transaction, group_roles) = query( + transaction, + "SELECT group_name, role_key, location_id, target FROM sqb_group_roles \ + ORDER BY group_name, role_key, location_id, target", + Vec::new(), + ) + .await?; + commit(transaction).await?; + Ok(RoleHoldings { + assignments: assignments + .into_iter() + .map(decode_row) + .collect::>()?, + group_roles: group_roles + .into_iter() + .map(decode_row) + .collect::>()?, + }) +} diff --git a/crates/sqlbuild-orchestration/src/authorization/main/mod.rs b/crates/sqlbuild-orchestration/src/authorization/main/mod.rs index 66851f5c97..408b92f28b 100644 --- a/crates/sqlbuild-orchestration/src/authorization/main/mod.rs +++ b/crates/sqlbuild-orchestration/src/authorization/main/mod.rs @@ -7,6 +7,8 @@ pub mod change_administrator; pub mod compile_roles; pub(crate) mod deny_authorization; pub mod grant_admin; +pub mod list_people; +pub mod list_role_holdings; pub(crate) mod person_lists_target; pub(crate) mod publish_custom_roles; pub(crate) mod publish_group_roles; diff --git a/crates/sqlbuild-orchestration/src/credentials/models.rs b/crates/sqlbuild-orchestration/src/credentials/models.rs index e9b2ac3978..47d5c42d33 100644 --- a/crates/sqlbuild-orchestration/src/credentials/models.rs +++ b/crates/sqlbuild-orchestration/src/credentials/models.rs @@ -44,6 +44,48 @@ pub struct SecurityAuditEvent { pub actor_name: Option, } +/// A person as administrators see them: how they sign in, their SSO groups and live sessions. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] +pub struct PersonSummary { + pub principal_id: Uuid, + pub name: String, + pub created_at: DateTime, + pub disabled_at: Option>, + pub has_password: bool, + /// Groups the identity provider reported at the person's last SSO sign-in. + pub groups: Vec, + /// Unrevoked, unexpired browser and CLI sessions. + pub active_sessions: i64, + pub last_signed_in_at: Option>, +} + +/// Who holds which role where: assigned to people here, or mapped to SSO groups in server config. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] +pub struct RoleHoldings { + pub assignments: Vec, + pub group_roles: Vec, +} + +/// A role assigned to one person, everywhere or on one location and target. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] +pub struct PersonRole { + pub assignment_id: Uuid, + pub principal_id: Uuid, + pub name: String, + pub role_key: String, + pub location_id: Option, + pub target: Option, +} + +/// A role every member of an SSO group holds. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] +pub struct GroupRole { + pub group_name: String, + pub role_key: String, + pub location_id: Option, + pub target: Option, +} + /// Installation configuration: rows past the horizon stay until every sink and retry lookup is done. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct AuditRetention { diff --git a/crates/sqlbuild-orchestration/tests/api/person_sessions/test_types.rs b/crates/sqlbuild-orchestration/tests/api/person_sessions/test_types.rs index 552889f4b8..ad725dfe56 100644 --- a/crates/sqlbuild-orchestration/tests/api/person_sessions/test_types.rs +++ b/crates/sqlbuild-orchestration/tests/api/person_sessions/test_types.rs @@ -17,4 +17,5 @@ pub(crate) struct SessionRoleCase { pub(crate) expected_connection: u16, pub(crate) expected_job: u16, pub(crate) expected_sql: u16, + pub(crate) expected_admin: u16, } diff --git a/crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs b/crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs index cc253368c7..5ecf59f0d0 100644 --- a/crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs +++ b/crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs @@ -321,6 +321,64 @@ fn given_browser_session_roles_when_work_is_requested_then_scope_revocation_and_ "{read_path}" ); } + for admin_read in ["/api/admin/people", "/api/admin/roles"] { + let listed = request( + &provider.client, + session, + (Method::GET, admin_read), + (&Value::Null, &session.csrf), + ) + .await; + assert_eq!( + listed.status().as_u16(), + role_case.expected_admin, + "{admin_read}" + ); + let listed: Value = listed.json().await.expect("admin read"); + if role_case.expected_admin != StatusCode::OK.as_u16() { + continue; + } + let entries = listed + .as_array() + .or(listed["assignments"].as_array()) + .expect("listed entries"); + let own = entries + .iter() + .find(|entry| entry["principal_id"] == json!(person)) + .expect("the person is listed"); + let mut keys: Vec<&str> = own + .as_object() + .expect("entry") + .keys() + .map(String::as_str) + .collect(); + keys.sort_unstable(); + if listed.is_array() { + assert_eq!( + keys, + [ + "active_sessions", + "created_at", + "disabled_at", + "groups", + "has_password", + "last_signed_in_at", + "name", + "principal_id" + ], + "sign-in facts only, never secrets" + ); + assert!(own["active_sessions"].as_i64() >= Some(1)); + } else { + assert!( + entries + .iter() + .any(|entry| entry["principal_id"] == json!(person) + && entry["role_key"] == "admin"), + "the person's admin assignment is listed" + ); + } + } let jobs: Value = request( &provider.client, session, From 5fa1089a8390e7ca2a8e465ead6352b14654a266 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 11:30:46 +0000 Subject: [PATCH 039/106] feat(ui): wire the people page to the people and role reads The people page lists everyone with their highest role, its source and last sign-in. A person's sign-in source is shown only when the server knows it. Links to the person page and access checker, and the risky actions filter, stay hidden until their reads exist. --- .../tests/api/test_person_sessions.rs | 79 ++++++++++--------- ui/src/lib/api/http-client.ts | 22 +++++- ui/src/lib/api/http-people.test.ts | 38 +++++++++ ui/src/lib/api/http-people.ts | 58 ++++++++++++++ ui/src/lib/api/types/platform.ts | 5 +- ui/src/lib/state/access-directory.svelte.ts | 6 +- ui/src/routes/admin/audit/+page.svelte | 6 +- ui/src/routes/admin/people/+page.svelte | 18 +++-- .../routes/admin/people/[name]/+page.svelte | 2 +- 9 files changed, 178 insertions(+), 56 deletions(-) create mode 100644 ui/src/lib/api/http-people.test.ts create mode 100644 ui/src/lib/api/http-people.ts diff --git a/crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs b/crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs index 5ecf59f0d0..71c844a5d4 100644 --- a/crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs +++ b/crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs @@ -321,6 +321,8 @@ fn given_browser_session_roles_when_work_is_requested_then_scope_revocation_and_ "{read_path}" ); } + let granted = usize::from(role_case.expected_admin == StatusCode::OK.as_u16()); + let mut admin_reads = Vec::new(); for admin_read in ["/api/admin/people", "/api/admin/roles"] { let listed = request( &provider.client, @@ -334,51 +336,50 @@ fn given_browser_session_roles_when_work_is_requested_then_scope_revocation_and_ role_case.expected_admin, "{admin_read}" ); - let listed: Value = listed.json().await.expect("admin read"); - if role_case.expected_admin != StatusCode::OK.as_u16() { - continue; - } - let entries = listed - .as_array() - .or(listed["assignments"].as_array()) - .expect("listed entries"); - let own = entries - .iter() - .find(|entry| entry["principal_id"] == json!(person)) - .expect("the person is listed"); - let mut keys: Vec<&str> = own + admin_reads.push(listed.json::().await.expect("admin read")); + } + let own: Vec<&Value> = admin_reads[0] + .as_array() + .into_iter() + .flatten() + .filter(|entry| entry["principal_id"] == json!(person)) + .collect(); + assert_eq!(own.len(), granted, "an administrator lists the person"); + for entry in own { + let mut keys: Vec<&str> = entry .as_object() - .expect("entry") + .expect("person") .keys() .map(String::as_str) .collect(); keys.sort_unstable(); - if listed.is_array() { - assert_eq!( - keys, - [ - "active_sessions", - "created_at", - "disabled_at", - "groups", - "has_password", - "last_signed_in_at", - "name", - "principal_id" - ], - "sign-in facts only, never secrets" - ); - assert!(own["active_sessions"].as_i64() >= Some(1)); - } else { - assert!( - entries - .iter() - .any(|entry| entry["principal_id"] == json!(person) - && entry["role_key"] == "admin"), - "the person's admin assignment is listed" - ); - } + assert_eq!( + keys, + [ + "active_sessions", + "created_at", + "disabled_at", + "groups", + "has_password", + "last_signed_in_at", + "name", + "principal_id" + ], + "sign-in facts only, never secrets" + ); + assert!(entry["active_sessions"].as_i64() >= Some(1)); } + assert_eq!( + admin_reads[1]["assignments"] + .as_array() + .into_iter() + .flatten() + .filter(|entry| entry["principal_id"] == json!(person) + && entry["role_key"] == "admin") + .count(), + granted, + "an administrator sees the person's admin assignment" + ); let jobs: Value = request( &provider.client, session, diff --git a/ui/src/lib/api/http-client.ts b/ui/src/lib/api/http-client.ts index 401465a6b0..b0d72ac0bc 100644 --- a/ui/src/lib/api/http-client.ts +++ b/ui/src/lib/api/http-client.ts @@ -5,11 +5,12 @@ import { checkDetailOf, toCheck, toCheckEvidence, toCheckSql } from './http-chec import { toCatalogNodes } from './http-catalog'; import { auditMatches, toAuditEntry } from './http-audit'; import { toSnapshotChanges } from './http-changes'; +import { toAccount, toRoleAssignments } from './http-people'; import { jobDetailOf, toJob, toTick, toTrigger, triggerDetailOf } from './http-jobs'; import { RUN_WINDOW, runDetailOf, runRowPage, runsMatching, runTabCountsOf, toRunNode, toRuns } from './http-runs'; import type { ChangeSubscription, ChangedChannel, ChangedSignal, ConnectionState } from './signals'; import type { AuthMode, Bootstrap, Capability, Location, Scope } from './types'; -import type { AuditEntry, AuditQuery } from './types/platform'; +import type { Account, AuditEntry, AuditQuery, RoleAssignment } from './types/platform'; import type { CatalogNode, Check, CheckDetail, CheckEvidence, CheckSql, SnapshotChange } from './types/build'; import type { Job, JobDetail, RerunMode, Run, RunDetail, RunListQuery, RunRowPage, RunRowQuery, RunTabCounts, Trigger, TriggerDetail, TriggerState } from './types/operate'; @@ -41,7 +42,9 @@ const SUPPORTED: ReadonlySet = new Set([ 'getCheckEvidence', 'listCatalogNodes', 'listSnapshotChanges', - 'listAuditEntries' + 'listAuditEntries', + 'listAccounts', + 'listRoleAssignments' ]); /** The server's largest audit page, and how many the audit log reads before it stops looking. */ @@ -278,6 +281,14 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt } return found.slice(0, limit); }, + listAccounts: async (): Promise => (await people()).map(toAccount), + async listRoleAssignments(): Promise { + const { data, error, response } = await http.GET('/api/admin/roles'); + if (response.status === 401) onUnauthorized(); + if (!data) throw new ApiError(errorText(error, response), response.status); + // Group members come from the people read; without it, SSO-derived roles stay unlisted. + return toRoleAssignments(data, await people().catch(() => [])); + }, listChecks: async (scope: Scope): Promise => (await checkResults(scope)).map(toCheck), getCheck: async (scope: Scope, id: string): Promise => checkDetailOf(await checkResults(scope, id)), async getCheckSql(scope: Scope, checkId: string): Promise { @@ -358,6 +369,13 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt } /** The newest result of every check, or one check's results newest first. */ + async function people() { + const { data, error, response } = await http.GET('/api/admin/people'); + if (response.status === 401) onUnauthorized(); + if (!data) throw new ApiError(errorText(error, response), response.status); + return data; + } + async function checkResults(scope: Scope, bindingKey?: string) { const { locationId, target } = parseLocationKey(scope.locationId); const { data, error, response } = bindingKey diff --git a/ui/src/lib/api/http-people.test.ts b/ui/src/lib/api/http-people.test.ts new file mode 100644 index 0000000000..518daeb696 --- /dev/null +++ b/ui/src/lib/api/http-people.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest'; +import { toAccount, toRoleAssignments } from './http-people'; + +const person = (name: string, groups: string[], has_password = false) => ({ + principal_id: `p-${name}`, + name, + created_at: '2026-11-01T00:00:00Z', + disabled_at: null, + has_password, + groups, + active_sessions: 0, + last_signed_in_at: null +}); + +describe('people', () => { + it('expands a group role to its members only, and scopes it to the location key', () => { + const people = [person('ada', ['data-admins']), person('vic', ['analysts']), person('sam', [], true)]; + const assignments = toRoleAssignments( + { + assignments: [{ assignment_id: 'a-1', principal_id: 'p-sam', name: 'sam', role_key: 'viewer', location_id: 'loc-1', target: 'prod' }], + group_roles: [{ group_name: 'data-admins', role_key: 'admin', location_id: null, target: null }] + }, + people + ); + expect(assignments.map((a) => [a.principal.name, a.role, a.source, a.ssoGroup, a.scope.locationId])).toEqual([ + ['sam', 'viewer', 'manual', null, 'loc-1:prod'], + ['ada', 'admin', 'sso', 'data-admins', null] + ]); + }); + + it('reports a sign-in source only when the server knows one', () => { + expect([toAccount(person('sam', [], true)).source, toAccount(person('ada', ['g'])).source, toAccount(person('vic', [])).source]).toEqual([ + 'password', + 'oidc', + null + ]); + }); +}); diff --git a/ui/src/lib/api/http-people.ts b/ui/src/lib/api/http-people.ts new file mode 100644 index 0000000000..9f3af49421 --- /dev/null +++ b/ui/src/lib/api/http-people.ts @@ -0,0 +1,58 @@ +import type { components } from '$control/schema.d.ts'; +import type { Account, RoleAssignment } from './types/platform'; + +type Wire = components['schemas']; + +/** A person's sign-in source as far as the server knows it: a password, or SSO groups. */ +export function toAccount(wire: Wire['PersonSummary']): Account { + return { + kind: 'user', + name: wire.name, + displayName: wire.name, + email: null, + source: wire.has_password ? 'password' : wire.groups.length ? 'oidc' : null, + ssoGroups: wire.groups, + createdAt: wire.created_at, + lastSeenAt: wire.last_signed_in_at ?? null, + disabled: wire.disabled_at != null, + activeSessions: wire.active_sessions + }; +} + +function scopeKey(locationId: string | null | undefined, target: string | null | undefined): string | null { + return locationId ? (target ? `${locationId}:${target}` : locationId) : null; +} + +/** Assignments given here, then one SSO assignment per member of each mapped group. */ +export function toRoleAssignments(holdings: Wire['RoleHoldings'], people: Wire['PersonSummary'][]): RoleAssignment[] { + const person = (name: string) => ({ kind: 'user' as const, name, displayName: name }); + const manual: RoleAssignment[] = holdings.assignments.map((a) => ({ + id: a.assignment_id, + principal: person(a.name), + role: a.role_key, + scope: { locationId: scopeKey(a.location_id, a.target) }, + source: 'manual', + ssoGroup: null, + definedIn: null, + assignedBy: null, + assignedAt: null, + stale: false + })); + const viaGroups: RoleAssignment[] = holdings.group_roles.flatMap((g) => + people + .filter((p) => p.groups.includes(g.group_name)) + .map((p) => ({ + id: `sso:${g.group_name}:${g.role_key}:${scopeKey(g.location_id, g.target) ?? '*'}:${p.principal_id}`, + principal: person(p.name), + role: g.role_key, + scope: { locationId: scopeKey(g.location_id, g.target) }, + source: 'sso' as const, + ssoGroup: g.group_name, + definedIn: null, + assignedBy: null, + assignedAt: null, + stale: false + })) + ); + return [...manual, ...viaGroups]; +} diff --git a/ui/src/lib/api/types/platform.ts b/ui/src/lib/api/types/platform.ts index 92defd33c6..bc1dc8c6c2 100644 --- a/ui/src/lib/api/types/platform.ts +++ b/ui/src/lib/api/types/platform.ts @@ -296,7 +296,8 @@ export type AccountSource = 'oidc' | 'trusted_proxy' | 'password'; export type Account = Principal & { kind: 'user'; email: string | null; - source: AccountSource; + /** Null when the server can't tell, such as a person with no password who hasn't used SSO. */ + source: AccountSource | null; ssoGroups: string[]; createdAt: Iso; lastSeenAt: Iso | null; @@ -369,7 +370,7 @@ export type RoleAssignment = { /** For policy and built-in assignments: where they are set, `path:line`. */ definedIn: string | null; assignedBy: Principal | null; - assignedAt: Iso; + assignedAt: Iso | null; /** The role no longer exists in code; the assignment shows but grants nothing. */ stale: boolean; }; diff --git a/ui/src/lib/state/access-directory.svelte.ts b/ui/src/lib/state/access-directory.svelte.ts index ad6bd39db1..84539ae8fa 100644 --- a/ui/src/lib/state/access-directory.svelte.ts +++ b/ui/src/lib/state/access-directory.svelte.ts @@ -11,10 +11,12 @@ export function createAccessDirectory() { const canAssign = () => app.can('role.assign'); const accounts = createResource((client) => (app.can('user.manage') || canAssign() ? client.listAccounts().catch(() => [] as Account[]) : Promise.resolve([] as Account[]))); const assignments = createResource((client) => (canAssign() ? client.listRoleAssignments() : Promise.resolve([] as RoleAssignment[]))); - const overview = createResource((client) => (app.can('user.manage') || canAssign() ? client.listAccessOverview() : Promise.resolve([] as AccessOverview[]))); + const overview = createResource((client) => + (app.can('user.manage') || canAssign()) && client.supports('listAccessOverview') ? client.listAccessOverview() : Promise.resolve([] as AccessOverview[]) + ); const roles = createResource(async (client) => { const map = new Map(); - if (!canAssign()) return map; + if (!canAssign() || !client.supports('listRoles')) return map; const locations = app.bootstrap?.locations ?? []; const lists = await Promise.all(locations.map((l) => client.listRoles(l.id))); locations.forEach((l, i) => map.set(l.id, lists[i])); diff --git a/ui/src/routes/admin/audit/+page.svelte b/ui/src/routes/admin/audit/+page.svelte index 8b0539a81b..b1412ebbb0 100644 --- a/ui/src/routes/admin/audit/+page.svelte +++ b/ui/src/routes/admin/audit/+page.svelte @@ -33,8 +33,8 @@ const actions = $derived([...new Set((everyone.data ?? []).map((e) => e.action))].sort()); const rows = $derived(entries.data ?? []); const locations = $derived(app.bootstrap?.locations ?? []); - // The person page and the access checker both read people. - const linksPeople = $derived(app.client.supports('listAccounts')); + const linksPeople = $derived(app.client.supports('effectiveAccess')); + const checksDecisions = $derived(app.client.supports('lookupDecision')); @@ -110,7 +110,7 @@
{e.decisionId} - {#if linksPeople}Check this{/if} + {#if checksDecisions}Check this{/if}
{/if} diff --git a/ui/src/routes/admin/people/+page.svelte b/ui/src/routes/admin/people/+page.svelte index 91ad7db394..615f799646 100644 --- a/ui/src/routes/admin/people/+page.svelte +++ b/ui/src/routes/admin/people/+page.svelte @@ -25,6 +25,7 @@ const d = people.directory; const allowed = $derived(app.can('user.manage') || app.can('role.assign')); const locations = $derived(app.bootstrap?.locations ?? []); + const opensPeople = $derived(app.client.supports('effectiveAccess')); const sources: AssignmentSource[] = ['manual', 'sso', 'policy', 'built_in']; @@ -46,7 +47,7 @@

Everyone who has signed in, with what they can do in each location. People sign in with single sign-on, or sqb login on the command line. - Open someone to see their effective access and why. + {#if opensPeople}Open someone to see their effective access and why.{/if}

@@ -54,13 +55,13 @@ ({ value: r, label: r }))} all="Any role" /> ({ value: l.id, label: l.name }))} all="All locations" /> ({ value: s, label: assignmentSourceLabel[s] }))} all="Any source" /> - {/if} {/snippet} {#snippet actions()} - + {#if app.client.supports('explainAccess')}{/if} {/snippet}
@@ -77,13 +78,13 @@ {#each people.shown as a (a.name)} {@const risky = d.riskyOf(a.name)} - goto(personHref(a))}> + opensPeople && goto(personHref(a))}>
- e.stopPropagation()}>{a.displayName} -
{a.name} · {a.source === 'oidc' ? 'SSO' : accountSourceLabel[a.source]}
+ {#if opensPeople} e.stopPropagation()}>{a.displayName}{:else}{a.displayName}{/if} +
{a.name} · {a.source === 'oidc' ? 'SSO' : a.source ? accountSourceLabel[a.source] : 'no password'}
@@ -145,4 +146,7 @@ gap: 4px; color: var(--sb-warning) !important; } + tr.static { + cursor: default; + } diff --git a/ui/src/routes/admin/people/[name]/+page.svelte b/ui/src/routes/admin/people/[name]/+page.svelte index 5bebd8f738..037735ebfa 100644 --- a/ui/src/routes/admin/people/[name]/+page.svelte +++ b/ui/src/routes/admin/people/[name]/+page.svelte @@ -126,7 +126,7 @@ Date: Sun, 8 Nov 2026 11:37:34 +0000 Subject: [PATCH 040/106] fix(server): count no active sessions for a disabled person Disabling a person blocks their sessions without revoking them, so the people read no longer counts those sessions as active. --- .../src/authorization/main/list_people.rs | 2 +- .../src/credentials/models.rs | 2 +- .../tests/api/test_person_live.rs | 27 +++++++++++++++++++ ui/src/lib/api/http-client.ts | 2 +- 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/crates/sqlbuild-orchestration/src/authorization/main/list_people.rs b/crates/sqlbuild-orchestration/src/authorization/main/list_people.rs index db9dfd4f5a..de67a734f9 100644 --- a/crates/sqlbuild-orchestration/src/authorization/main/list_people.rs +++ b/crates/sqlbuild-orchestration/src/authorization/main/list_people.rs @@ -32,7 +32,7 @@ pub async fn list_people( "SELECT p.principal_id, p.name, p.created_at, p.disabled_at, \ CASE WHEN c.principal_id IS NULL THEN 0 ELSE 1 END AS has_password, \ (SELECT COUNT(*) FROM sqb_sessions s WHERE s.principal_id=p.principal_id \ - AND s.revoked_at IS NULL AND s.expires_at>$1) AS active_sessions, \ + AND s.revoked_at IS NULL AND s.expires_at>$1 AND p.disabled_at IS NULL) AS active_sessions, \ (SELECT MAX(s.created_at) FROM sqb_sessions s WHERE s.principal_id=p.principal_id) AS last_signed_in_at \ FROM sqb_principals p LEFT JOIN sqb_password_credentials c ON c.principal_id=p.principal_id ORDER BY p.name", vec![Parameter::Time(now)]).await?; diff --git a/crates/sqlbuild-orchestration/src/credentials/models.rs b/crates/sqlbuild-orchestration/src/credentials/models.rs index 47d5c42d33..cb9cb43fc1 100644 --- a/crates/sqlbuild-orchestration/src/credentials/models.rs +++ b/crates/sqlbuild-orchestration/src/credentials/models.rs @@ -54,7 +54,7 @@ pub struct PersonSummary { pub has_password: bool, /// Groups the identity provider reported at the person's last SSO sign-in. pub groups: Vec, - /// Unrevoked, unexpired browser and CLI sessions. + /// Unrevoked, unexpired browser and CLI sessions; none while the person is disabled. pub active_sessions: i64, pub last_signed_in_at: Option>, } diff --git a/crates/sqlbuild-orchestration/tests/api/test_person_live.rs b/crates/sqlbuild-orchestration/tests/api/test_person_live.rs index b8abf89554..600de855ad 100644 --- a/crates/sqlbuild-orchestration/tests/api/test_person_live.rs +++ b/crates/sqlbuild-orchestration/tests/api/test_person_live.rs @@ -216,6 +216,33 @@ fn given_people_with_different_roles_when_they_bootstrap_and_subscribe_then_only .await .expect("disabled bootstrap") .status(); + let people: Value = client + .get(format!("{}/api/admin/people", api.url)) + .header(header::HOST, "control.example.com") + .header( + header::COOKIE, + format!("__Host-sqb-session={}", roles.administrator.bearer), + ) + .send() + .await + .expect("people") + .json() + .await + .expect("people body"); + let member = people + .as_array() + .into_iter() + .flatten() + .find(|person| person["principal_id"] == json!(roles.member_id)) + .expect("disabled member is listed"); + assert_eq!( + ( + member["disabled_at"].is_string(), + &member["active_sessions"] + ), + (true, &json!(0)), + "a disabled person's sessions no longer count as active" + ); let logout = client .post(format!("{}/api/auth/logout", api.url)) .header(header::HOST, "control.example.com") diff --git a/ui/src/lib/api/http-client.ts b/ui/src/lib/api/http-client.ts index b0d72ac0bc..adb5e0b2f7 100644 --- a/ui/src/lib/api/http-client.ts +++ b/ui/src/lib/api/http-client.ts @@ -368,7 +368,6 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt return data; } - /** The newest result of every check, or one check's results newest first. */ async function people() { const { data, error, response } = await http.GET('/api/admin/people'); if (response.status === 401) onUnauthorized(); @@ -376,6 +375,7 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt return data; } + /** The newest result of every check, or one check's results newest first. */ async function checkResults(scope: Scope, bindingKey?: string) { const { locationId, target } = parseLocationKey(scope.locationId); const { data, error, response } = bindingKey From d27158a1a17cf85326a8f3c6719717ded07d886f Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 10:42:47 +0000 Subject: [PATCH 041/106] fix(contracts): read relation markers as relations in dynamic pivot proofs Newer SQLQuack Snowflake parsers refuse a bare __ref(...) or __source(...) in FROM, so the proof parses the model SQL through the shared markers_as_names helper and renames each dependency marker's table or column node back to the relation its call names. --- .../_helpers/dynamic_pivot.rs | 63 ++++++++++++++++--- .../tests/test_analysis_session.rs | 12 ++++ 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dynamic_pivot.rs b/crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dynamic_pivot.rs index e3de483bf9..23877105c6 100644 --- a/crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dynamic_pivot.rs +++ b/crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dynamic_pivot.rs @@ -7,6 +7,7 @@ use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use serde_json::{Map, Value}; use sqlbuild_core::panics::main::catch_compiler_panic::catch_compiler_panic; use sqlbuild_core::panics::main::native_failure::native_failure; +use sqlbuild_sqltext::sql_scan::main::dialect_lexical_syntax::dialect_lexical_syntax; use sqlquack_syntax::{CallMode, Dialect, Expression, ExpressionWalk, ParseOptions}; use crate::assembly::analysis_session::_helpers::dict_walk::{ @@ -14,15 +15,19 @@ use crate::assembly::analysis_session::_helpers::dict_walk::{ payload, relation_name, render_type, truthy, upper, }; use crate::assembly::analysis_session::constants::{ - ALIAS_AST_KIND, CAST_AST_KIND, DUCKDB_DIALECT, DYNAMIC_VALUE_SOURCE_KINDS, MOTHERDUCK_DIALECT, - PIVOT_AST_KIND, PIVOT_MAX_AST_DEPTH, QUERY_AST_KINDS, SELECT_AST_KIND, - SIMPLIFIED_PIVOT_DIALECTS, SNOWFLAKE_DIALECT, SUPPORTED_PIVOT_DIALECTS, - TYPE_PRESERVING_AGGREGATES, UNKNOWN_NULLABILITY, UNKNOWN_TYPE, + ALIAS_AST_KIND, CAST_AST_KIND, COLUMN_AST_KIND, DEPENDENCY_FUNCTION_NAMES, DUCKDB_DIALECT, + DYNAMIC_VALUE_SOURCE_KINDS, MOTHERDUCK_DIALECT, PIVOT_AST_KIND, PIVOT_MAX_AST_DEPTH, + QUERY_AST_KINDS, SELECT_AST_KIND, SIMPLIFIED_PIVOT_DIALECTS, SNOWFLAKE_DIALECT, + SUPPORTED_PIVOT_DIALECTS, TABLE_AST_KIND, TYPE_PRESERVING_AGGREGATES, UNKNOWN_NULLABILITY, + UNKNOWN_TYPE, }; use crate::assembly::analysis_session::models::{ ColumnFact, ContractProof, DynamicFamily, PivotModel, PivotOutcome, PivotTables, }; use crate::assembly::analysis_session::types::Shapes; +use crate::relation_markers::constants::MARKER_NAME_PREFIX; +use crate::relation_markers::main::markers_as_names::markers_as_names; +use crate::relation_markers::types::MarkerCall; use sqlbuild_core::constants::CTE_AST_KIND; /// What an internal native failure of a pivot proof names. @@ -117,15 +122,16 @@ pub(crate) fn pivot_outcome( .map_err(|reason| native_failure(PIVOT_CONTEXT, &reason)) } -/// The one statement of `sql` as SQLQuack's tagged JSON tree, or why it does not parse. +/// The one statement of `sql` as SQLQuack's tagged JSON tree, marker relations as their names. fn parsed(sql: &str, dialect: &str) -> Result, String> { + let (named, calls) = markers_as_names(sql, &dialect_lexical_syntax(dialect))?; let dialect: Dialect = if dialect == SNOWFLAKE_DIALECT { Dialect::Snowflake } else { Dialect::DuckDB }; let parsed = sqlquack_syntax::parse_statements_with( - sql, + &named, dialect, &ParseOptions::default(), CallMode::Faithful, @@ -147,9 +153,48 @@ fn parsed(sql: &str, dialect: &str) -> Result, String> { PIVOT_MAX_AST_DEPTH + 1 ))); } - serde_json::to_value(statement) - .map(Ok) - .map_err(|error| error.to_string()) + let mut root: Value = serde_json::to_value(statement).map_err(|error| error.to_string())?; + name_marker_tables(&mut root, &calls); + Ok(Ok(root)) +} + +/// Each `__ref`, `__source` or `__seed` marker, a table or column node, renamed to the relation its call names. +fn name_marker_tables(root: &mut Value, calls: &[MarkerCall]) { + let mut stack: Vec<&mut Value> = vec![root]; + while let Some(node) = stack.pop() { + match node { + Value::Object(object) => { + for kind in [TABLE_AST_KIND, COLUMN_AST_KIND] { + if let Some(Value::Object(relation)) = object.get_mut(kind) { + rename_marker(relation.get_mut("name"), calls); + } + } + stack.extend(object.values_mut()); + } + Value::Array(items) => stack.extend(items.iter_mut()), + _ => {} + } + } +} + +/// The marker name in `name`, a plain string or an identifier object, replaced by its call's relation. +fn rename_marker(name: Option<&mut Value>, calls: &[MarkerCall]) { + let text: Option<&mut Value> = match name { + Some(Value::Object(identifier)) => identifier.get_mut("name"), + other => other, + }; + let Some(Value::String(text)) = text else { + return; + }; + let call: Option<&MarkerCall> = text + .strip_prefix(MARKER_NAME_PREFIX) + .and_then(|index| index.parse::().ok()) + .and_then(|index| calls.get(index)); + if let Some((function, relation)) = call + && DEPENDENCY_FUNCTION_NAMES.contains(&function.as_str()) + { + *text = relation.clone(); + } } /// Whether `statement` nests more than `limit` levels, walked iteratively so depth cannot overflow. diff --git a/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_analysis_session.rs b/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_analysis_session.rs index 05dcaad310..4f12a16ea6 100644 --- a/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_analysis_session.rs +++ b/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_analysis_session.rs @@ -458,6 +458,18 @@ fn given_dynamic_pivots_when_proving_then_matches_python_or_defers() { false, ), }, + PivotTestCase { + description: "a Snowflake passthrough reads its relation marker as the relation", + dialect: "snowflake", + sql: "SELECT * FROM __ref(\"status_amounts\")", + family: Some(("status", "amount", "MAX")), + expected_outcome: proven( + &[("customer_id", "INTEGER", "unknown")], + Some("DOUBLE"), + "status_amounts", + false, + ), + }, PivotTestCase { description: "a passthrough redeclaring the upstream families", dialect: "duckdb", From 9b3282ce5156f0efecd51469d7d9872778a91693 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 10:49:59 +0000 Subject: [PATCH 042/106] refactor(analysis): share the statement depth guard between pivot proofs and rule facts Both crates carried the same iterative deeper_than; sql_tree now owns it. The deep pivot test asserts its expected outcome directly. --- .../_helpers/dynamic_pivot.rs | 25 ++++++------------- .../tests/test_analysis_session.rs | 15 ++++++++--- .../src/sql_tree/main/deeper_than.rs | 15 +++++++++++ .../src/sql_tree/main/mod.rs | 1 + .../src/query_facts/_helpers/facts.rs | 13 +--------- 5 files changed, 37 insertions(+), 32 deletions(-) create mode 100644 crates/sqlbuild-analysis/src/sql_tree/main/deeper_than.rs diff --git a/crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dynamic_pivot.rs b/crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dynamic_pivot.rs index 23877105c6..087063fef5 100644 --- a/crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dynamic_pivot.rs +++ b/crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dynamic_pivot.rs @@ -8,7 +8,7 @@ use serde_json::{Map, Value}; use sqlbuild_core::panics::main::catch_compiler_panic::catch_compiler_panic; use sqlbuild_core::panics::main::native_failure::native_failure; use sqlbuild_sqltext::sql_scan::main::dialect_lexical_syntax::dialect_lexical_syntax; -use sqlquack_syntax::{CallMode, Dialect, Expression, ExpressionWalk, ParseOptions}; +use sqlquack_syntax::{CallMode, Dialect, Expression, ParseOptions}; use crate::assembly::analysis_session::_helpers::dict_walk::{ casefold, column_name, dict_list, identifier_name, is_single_wildcard, nested, node_key, @@ -28,6 +28,7 @@ use crate::assembly::analysis_session::types::Shapes; use crate::relation_markers::constants::MARKER_NAME_PREFIX; use crate::relation_markers::main::markers_as_names::markers_as_names; use crate::relation_markers::types::MarkerCall; +use crate::sql_tree::main::deeper_than::deeper_than; use sqlbuild_core::constants::CTE_AST_KIND; /// What an internal native failure of a pivot proof names. @@ -186,29 +187,19 @@ fn rename_marker(name: Option<&mut Value>, calls: &[MarkerCall]) { let Some(Value::String(text)) = text else { return; }; - let call: Option<&MarkerCall> = text + let Some(Ok(index)) = text .strip_prefix(MARKER_NAME_PREFIX) - .and_then(|index| index.parse::().ok()) - .and_then(|index| calls.get(index)); - if let Some((function, relation)) = call + .map(str::parse::) + else { + return; + }; + if let Some((function, relation)) = calls.get(index) && DEPENDENCY_FUNCTION_NAMES.contains(&function.as_str()) { *text = relation.clone(); } } -/// Whether `statement` nests more than `limit` levels, walked iteratively so depth cannot overflow. -fn deeper_than(statement: &Expression, limit: usize) -> bool { - let mut stack: Vec<(&Expression, usize)> = vec![(statement, 1)]; - while let Some((node, depth)) = stack.pop() { - if depth > limit { - return true; - } - stack.extend(node.children().into_iter().map(|child| (child, depth + 1))); - } - false -} - fn proof( root: &Value, dialect: &str, diff --git a/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_analysis_session.rs b/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_analysis_session.rs index 4f12a16ea6..c7e56fe646 100644 --- a/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_analysis_session.rs +++ b/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_analysis_session.rs @@ -803,8 +803,17 @@ fn given_deeply_nested_pivot_input_when_proving_then_the_proof_fails_without_abo let in_session = prove_finished_dynamic_contracts(&session, &request.models).expect("the proofs run"); - let expected = vec![test_case.expected_outcome, PivotOutcome::Absent]; - assert_eq!(standalone, expected, "{}", test_case.description); - assert_eq!(in_session, expected, "{}", test_case.description); + assert_eq!( + standalone, + vec![test_case.expected_outcome.clone(), PivotOutcome::Absent], + "{}", + test_case.description + ); + assert_eq!( + in_session, + vec![test_case.expected_outcome, PivotOutcome::Absent], + "{}", + test_case.description + ); } } diff --git a/crates/sqlbuild-analysis/src/sql_tree/main/deeper_than.rs b/crates/sqlbuild-analysis/src/sql_tree/main/deeper_than.rs new file mode 100644 index 0000000000..30d6a9998d --- /dev/null +++ b/crates/sqlbuild-analysis/src/sql_tree/main/deeper_than.rs @@ -0,0 +1,15 @@ +//! `deeper_than`: whether a parsed statement nests past a depth limit. + +use sqlquack_syntax::{Expression, ExpressionWalk}; + +/// Whether `statement` nests more than `limit` levels, walked iteratively so depth cannot overflow. +pub fn deeper_than(statement: &Expression, limit: usize) -> bool { + let mut stack: Vec<(&Expression, usize)> = vec![(statement, 1)]; + while let Some((node, depth)) = stack.pop() { + if depth > limit { + return true; + } + stack.extend(node.children().into_iter().map(|child| (child, depth + 1))); + } + false +} diff --git a/crates/sqlbuild-analysis/src/sql_tree/main/mod.rs b/crates/sqlbuild-analysis/src/sql_tree/main/mod.rs index 924d435a79..26be427137 100644 --- a/crates/sqlbuild-analysis/src/sql_tree/main/mod.rs +++ b/crates/sqlbuild-analysis/src/sql_tree/main/mod.rs @@ -1,4 +1,5 @@ //! Entry points. +pub mod deeper_than; pub mod shared_dialect; pub mod sql_tree_nodes; diff --git a/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs b/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs index 3b468b18cb..a8d3b8e977 100644 --- a/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs +++ b/crates/sqlbuild-rules/src/query_facts/_helpers/facts.rs @@ -8,6 +8,7 @@ use crate::query_facts::models::{ CallKind, ColumnFact, ComparisonFacts, ComparisonSpan, CteFacts, ExpressionItem, ItemFacts, ModelShape, ParsedFixture, QueryFacts, RelationFacts, SelectFacts, SetFacts, SourceText, }; +use sqlbuild_analysis::sql_tree::main::deeper_than::deeper_than; use sqlbuild_analysis::sql_tree::main::shared_dialect::shared_dialect; use sqlquack_syntax::expressions::names::models::{Identifier, Literal, TableRef}; use sqlquack_syntax::expressions::queries::models::{Select, With}; @@ -46,18 +47,6 @@ fn parse_query(sql: &str, dialect_name: &str) -> Result<(Expression, Dialect), S Ok((statement, dialect)) } -/// Whether `statement` nests deeper than `limit` levels, measured without recursion. -fn deeper_than(statement: &Expression, limit: usize) -> bool { - let mut stack: Vec<(&Expression, usize)> = vec![(statement, 1)]; - while let Some((node, depth)) = stack.pop() { - if depth > limit { - return true; - } - stack.extend(node.children().into_iter().map(|child| (child, depth + 1))); - } - false -} - fn code_tokens(sql: &str, dialect: Dialect) -> Result, String> { let tokens = sqlquack_syntax::tokenize(sql, dialect).map_err(|error| error.to_string())?; Ok(QueryTokens::new(tokens, sql)) From 87b8bd94b9b18d131f8f3787fa4117051100ac33 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 11:05:02 +0000 Subject: [PATCH 043/106] fix(contracts): keep non-dependency markers out of pivot relations A __dbt_ref passthrough named its internal placeholder in the failure reason; it now fails as before. The marker scan reads comments and strings with the dialect the proof parses, so MotherDuck scans as DuckDB. --- .../assembly/analysis_session/_helpers/dict_walk.rs | 4 +++- .../analysis_session/_helpers/dynamic_pivot.rs | 8 ++++---- .../analysis_session/tests/test_analysis_session.rs | 10 ++++++++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dict_walk.rs b/crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dict_walk.rs index 3daa93527a..9602a08aab 100644 --- a/crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dict_walk.rs +++ b/crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dict_walk.rs @@ -6,6 +6,7 @@ use polyglot_sql::{ComplexityGuardOptions, DataType, Dialect, Expression, ParseO use serde_json::{Map, Value, json}; use crate::assembly::analysis_session::models::ProjectionFacts; +use crate::relation_markers::constants::MARKER_NAME_PREFIX; use crate::assembly::analysis_session::constants::{ ALIAS_AST_KIND, ANNOTATED_AST_KIND, ARRAY_FIXTURE_TYPE, ARRAY_PAYLOAD_KIND, CATALOG_TABLE_ARG, @@ -95,7 +96,8 @@ pub(crate) fn relation_name(node: Option<&Value>) -> Option<&str> { let key: &str = node_key(node); let payload: &Map = payload(node)?; if key == TABLE_AST_KIND || key == COLUMN_AST_KIND { - return identifier_name(payload.get("name")); + return identifier_name(payload.get("name")) + .filter(|name| !name.starts_with(MARKER_NAME_PREFIX)); } let called: &str = payload .get("name") diff --git a/crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dynamic_pivot.rs b/crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dynamic_pivot.rs index 087063fef5..76695224b6 100644 --- a/crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dynamic_pivot.rs +++ b/crates/sqlbuild-analysis/src/assembly/analysis_session/_helpers/dynamic_pivot.rs @@ -125,12 +125,12 @@ pub(crate) fn pivot_outcome( /// The one statement of `sql` as SQLQuack's tagged JSON tree, marker relations as their names. fn parsed(sql: &str, dialect: &str) -> Result, String> { - let (named, calls) = markers_as_names(sql, &dialect_lexical_syntax(dialect))?; - let dialect: Dialect = if dialect == SNOWFLAKE_DIALECT { - Dialect::Snowflake + let (lexical_dialect, dialect): (&str, Dialect) = if dialect == SNOWFLAKE_DIALECT { + (SNOWFLAKE_DIALECT, Dialect::Snowflake) } else { - Dialect::DuckDB + (DUCKDB_DIALECT, Dialect::DuckDB) }; + let (named, calls) = markers_as_names(sql, &dialect_lexical_syntax(lexical_dialect))?; let parsed = sqlquack_syntax::parse_statements_with( &named, dialect, diff --git a/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_analysis_session.rs b/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_analysis_session.rs index c7e56fe646..b28cd18944 100644 --- a/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_analysis_session.rs +++ b/crates/sqlbuild-analysis/src/assembly/analysis_session/tests/test_analysis_session.rs @@ -538,6 +538,16 @@ fn given_dynamic_pivots_when_proving_then_matches_python_or_defers() { families exactly", ), }, + PivotTestCase { + description: "a passthrough from a call that names no dependency", + dialect: "duckdb", + sql: "SELECT * FROM __dbt_ref(\"shop\", \"status_amounts\")", + family: Some(("status", "amount", "MAX")), + expected_outcome: failed( + "output must be a wildcard projection from one supported dynamic pivot or a \ + proven dynamic-family passthrough", + ), + }, PivotTestCase { description: "no declared families", dialect: "duckdb", From 42579eff51c66613a319083beee0035c6b49e571 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 12:41:48 +0000 Subject: [PATCH 044/106] fix(types): spell ClickHouse Nullable and BINARY as the engine does Under ClickHouse, Nullable(T) is read as a wrapper, so an alias inside it is respelled: Nullable(INT) is Nullable(Int32), as bare INT is. BINARY(n) spells as FixedString(n). Bare BINARY, and a Nullable that wraps another Nullable or a nested type, are refused as the engine refuses them. --- .../src/type_system/_helpers/spelling.rs | 10 +++- .../src/type_system/_helpers/type_reader.rs | 22 +++++++- .../type_system/tests/test_normalize_type.rs | 52 +++++++++++++++++++ 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/crates/sqlbuild-analysis/src/type_system/_helpers/spelling.rs b/crates/sqlbuild-analysis/src/type_system/_helpers/spelling.rs index 87235d9225..cf0b2056fa 100644 --- a/crates/sqlbuild-analysis/src/type_system/_helpers/spelling.rs +++ b/crates/sqlbuild-analysis/src/type_system/_helpers/spelling.rs @@ -206,7 +206,7 @@ impl Speller<'_> { let number = |key: &str| tree.get(key).and_then(Value::as_u64); let length: Option = number("length"); match tag { - "text_with_length" if dialect == ClickHouse => self.write("Nullable(String)"), + "text_with_length" if dialect == ClickHouse => self.clickhouse("String", map_key), "char" | "var_char" | "string" | "text" if dialect == ClickHouse => { self.write("String"); } @@ -243,6 +243,7 @@ impl Speller<'_> { "binary" => match dialect { Postgres => self.sized("BYTEA", length), DuckDb | Sqlite => self.sized("BLOB", length), + ClickHouse => self.sized("FixedString", length), _ => self.sized("BINARY", length), }, "var_binary" => match dialect { @@ -257,7 +258,7 @@ impl Speller<'_> { Tsql | DuckDb => self.write("VARBINARY"), BigQuery => self.write("BYTES"), Databricks => self.write("BINARY"), - ClickHouse => self.write("Nullable(String)"), + ClickHouse => self.clickhouse("String", map_key), _ => self.write("BLOB"), }, "bit" => match dialect { @@ -335,6 +336,11 @@ impl Speller<'_> { let dialect: SpellingDialect = self.dialect; let number = |key: &str| tree.get(key).and_then(Value::as_u64); match tag { + "nullable" => { + self.write("Nullable("); + self.data_type(tree.get("inner_type").unwrap_or(&Value::Null), true); + self.write(")"); + } "array" => { let element: &Value = tree.get("element_type").unwrap_or(&Value::Null); match dialect { diff --git a/crates/sqlbuild-analysis/src/type_system/_helpers/type_reader.rs b/crates/sqlbuild-analysis/src/type_system/_helpers/type_reader.rs index b95d3108bf..4ee2951ea5 100644 --- a/crates/sqlbuild-analysis/src/type_system/_helpers/type_reader.rs +++ b/crates/sqlbuild-analysis/src/type_system/_helpers/type_reader.rs @@ -228,6 +228,7 @@ impl Reader<'_> { self.expect_punct(close)?; Ok(json!({"data_type": "array", "element_type": element})) } + "NULLABLE" if self.dialect == SpellingDialect::ClickHouse => self.nullable(), "MAP" => { let close: char = self.open_nested()?; let key: Value = self.data_type()?; @@ -269,6 +270,19 @@ impl Reader<'_> { } } + /// ClickHouse's `Nullable(T)`, which wraps one scalar type: not another Nullable or a nested type. + fn nullable(&mut self) -> Result { + self.expect_punct('(')?; + let inner: Value = self.data_type()?; + self.expect_punct(')')?; + match inner.get("data_type").and_then(Value::as_str) { + Some(tag @ ("nullable" | "array" | "map" | "struct")) => { + Err(format!("Nullable cannot wrap the {tag} type")) + } + _ => Ok(json!({"data_type": "nullable", "inner_type": inner})), + } + } + /// A time or timestamp; `zoned` when the bare type has a time zone, as in BigQuery. fn time_type(&mut self, tag: &str, zoned: bool) -> Result { let precision: Option = self.length()?; @@ -315,7 +329,13 @@ impl Reader<'_> { } "CHAR" | "CHARACTER" => tagged("char", self.text_length(Self::sized_length)?), "STRING" => tagged("string", self.text_length(Self::length)?), - "BINARY" => tagged("binary", self.length()?), + "BINARY" => { + let length: Option = self.length()?; + if length.is_none() && self.dialect == SpellingDialect::ClickHouse { + return Err("ClickHouse's BINARY needs a length".to_owned()); + } + tagged("binary", length) + } "GEOGRAPHY" if self.single_geography() => { if !self.max_length() { self.length()?; diff --git a/crates/sqlbuild-analysis/src/type_system/tests/test_normalize_type.rs b/crates/sqlbuild-analysis/src/type_system/tests/test_normalize_type.rs index 278b6c7657..3e1f843a2a 100644 --- a/crates/sqlbuild-analysis/src/type_system/tests/test_normalize_type.rs +++ b/crates/sqlbuild-analysis/src/type_system/tests/test_normalize_type.rs @@ -112,6 +112,58 @@ fn given_type_strings_when_normalizing_then_python_normalization_is_returned() { "TUPLE(AINT64,BLOWCARDINALITY(NULLABLE(STRING))) | other | - | - | - | -", ), }, + NormalizeTypeTestCase { + description: "ClickHouse respells an alias inside Nullable as the bare alias", + type_sql: "NULLABLE(INT)", + dialect: "clickhouse", + expected_normalization: Ok("NULLABLE(INT32) | other | - | - | - | -"), + }, + NormalizeTypeTestCase { + description: "ClickHouse respells a Nullable alias inside an array", + type_sql: "ARRAY(NULLABLE(INT))", + dialect: "clickhouse", + expected_normalization: Ok("ARRAY(NULLABLE(INT32)) | other | - | - | - | -"), + }, + NormalizeTypeTestCase { + description: "ClickHouse respells a Nullable timestamp", + type_sql: "NULLABLE(TIMESTAMP)", + dialect: "clickhouse", + expected_normalization: Ok("NULLABLE(DATETIME) | other | - | - | - | -"), + }, + NormalizeTypeTestCase { + description: "ClickHouse respells a Nullable boolean", + type_sql: "NULLABLE(BOOLEAN)", + dialect: "clickhouse", + expected_normalization: Ok("NULLABLE(BOOL) | other | - | - | - | -"), + }, + NormalizeTypeTestCase { + description: "ClickHouse reads a sized BINARY as FixedString", + type_sql: "BINARY(5)", + dialect: "clickhouse", + expected_normalization: Ok("FIXEDSTRING(5) | other | - | - | - | -"), + }, + NormalizeTypeTestCase { + description: "ClickHouse refuses a BINARY without a length", + type_sql: "BINARY", + dialect: "clickhouse", + expected_normalization: Ok( + "BINARY | other | - | - | - | ClickHouse's BINARY needs a length", + ), + }, + NormalizeTypeTestCase { + description: "ClickHouse refuses a Nullable inside a Nullable", + type_sql: "NULLABLE(NULLABLE(INT))", + dialect: "clickhouse", + expected_normalization: Ok( + "NULLABLE(NULLABLE(INT)) | other | - | - | - | Nullable cannot wrap the nullable type", + ), + }, + NormalizeTypeTestCase { + description: "ClickHouse reads a bare alias with its Polyglot nullable spelling", + type_sql: "INT", + dialect: "clickhouse", + expected_normalization: Ok("NULLABLE(INT32) | integer | - | - | - | -"), + }, NormalizeTypeTestCase { description: "an enum member outside ClickHouse falls back to the text", type_sql: "ENUM8('a' = 1)", From 5494e247a73bd663b588948f42a5abae7229f0f1 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 13:28:38 +0000 Subject: [PATCH 045/106] feat(server): let people read a target's location health Add GET /api/control/locations/{location_id}/targets/{target}/health behind the same listing gate as jobs. It returns the target's newest health observations and instance check-ins, shared with the machine read through one helper. --- clients/typescript/src/schema.d.ts | 56 ++++++++++++++++- .../authentication/password_sessions.rs | 4 ++ .../api/_helpers/resources/person_reads.rs | 24 ++++++++ .../sqlbuild-orchestration/src/api/models.rs | 1 + .../src/platform/_helpers/mod.rs | 1 + .../src/platform/_helpers/recent_health.rs | 60 +++++++++++++++++++ .../src/platform/main/location_health.rs | 54 ++--------------- .../src/platform/main/mod.rs | 1 + .../main/read_person_location_health.rs | 33 ++++++++++ .../tests/api/test_location_health.rs | 33 +++++++++- .../tests/api/test_person_sessions.rs | 1 + 11 files changed, 217 insertions(+), 51 deletions(-) create mode 100644 crates/sqlbuild-orchestration/src/platform/_helpers/recent_health.rs create mode 100644 crates/sqlbuild-orchestration/src/platform/main/read_person_location_health.rs diff --git a/clients/typescript/src/schema.d.ts b/clients/typescript/src/schema.d.ts index 1d25acfe23..4aba6e0688 100644 --- a/clients/typescript/src/schema.d.ts +++ b/clients/typescript/src/schema.d.ts @@ -370,6 +370,22 @@ export interface paths { patch?: never; trace?: never; }; + "/api/control/locations/{location_id}/targets/{target}/health": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["readPersonLocationHealth"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/control/locations/{location_id}/targets/{target}/jobs": { parameters: { query?: never; @@ -1532,7 +1548,7 @@ export interface components { PersonSummary: { /** * Format: int64 - * @description Unrevoked, unexpired browser and CLI sessions. + * @description Unrevoked, unexpired browser and CLI sessions; none while the person is disabled. */ active_sessions: number; /** Format: date-time */ @@ -3129,6 +3145,44 @@ export interface operations { }; }; }; + readPersonLocationHealth: { + parameters: { + query?: never; + header?: never; + path: { + location_id: string; + target: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description This target's newest health observations and instance check-ins */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LocationHealthObservation"][]; + }; + }; + /** @description Session rejected */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + }; + }; listPersonJobs: { parameters: { query?: never; diff --git a/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs b/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs index f259c54582..bcc76af513 100644 --- a/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs +++ b/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs @@ -126,6 +126,10 @@ pub(crate) fn person_routes() -> Router { "/api/control/locations/{location_id}/targets/{target}/snapshots", get(crate::api::_helpers::resources::person_reads::snapshots), ) + .route( + "/api/control/locations/{location_id}/targets/{target}/health", + get(crate::api::_helpers::resources::person_reads::health), + ) .route( "/api/control/locations/{location_id}/targets/{target}/catalog", get(crate::api::_helpers::resources::person_operations::catalog), diff --git a/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_reads.rs b/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_reads.rs index aa10e0c867..181d5833d1 100644 --- a/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_reads.rs +++ b/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_reads.rs @@ -11,6 +11,8 @@ use crate::credentials::models::{AuditContext, AuthenticatedPerson, DefaultRole} use crate::declarations::main::list_person_jobs::list_person_jobs; use crate::declarations::main::list_person_triggers::list_person_triggers; use crate::declarations::models::{JobRecord, TriggerRecord}; +use crate::platform::main::read_person_location_health::read_person_location_health; +use crate::platform::models::LocationHealthObservation; use crate::runs::main::list_person_run_nodes::list_person_run_nodes; use crate::runs::models::RunNodeRecord; use crate::scheduler::main::list_person_ticks::list_person_ticks; @@ -161,6 +163,28 @@ pub(crate) async fn snapshots( )) } +#[utoipa::path(get, path = "/api/control/locations/{location_id}/targets/{target}/health", operation_id = "readPersonLocationHealth", + params(("location_id" = Uuid, Path), ("target" = String, Path)), + responses((status = 200, body = Vec, description = "This target's newest health observations and instance check-ins"), + (status = 401, description = "Session rejected"), + (status = 403, body = crate::api::models::ApiErrorBody)), security(("session" = []), ("cliSession" = [])))] +pub(crate) async fn health( + (State(store), Extension(context)): (State, Extension), + Extension(default_role): Extension, + Path((location_id, target)): Path<(Uuid, String)>, + Extension(AuthenticatedPerson(principal, _)): Extension, +) -> Result>, ApiError> { + Ok(Json( + read_person_location_health( + &store, + (&context, principal), + (location_id, &target), + default_role, + ) + .await?, + )) +} + fn not_found(message: &str) -> ApiError { ApiError { status: StatusCode::NOT_FOUND, diff --git a/crates/sqlbuild-orchestration/src/api/models.rs b/crates/sqlbuild-orchestration/src/api/models.rs index a80ef4bcec..c28dcbec2f 100644 --- a/crates/sqlbuild-orchestration/src/api/models.rs +++ b/crates/sqlbuild-orchestration/src/api/models.rs @@ -28,6 +28,7 @@ use crate::api::_helpers::resources::{ person_reads::triggers, person_reads::ticks, person_reads::snapshots, + person_reads::health, person_operations::catalog, person_operations::statistics, person_operations::fingerprint, diff --git a/crates/sqlbuild-orchestration/src/platform/_helpers/mod.rs b/crates/sqlbuild-orchestration/src/platform/_helpers/mod.rs index 0c2b80f5ca..791b027cff 100644 --- a/crates/sqlbuild-orchestration/src/platform/_helpers/mod.rs +++ b/crates/sqlbuild-orchestration/src/platform/_helpers/mod.rs @@ -2,6 +2,7 @@ pub(crate) mod connection_tests; pub(crate) mod liveness; pub(crate) mod location_heartbeat; pub(crate) mod observations; +pub(crate) mod recent_health; pub(crate) mod retention; pub(crate) mod watchdog; pub(crate) mod work_client; diff --git a/crates/sqlbuild-orchestration/src/platform/_helpers/recent_health.rs b/crates/sqlbuild-orchestration/src/platform/_helpers/recent_health.rs new file mode 100644 index 0000000000..b050c540b2 --- /dev/null +++ b/crates/sqlbuild-orchestration/src/platform/_helpers/recent_health.rs @@ -0,0 +1,60 @@ +//! The newest health observations of a location: reported events and instance liveness rows. + +use uuid::Uuid; + +use crate::platform::models::LocationHealthObservation; +use crate::store::errors::ControlError; +use crate::store::main::decode_row::decode_row; +use crate::store::main::query::query; +use crate::store::models::{ControlTransaction, Parameter}; + +/// At most 100, newest first, from today's and yesterday's reports plus each instance's last check-in. +pub(crate) async fn recent_health( + transaction: ControlTransaction, + location_id: Uuid, +) -> Result<(ControlTransaction, Vec), ControlError> { + let today = transaction.now.date_naive(); + let yesterday = today - chrono::TimeDelta::days(1); + let (transaction, rows) = query( + transaction, + "SELECT received_at, payload FROM (\ + SELECT * FROM (SELECT occurred_at AS received_at, invocation_sequence, payload FROM recall_events \ + WHERE invocation_id=$1 AND event_type='location_health' ORDER BY invocation_sequence DESC LIMIT 100) current_day \ + UNION ALL \ + SELECT * FROM (SELECT occurred_at AS received_at, invocation_sequence, payload FROM recall_events \ + WHERE invocation_id=$2 AND event_type='location_health' ORDER BY invocation_sequence DESC LIMIT 100) previous_day\ + ) observations ORDER BY received_at DESC, invocation_sequence DESC LIMIT 100", + vec![ + Parameter::Text(format!("location/{location_id}/{today}")), + Parameter::Text(format!("location/{location_id}/{yesterday}")), + ], + ) + .await?; + let mut observations: Vec = rows + .into_iter() + .map(|mut row| { + let report = row + .remove("payload") + .and_then(|mut payload| payload.get_mut("report").map(serde_json::Value::take)) + .ok_or_else(|| ControlError::Invalid("location health report is missing".into()))?; + row.insert("report".into(), report); + decode_row(row) + }) + .collect::>()?; + let (transaction, instances) = query( + transaction, + "SELECT last_seen_at AS received_at, health_json AS report FROM sqb_location_instances \ + WHERE location_id=$1 ORDER BY last_seen_at DESC LIMIT 100", + vec![Parameter::Id(location_id)], + ) + .await?; + observations.extend( + instances + .into_iter() + .map(decode_row) + .collect::, _>>()?, + ); + observations.sort_by_key(|observation| std::cmp::Reverse(observation.received_at)); + observations.truncate(100); + Ok((transaction, observations)) +} diff --git a/crates/sqlbuild-orchestration/src/platform/main/location_health.rs b/crates/sqlbuild-orchestration/src/platform/main/location_health.rs index 977a61e6c4..49fce2a188 100644 --- a/crates/sqlbuild-orchestration/src/platform/main/location_health.rs +++ b/crates/sqlbuild-orchestration/src/platform/main/location_health.rs @@ -2,62 +2,18 @@ use uuid::Uuid; +use crate::platform::_helpers::recent_health::recent_health; use crate::platform::models::LocationHealthObservation; use crate::store::errors::ControlError; -use crate::store::main::begin::begin; -use crate::store::main::commit::commit; -use crate::store::main::decode_row::decode_row; -use crate::store::main::query::query; -use crate::store::models::{ControlStore, Parameter, TransactionMode}; +use crate::store::main::{begin::begin, commit::commit}; +use crate::store::models::{ControlStore, TransactionMode}; pub async fn location_health( store: &ControlStore, location_id: Uuid, ) -> Result, ControlError> { - let transaction = begin(store, TransactionMode::Read).await?; - let today = transaction.now.date_naive(); - let yesterday = today - chrono::TimeDelta::days(1); - let (transaction, rows) = query( - transaction, - "SELECT received_at, payload FROM (\ - SELECT * FROM (SELECT occurred_at AS received_at, invocation_sequence, payload FROM recall_events \ - WHERE invocation_id=$1 AND event_type='location_health' ORDER BY invocation_sequence DESC LIMIT 100) current_day \ - UNION ALL \ - SELECT * FROM (SELECT occurred_at AS received_at, invocation_sequence, payload FROM recall_events \ - WHERE invocation_id=$2 AND event_type='location_health' ORDER BY invocation_sequence DESC LIMIT 100) previous_day\ - ) observations ORDER BY received_at DESC, invocation_sequence DESC LIMIT 100", - vec![ - Parameter::Text(format!("location/{location_id}/{today}")), - Parameter::Text(format!("location/{location_id}/{yesterday}")), - ], - ) - .await?; - let mut observations: Vec = rows - .into_iter() - .map(|mut row| { - let report = row - .remove("payload") - .and_then(|mut payload| payload.get_mut("report").map(serde_json::Value::take)) - .ok_or_else(|| ControlError::Invalid("location health report is missing".into()))?; - row.insert("report".into(), report); - decode_row(row) - }) - .collect::>()?; - let (transaction, instances) = query( - transaction, - "SELECT last_seen_at AS received_at, health_json AS report FROM sqb_location_instances \ - WHERE location_id=$1 ORDER BY last_seen_at DESC LIMIT 100", - vec![Parameter::Id(location_id)], - ) - .await?; - observations.extend( - instances - .into_iter() - .map(decode_row) - .collect::, _>>()?, - ); - observations.sort_by_key(|observation| std::cmp::Reverse(observation.received_at)); - observations.truncate(100); + let (transaction, observations) = + recent_health(begin(store, TransactionMode::Read).await?, location_id).await?; commit(transaction).await?; Ok(observations) } diff --git a/crates/sqlbuild-orchestration/src/platform/main/mod.rs b/crates/sqlbuild-orchestration/src/platform/main/mod.rs index ecf74ceb6f..fe0e22b625 100644 --- a/crates/sqlbuild-orchestration/src/platform/main/mod.rs +++ b/crates/sqlbuild-orchestration/src/platform/main/mod.rs @@ -5,6 +5,7 @@ pub mod location_health; pub mod location_work; pub(crate) mod read_location_work; pub mod read_person_connection_test; +pub mod read_person_location_health; pub(crate) mod read_platform_events; pub mod report_location_health; pub mod report_remote_location_health; diff --git a/crates/sqlbuild-orchestration/src/platform/main/read_person_location_health.rs b/crates/sqlbuild-orchestration/src/platform/main/read_person_location_health.rs new file mode 100644 index 0000000000..b8a559d466 --- /dev/null +++ b/crates/sqlbuild-orchestration/src/platform/main/read_person_location_health.rs @@ -0,0 +1,33 @@ +//! People read a target's health only with view on every current node, like its listings. + +use uuid::Uuid; + +use crate::authorization::main::authorize_listing::authorize_listing; +use crate::credentials::models::{AuditContext, DefaultRole}; +use crate::platform::_helpers::recent_health::recent_health; +use crate::platform::models::LocationHealthObservation; +use crate::store::errors::ControlError; +use crate::store::main::{begin::begin, commit::commit}; +use crate::store::models::{ControlStore, TransactionMode}; + +/// The location's newest observations for this target only, newest first. +pub async fn read_person_location_health( + store: &ControlStore, + (context, credentials): (&AuditContext, Uuid), + (location_id, target): (Uuid, &str), + default_role: DefaultRole, +) -> Result, ControlError> { + let (transaction, _) = authorize_listing( + begin(store, TransactionMode::Write).await?, + (context, credentials), + (location_id, target), + default_role, + ) + .await?; + let (transaction, observations) = recent_health(transaction, location_id).await?; + commit(transaction).await?; + Ok(observations + .into_iter() + .filter(|observation| observation.report.target == target) + .collect()) +} diff --git a/crates/sqlbuild-orchestration/tests/api/test_location_health.rs b/crates/sqlbuild-orchestration/tests/api/test_location_health.rs index db5ebfe4fa..556d5669a9 100644 --- a/crates/sqlbuild-orchestration/tests/api/test_location_health.rs +++ b/crates/sqlbuild-orchestration/tests/api/test_location_health.rs @@ -1,5 +1,7 @@ +use crate::authorization::main::bootstrap_user::bootstrap_user; use crate::credentials::main::tokens::issue_token::issue_token; -use crate::credentials::models::{AuditContext, CredentialScope}; +use crate::credentials::models::{AuditContext, CredentialScope, DefaultRole}; +use crate::platform::main::read_person_location_health::read_person_location_health; use crate::platform::main::report_remote_location_health::report_remote_location_health; use crate::platform::models::{LocationHealth, LocationHealthObservation, LocationHealthReport}; use crate::snapshots::main::publish_snapshot::publish_snapshot; @@ -171,6 +173,35 @@ fn given_scoped_location_observations_when_retried_or_read_then_history_and_auth ); assert_eq!(observations[0].report, connection); assert_eq!(observations[1].report, heartbeat); + let audit = AuditContext { + actor: "test-admin".into(), + granting_role: "admin".into(), + request_id: Uuid::now_v7(), + ip_address: None, + }; + let administrator = bootstrap_user(store, &audit, "orders-admin") + .await + .expect("administrator"); + let person_read = |target: &'static str| { + read_person_location_health( + store, + (&audit, administrator), + (location_id, target), + DefaultRole::None, + ) + }; + assert_eq!( + person_read("preview #1").await.expect("person health"), + observations, + "a person sees the same observations for the reporting target" + ); + assert!( + person_read("another target") + .await + .expect("other target health") + .is_empty(), + "another target's read leaves this target's observations out" + ); let events = read_recall_events(store, 0, 100) .await .expect("read durable events"); diff --git a/crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs b/crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs index 71c844a5d4..d1533549d9 100644 --- a/crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs +++ b/crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs @@ -289,6 +289,7 @@ fn given_browser_session_roles_when_work_is_requested_then_scope_revocation_and_ format!("/api/control/locations/{location}/targets/development/jobs"), format!("/api/control/locations/{location}/targets/development/triggers"), format!("/api/control/locations/{location}/targets/development/snapshots"), + format!("/api/control/locations/{location}/targets/development/health"), format!( "/api/control/locations/{location}/targets/development/triggers/{}/ticks", trigger.trigger_id From fd394083d66124aa318ea1844dc37ae390c1dec2 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 13:40:16 +0000 Subject: [PATCH 046/106] feat(ui): show live location health on the Locations page Health comes from each location's check-ins and observations: no check-in for 90 seconds is broken, a failed compile at or after the connected generation is falling back, and an unhealthy connection is degraded. People without listing view see the health as unknown. Server status, detail links and the deploy policy column appear only when supported. --- fensu.toml | 2 +- ui/src/lib/api/http-client.ts | 24 ++++++++++- ui/src/lib/api/http-locations.test.ts | 33 ++++++++++++++ ui/src/lib/api/http-locations.ts | 62 +++++++++++++++++++++++++++ ui/src/lib/api/types/platform.ts | 6 ++- ui/src/lib/platform/labels.ts | 2 +- ui/src/routes/locations/+page.svelte | 31 ++++++++++---- 7 files changed, 146 insertions(+), 14 deletions(-) create mode 100644 ui/src/lib/api/http-locations.test.ts create mode 100644 ui/src/lib/api/http-locations.ts diff --git a/fensu.toml b/fensu.toml index 7c1f8e7b53..363656d382 100644 --- a/fensu.toml +++ b/fensu.toml @@ -1381,7 +1381,7 @@ paths = ["crates/sqlbuild-orchestration/src/api/_helpers/resources/administrator reason = "Each function is one utoipa-annotated axum route: OpenAPI needs one annotated handler per operation, and each body only checks the caller's scope and calls one store operation. Scope checks and store work live in their shared owners." [[dupes.allowlist]] -paths = ["crates/sqlbuild-orchestration/src/authorization/main/read_security_audit.rs", "crates/sqlbuild-orchestration/src/credentials/_helpers/active_session.rs", "crates/sqlbuild-orchestration/src/declarations/_helpers/reads.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_person_jobs.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_person_triggers.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_triggers.rs", "crates/sqlbuild-orchestration/src/platform/main/read_location_work.rs", "crates/sqlbuild-orchestration/src/runs/main/list_person_runs.rs", "crates/sqlbuild-orchestration/src/runs/main/list_person_run_nodes.rs", "crates/sqlbuild-orchestration/src/runs/main/list_run_nodes.rs", "crates/sqlbuild-orchestration/src/runs/main/list_runs.rs", "crates/sqlbuild-orchestration/src/runs/_helpers/lifecycle/reads.rs", "crates/sqlbuild-orchestration/src/warehouse_history/_helpers/checks.rs", "crates/sqlbuild-orchestration/src/scheduler/main/list_person_ticks.rs", "crates/sqlbuild-orchestration/src/store/main/read_records.rs", "crates/sqlbuild-orchestration/src/platform/main/location_work.rs", "crates/sqlbuild-orchestration/src/scheduler/main/list_ticks.rs", "crates/sqlbuild-orchestration/src/sensors/_helpers/attempts.rs", "crates/sqlbuild-orchestration/src/sensors/_helpers/facts.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_current_record.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_snapshot_documents.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_snapshot_nodes.rs", "crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_catalog.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_check.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_checks.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_maintenance.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_microbatch_coverage.rs", "crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_statistics.rs"] +paths = ["crates/sqlbuild-orchestration/src/authorization/main/read_security_audit.rs", "crates/sqlbuild-orchestration/src/credentials/_helpers/active_session.rs", "crates/sqlbuild-orchestration/src/declarations/_helpers/reads.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_person_jobs.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_person_triggers.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_triggers.rs", "crates/sqlbuild-orchestration/src/platform/main/read_location_work.rs", "crates/sqlbuild-orchestration/src/platform/main/read_person_location_health.rs", "crates/sqlbuild-orchestration/src/runs/main/list_person_runs.rs", "crates/sqlbuild-orchestration/src/runs/main/list_person_run_nodes.rs", "crates/sqlbuild-orchestration/src/runs/main/list_run_nodes.rs", "crates/sqlbuild-orchestration/src/runs/main/list_runs.rs", "crates/sqlbuild-orchestration/src/runs/_helpers/lifecycle/reads.rs", "crates/sqlbuild-orchestration/src/warehouse_history/_helpers/checks.rs", "crates/sqlbuild-orchestration/src/scheduler/main/list_person_ticks.rs", "crates/sqlbuild-orchestration/src/store/main/read_records.rs", "crates/sqlbuild-orchestration/src/platform/main/location_work.rs", "crates/sqlbuild-orchestration/src/scheduler/main/list_ticks.rs", "crates/sqlbuild-orchestration/src/sensors/_helpers/attempts.rs", "crates/sqlbuild-orchestration/src/sensors/_helpers/facts.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_current_record.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_snapshot_documents.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_snapshot_nodes.rs", "crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_catalog.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_check.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_checks.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_maintenance.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_microbatch_coverage.rs", "crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_statistics.rs"] reason = "Each is one control-store read with its own SQL and record type. Transactions, person authorization and decoding go through the shared begin_authorized, query_records, read_records and query_bodies helpers; what remains alike is the call shape." [[dupes.allowlist]] diff --git a/ui/src/lib/api/http-client.ts b/ui/src/lib/api/http-client.ts index adb5e0b2f7..b45308f9cb 100644 --- a/ui/src/lib/api/http-client.ts +++ b/ui/src/lib/api/http-client.ts @@ -5,12 +5,13 @@ import { checkDetailOf, toCheck, toCheckEvidence, toCheckSql } from './http-chec import { toCatalogNodes } from './http-catalog'; import { auditMatches, toAuditEntry } from './http-audit'; import { toSnapshotChanges } from './http-changes'; +import { toLocationSummary } from './http-locations'; import { toAccount, toRoleAssignments } from './http-people'; import { jobDetailOf, toJob, toTick, toTrigger, triggerDetailOf } from './http-jobs'; import { RUN_WINDOW, runDetailOf, runRowPage, runsMatching, runTabCountsOf, toRunNode, toRuns } from './http-runs'; import type { ChangeSubscription, ChangedChannel, ChangedSignal, ConnectionState } from './signals'; import type { AuthMode, Bootstrap, Capability, Location, Scope } from './types'; -import type { Account, AuditEntry, AuditQuery, RoleAssignment } from './types/platform'; +import type { Account, AuditEntry, AuditQuery, LocationSummary, RoleAssignment } from './types/platform'; import type { CatalogNode, Check, CheckDetail, CheckEvidence, CheckSql, SnapshotChange } from './types/build'; import type { Job, JobDetail, RerunMode, Run, RunDetail, RunListQuery, RunRowPage, RunRowQuery, RunTabCounts, Trigger, TriggerDetail, TriggerState } from './types/operate'; @@ -44,7 +45,8 @@ const SUPPORTED: ReadonlySet = new Set([ 'listSnapshotChanges', 'listAuditEntries', 'listAccounts', - 'listRoleAssignments' + 'listRoleAssignments', + 'listLocations' ]); /** The server's largest audit page, and how many the audit log reads before it stops looking. */ @@ -281,6 +283,24 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt } return found.slice(0, limit); }, + /** Each location the person sees, with health only where they may list the whole target. */ + async listLocations(): Promise { + const { data, error, response } = await http.GET('/api/control/bootstrap'); + if (response.status === 401) onUnauthorized(); + if (!data) throw new ApiError(errorText(error, response), response.status); + return Promise.all( + toBootstrap(data).locations.map(async (location) => { + const { locationId, target } = parseLocationKey(location.id); + const path = { location_id: locationId, target }; + const [health, snapshots] = await Promise.all([ + http.GET('/api/control/locations/{location_id}/targets/{target}/health', { params: { path } }), + http.GET('/api/control/locations/{location_id}/targets/{target}/snapshots', { params: { path, query: { limit: 1 } } }) + ]); + if (health.response.status === 401 || snapshots.response.status === 401) onUnauthorized(); + return toLocationSummary(location, health.data ?? null, snapshots.data?.[0] ?? null, Date.now()); + }) + ); + }, listAccounts: async (): Promise => (await people()).map(toAccount), async listRoleAssignments(): Promise { const { data, error, response } = await http.GET('/api/admin/roles'); diff --git a/ui/src/lib/api/http-locations.test.ts b/ui/src/lib/api/http-locations.test.ts new file mode 100644 index 0000000000..05d99ab2b8 --- /dev/null +++ b/ui/src/lib/api/http-locations.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest'; +import { connectionTestOf, healthOf } from './http-locations'; + +const now = Date.parse('2026-11-08T12:00:00Z'); +const at = (secondsAgo: number) => new Date(now - secondsAgo * 1000).toISOString(); +type Health = Parameters[0][number]['report']['health']; +const seen = (secondsAgo: number, health: Health, session = 's-1') => ({ + received_at: at(secondsAgo), + report: { event_id: `e-${secondsAgo}`, session_id: session, target: 'prod', health } +}); + +describe('location health', () => { + it('is broken without a check-in in the last 90 seconds, whatever was reported before', () => { + expect(healthOf([seen(91, { kind: 'heartbeat' })], now)).toEqual({ health: 'broken', liveInstances: 0, lastSeenAt: at(91) }); + }); + + it('counts each live instance once', () => { + const observations = [seen(10, { kind: 'heartbeat' }), seen(20, { kind: 'connection', generation: 2, healthy: true }), seen(30, { kind: 'heartbeat' }, 's-2')]; + expect(healthOf(observations, now)).toMatchObject({ health: 'healthy', liveInstances: 2, lastSeenAt: at(10) }); + }); + + it('falls back after a failed load until a newer generation connects', () => { + const failed = seen(20, { kind: 'compile_failed', generation: 3 }); + expect(healthOf([seen(5, { kind: 'heartbeat' }), failed, seen(40, { kind: 'connection', generation: 2, healthy: true })], now).health).toBe('falling_back'); + expect(healthOf([seen(5, { kind: 'connection', generation: 4, healthy: true }), failed], now).health).toBe('healthy'); + }); + + it('needs attention when the latest connection check failed', () => { + const observations = [seen(5, { kind: 'connection', generation: 2, healthy: false }), seen(60, { kind: 'connection', generation: 1, healthy: true })]; + expect(healthOf(observations, now).health).toBe('degraded'); + expect(connectionTestOf(observations)).toMatchObject({ outcome: 'failed', at: at(5) }); + }); +}); diff --git a/ui/src/lib/api/http-locations.ts b/ui/src/lib/api/http-locations.ts new file mode 100644 index 0000000000..d08a1dadb8 --- /dev/null +++ b/ui/src/lib/api/http-locations.ts @@ -0,0 +1,62 @@ +import type { components } from '$control/schema.d.ts'; +import type { Location, LocationHealth } from './types'; +import type { ConnectionTest, LocationSummary } from './types/platform'; + +type Wire = components['schemas']; + +/** Locations report every 30 seconds; the server treats two missed reports as down. */ +const LIVE_WINDOW_MS = 90_000; + +function latestOf(observations: Wire['LocationHealthObservation'][], kind: K) { + return observations.find((o) => o.report.health.kind === kind) as (Wire['LocationHealthObservation'] & { report: { health: Extract } }) | undefined; +} + +/** Health from the newest observations: down without a recent check-in, then load failures, then connection failures. */ +export function healthOf(observations: Wire['LocationHealthObservation'][], now: number): { health: LocationHealth; liveInstances: number; lastSeenAt: string | null } { + const live = new Set(observations.filter((o) => now - Date.parse(o.received_at) <= LIVE_WINDOW_MS).map((o) => o.report.session_id)); + const lastSeenAt = observations.reduce((latest, o) => (!latest || o.received_at > latest ? o.received_at : latest), null); + const failed = latestOf(observations, 'compile_failed'); + const connection = latestOf(observations, 'connection'); + const health: LocationHealth = !live.size + ? 'broken' + : failed && (!connection || failed.report.health.generation >= connection.report.health.generation) + ? 'falling_back' + : connection && !connection.report.health.healthy + ? 'degraded' + : 'healthy'; + return { health, liveInstances: live.size, lastSeenAt }; +} + +/** The connection check a location ran after publishing; the server keeps no message or duration. */ +export function connectionTestOf(observations: Wire['LocationHealthObservation'][]): ConnectionTest | null { + const latest = latestOf(observations, 'connection'); + if (!latest) return null; + const ok = latest.report.health.healthy; + return { outcome: ok ? 'ok' : 'failed', at: latest.received_at, durationMs: 0, trigger: 'publish', by: null, message: ok ? 'Connected after publishing' : 'Could not connect after publishing' }; +} + +/** `observations` is null when the person may not list the whole target. */ +export function toLocationSummary(location: Location, observations: Wire['LocationHealthObservation'][] | null, published: Wire['PublishedSnapshot'] | null, now: number): LocationSummary { + const known = observations ? healthOf(observations, now) : { health: null, liveInstances: 0, lastSeenAt: null }; + return { + id: location.id, + name: location.name, + target: location.target, + ...known, + brokenDeployPolicy: null, + failedDeploy: null, + connectionTest: observations ? connectionTestOf(observations) : null, + activeProblems: 0, + snapshot: published + ? { + id: published.snapshot_id, + generation: published.generation, + gitRevision: published.git_revision ?? '', + gitDirty: published.git_dirty, + gitAuthor: published.git_author ?? '', + publishedAt: published.published_at, + sqbVersion: published.sqb_version + } + : null + }; +} diff --git a/ui/src/lib/api/types/platform.ts b/ui/src/lib/api/types/platform.ts index bc1dc8c6c2..4a87a54ab7 100644 --- a/ui/src/lib/api/types/platform.ts +++ b/ui/src/lib/api/types/platform.ts @@ -163,7 +163,11 @@ export type LocationDetail = { connectionTest: ConnectionTest | null; }; -export type LocationSummary = Pick & { +export type LocationSummary = Pick & { + /** Null when the server can't report it to this person. */ + health: LocationHealth | null; + /** Null when the server doesn't report the policy. */ + brokenDeployPolicy: BrokenDeployPolicy | null; /** Unresolved platform events of this location. */ activeProblems: number; liveInstances: number; diff --git a/ui/src/lib/platform/labels.ts b/ui/src/lib/platform/labels.ts index 7d29b9b9a9..c6702a11f0 100644 --- a/ui/src/lib/platform/labels.ts +++ b/ui/src/lib/platform/labels.ts @@ -38,7 +38,7 @@ export function healthWords(location: Pick = { diff --git a/ui/src/routes/locations/+page.svelte b/ui/src/routes/locations/+page.svelte index 548b28e2bd..f7cc7c541e 100644 --- a/ui/src/routes/locations/+page.svelte +++ b/ui/src/routes/locations/+page.svelte @@ -19,14 +19,17 @@ let query = $state(''); const all = $derived(locations.data ?? []); const filtered = $derived(all.filter((l) => !query.trim() || l.name.toLowerCase().includes(query.trim().toLowerCase()))); - const unhealthy = $derived(all.filter((l) => l.health !== 'healthy' || l.activeProblems > 0).length); + const unhealthy = $derived(all.filter((l) => (l.health && l.health !== 'healthy') || l.activeProblems > 0).length); + const opensDetail = $derived(app.client.supports('getLocation')); + const showsServer = $derived(app.client.supports('platformHealth')); + const knowsPolicy = $derived(all.some((l) => l.brokenDeployPolicy)); - +{#if showsServer}{/if} Deployed version Warehouse Location processes - If a deploy fails + {#if knowsPolicy}If a deploy fails{/if} {#each filtered as l (l.id)} - goto(`/locations/${l.id}`)}> + opensDetail && goto(`/locations/${l.id}`)}>
- e.stopPropagation()}>{l.name} + {#if opensDetail} e.stopPropagation()}>{l.name}{:else}{l.name}{/if}
{targetWords(l.target)}
- + {#if l.health} + + {:else} + + {/if} {#if l.activeProblems} {/if} @@ -85,13 +92,19 @@
{l.liveInstances === 1 ? 'Running' : l.liveInstances === 0 ? 'Not running' : `${l.liveInstances} running`}
- + {#if l.lastSeenAt}{/if} - {l.brokenDeployPolicy === 'fallback' ? 'Keep running the previous version' : 'Hold scheduled runs until a deploy works'} + {#if knowsPolicy}{l.brokenDeployPolicy === 'fallback' ? 'Keep running the previous version' : l.brokenDeployPolicy ? 'Hold scheduled runs until a deploy works' : '—'}{/if} {:else} - + {/each}
+ + From 8dc27f1bcae7b339db4e59daeaba7be0954aa204 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 13:49:27 +0000 Subject: [PATCH 047/106] fix(server): limit a target's health read to its own observations The person health read filtered by target only after the location's newest 100 observations were taken across all targets, so a busier target could push out another target's compile or connection reports. The target filter now applies before each limit. --- .../src/platform/_helpers/recent_health.rs | 36 +++++++++++++----- .../src/platform/main/location_health.rs | 8 +++- .../main/read_person_location_health.rs | 7 +--- .../tests/api/test_location_health.rs | 38 +++++++++++++++++++ fensu.toml | 2 +- 5 files changed, 73 insertions(+), 18 deletions(-) diff --git a/crates/sqlbuild-orchestration/src/platform/_helpers/recent_health.rs b/crates/sqlbuild-orchestration/src/platform/_helpers/recent_health.rs index b050c540b2..40e68baa26 100644 --- a/crates/sqlbuild-orchestration/src/platform/_helpers/recent_health.rs +++ b/crates/sqlbuild-orchestration/src/platform/_helpers/recent_health.rs @@ -8,26 +8,38 @@ use crate::store::main::decode_row::decode_row; use crate::store::main::query::query; use crate::store::models::{ControlTransaction, Parameter}; -/// At most 100, newest first, from today's and yesterday's reports plus each instance's last check-in. +/// At most 100, newest first, from today's and yesterday's reports plus each instance's last check-in, of one target when given. pub(crate) async fn recent_health( transaction: ControlTransaction, location_id: Uuid, + target: Option<&str>, ) -> Result<(ControlTransaction, Vec), ControlError> { let today = transaction.now.date_naive(); let yesterday = today - chrono::TimeDelta::days(1); + let (event_target, instance_target) = match target { + Some(_) => (" AND payload->'report'->>'target'=$3", " AND target=$2"), + None => ("", ""), + }; + let target_parameter: Vec = target + .map(|target| Parameter::Text(target.to_owned())) + .into_iter() + .collect(); let (transaction, rows) = query( transaction, - "SELECT received_at, payload FROM (\ + &format!("SELECT received_at, payload FROM (\ SELECT * FROM (SELECT occurred_at AS received_at, invocation_sequence, payload FROM recall_events \ - WHERE invocation_id=$1 AND event_type='location_health' ORDER BY invocation_sequence DESC LIMIT 100) current_day \ + WHERE invocation_id=$1 AND event_type='location_health'{event_target} ORDER BY invocation_sequence DESC LIMIT 100) current_day \ UNION ALL \ SELECT * FROM (SELECT occurred_at AS received_at, invocation_sequence, payload FROM recall_events \ - WHERE invocation_id=$2 AND event_type='location_health' ORDER BY invocation_sequence DESC LIMIT 100) previous_day\ - ) observations ORDER BY received_at DESC, invocation_sequence DESC LIMIT 100", - vec![ + WHERE invocation_id=$2 AND event_type='location_health'{event_target} ORDER BY invocation_sequence DESC LIMIT 100) previous_day\ + ) observations ORDER BY received_at DESC, invocation_sequence DESC LIMIT 100"), + [ Parameter::Text(format!("location/{location_id}/{today}")), Parameter::Text(format!("location/{location_id}/{yesterday}")), - ], + ] + .into_iter() + .chain(target_parameter.clone()) + .collect(), ) .await?; let mut observations: Vec = rows @@ -43,9 +55,13 @@ pub(crate) async fn recent_health( .collect::>()?; let (transaction, instances) = query( transaction, - "SELECT last_seen_at AS received_at, health_json AS report FROM sqb_location_instances \ - WHERE location_id=$1 ORDER BY last_seen_at DESC LIMIT 100", - vec![Parameter::Id(location_id)], + &format!( + "SELECT last_seen_at AS received_at, health_json AS report FROM sqb_location_instances \ + WHERE location_id=$1{instance_target} ORDER BY last_seen_at DESC LIMIT 100" + ), + std::iter::once(Parameter::Id(location_id)) + .chain(target_parameter) + .collect(), ) .await?; observations.extend( diff --git a/crates/sqlbuild-orchestration/src/platform/main/location_health.rs b/crates/sqlbuild-orchestration/src/platform/main/location_health.rs index 49fce2a188..148e6fb77f 100644 --- a/crates/sqlbuild-orchestration/src/platform/main/location_health.rs +++ b/crates/sqlbuild-orchestration/src/platform/main/location_health.rs @@ -12,8 +12,12 @@ pub async fn location_health( store: &ControlStore, location_id: Uuid, ) -> Result, ControlError> { - let (transaction, observations) = - recent_health(begin(store, TransactionMode::Read).await?, location_id).await?; + let (transaction, observations) = recent_health( + begin(store, TransactionMode::Read).await?, + location_id, + None, + ) + .await?; commit(transaction).await?; Ok(observations) } diff --git a/crates/sqlbuild-orchestration/src/platform/main/read_person_location_health.rs b/crates/sqlbuild-orchestration/src/platform/main/read_person_location_health.rs index b8a559d466..91b9eeab0d 100644 --- a/crates/sqlbuild-orchestration/src/platform/main/read_person_location_health.rs +++ b/crates/sqlbuild-orchestration/src/platform/main/read_person_location_health.rs @@ -24,10 +24,7 @@ pub async fn read_person_location_health( default_role, ) .await?; - let (transaction, observations) = recent_health(transaction, location_id).await?; + let (transaction, observations) = recent_health(transaction, location_id, Some(target)).await?; commit(transaction).await?; - Ok(observations - .into_iter() - .filter(|observation| observation.report.target == target) - .collect()) + Ok(observations) } diff --git a/crates/sqlbuild-orchestration/tests/api/test_location_health.rs b/crates/sqlbuild-orchestration/tests/api/test_location_health.rs index 556d5669a9..928b2759ca 100644 --- a/crates/sqlbuild-orchestration/tests/api/test_location_health.rs +++ b/crates/sqlbuild-orchestration/tests/api/test_location_health.rs @@ -222,6 +222,44 @@ fn given_scoped_location_observations_when_retried_or_read_then_history_and_auth .all(|observation| observation.received_at >= before && observation.received_at <= Utc::now()) ); + let mut busy = snapshot(location_id); + busy.target = "another target".into(); + publish_snapshot(store, &busy) + .await + .expect("busy target generation"); + let busy_heartbeat = LocationHealthReport { + event_id: Uuid::now_v7(), + session_id: Uuid::now_v7(), + target: busy.target.clone(), + health: LocationHealth::Heartbeat {}, + }; + report_remote_location_health((&api.url, &token.bearer), location_id, &busy_heartbeat) + .await + .expect("busy target instance"); + for _ in 0..101 { + let busy_connection = LocationHealthReport { + event_id: Uuid::now_v7(), + health: LocationHealth::Connection { + generation: 1, + healthy: true, + }, + ..busy_heartbeat.clone() + }; + report_remote_location_health( + (&api.url, &token.bearer), + location_id, + &busy_connection, + ) + .await + .expect("busy target observation"); + } + assert_eq!( + person_read("preview #1") + .await + .expect("quiet target health"), + observations, + "a busier target's newer observations do not push out this target's" + ); stop_api(api).await; close_test_store(fixture).await; }); diff --git a/fensu.toml b/fensu.toml index 363656d382..51249679c6 100644 --- a/fensu.toml +++ b/fensu.toml @@ -1381,7 +1381,7 @@ paths = ["crates/sqlbuild-orchestration/src/api/_helpers/resources/administrator reason = "Each function is one utoipa-annotated axum route: OpenAPI needs one annotated handler per operation, and each body only checks the caller's scope and calls one store operation. Scope checks and store work live in their shared owners." [[dupes.allowlist]] -paths = ["crates/sqlbuild-orchestration/src/authorization/main/read_security_audit.rs", "crates/sqlbuild-orchestration/src/credentials/_helpers/active_session.rs", "crates/sqlbuild-orchestration/src/declarations/_helpers/reads.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_person_jobs.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_person_triggers.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_triggers.rs", "crates/sqlbuild-orchestration/src/platform/main/read_location_work.rs", "crates/sqlbuild-orchestration/src/platform/main/read_person_location_health.rs", "crates/sqlbuild-orchestration/src/runs/main/list_person_runs.rs", "crates/sqlbuild-orchestration/src/runs/main/list_person_run_nodes.rs", "crates/sqlbuild-orchestration/src/runs/main/list_run_nodes.rs", "crates/sqlbuild-orchestration/src/runs/main/list_runs.rs", "crates/sqlbuild-orchestration/src/runs/_helpers/lifecycle/reads.rs", "crates/sqlbuild-orchestration/src/warehouse_history/_helpers/checks.rs", "crates/sqlbuild-orchestration/src/scheduler/main/list_person_ticks.rs", "crates/sqlbuild-orchestration/src/store/main/read_records.rs", "crates/sqlbuild-orchestration/src/platform/main/location_work.rs", "crates/sqlbuild-orchestration/src/scheduler/main/list_ticks.rs", "crates/sqlbuild-orchestration/src/sensors/_helpers/attempts.rs", "crates/sqlbuild-orchestration/src/sensors/_helpers/facts.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_current_record.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_snapshot_documents.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_snapshot_nodes.rs", "crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_catalog.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_check.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_checks.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_maintenance.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_microbatch_coverage.rs", "crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_statistics.rs"] +paths = ["crates/sqlbuild-orchestration/src/authorization/main/read_security_audit.rs", "crates/sqlbuild-orchestration/src/credentials/_helpers/active_session.rs", "crates/sqlbuild-orchestration/src/declarations/_helpers/reads.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_person_jobs.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_person_triggers.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_triggers.rs", "crates/sqlbuild-orchestration/src/platform/main/read_location_work.rs", "crates/sqlbuild-orchestration/src/platform/main/read_person_location_health.rs", "crates/sqlbuild-orchestration/src/platform/main/location_health.rs", "crates/sqlbuild-orchestration/src/snapshots/main/current_snapshot_record.rs", "crates/sqlbuild-orchestration/src/runs/main/list_person_runs.rs", "crates/sqlbuild-orchestration/src/runs/main/list_person_run_nodes.rs", "crates/sqlbuild-orchestration/src/runs/main/list_run_nodes.rs", "crates/sqlbuild-orchestration/src/runs/main/list_runs.rs", "crates/sqlbuild-orchestration/src/runs/_helpers/lifecycle/reads.rs", "crates/sqlbuild-orchestration/src/warehouse_history/_helpers/checks.rs", "crates/sqlbuild-orchestration/src/scheduler/main/list_person_ticks.rs", "crates/sqlbuild-orchestration/src/store/main/read_records.rs", "crates/sqlbuild-orchestration/src/platform/main/location_work.rs", "crates/sqlbuild-orchestration/src/scheduler/main/list_ticks.rs", "crates/sqlbuild-orchestration/src/sensors/_helpers/attempts.rs", "crates/sqlbuild-orchestration/src/sensors/_helpers/facts.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_current_record.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_snapshot_documents.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_snapshot_nodes.rs", "crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_catalog.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_check.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_checks.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_maintenance.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_microbatch_coverage.rs", "crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_statistics.rs"] reason = "Each is one control-store read with its own SQL and record type. Transactions, person authorization and decoding go through the shared begin_authorized, query_records, read_records and query_bodies helpers; what remains alike is the call shape." [[dupes.allowlist]] From 1f04a8666e5ac2d4f17eace655f9b42dc305b6fd Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 13:56:08 +0000 Subject: [PATCH 048/106] feat(server): let people read live platform problems Add GET /api/control/platform/problems for the UI's alert banner. It derives problems at read time with the watchdog's thresholds: whether the scheduler has a leader, location targets whose instances all missed two check-ins, and running schedules more than 30 seconds late. Broken locations and late ticks appear only for targets the person lists, and access is checked per target before that target's ticks are read. --- clients/typescript/src/schema.d.ts | 68 ++++++++ .../authentication/password_sessions.rs | 4 + .../api/_helpers/resources/person_reads.rs | 16 +- .../sqlbuild-orchestration/src/api/models.rs | 1 + .../src/platform/main/mod.rs | 1 + .../src/platform/main/read_person_problems.rs | 108 ++++++++++++ .../src/platform/models.rs | 32 ++++ .../tests/platform/test_contracts.rs | 2 + .../tests/platform/test_problems.rs | 154 ++++++++++++++++++ .../tests/platform/test_types.rs | 6 + 10 files changed, 391 insertions(+), 1 deletion(-) create mode 100644 crates/sqlbuild-orchestration/src/platform/main/read_person_problems.rs create mode 100644 crates/sqlbuild-orchestration/tests/platform/test_problems.rs diff --git a/clients/typescript/src/schema.d.ts b/clients/typescript/src/schema.d.ts index 4aba6e0688..34a6de6854 100644 --- a/clients/typescript/src/schema.d.ts +++ b/clients/typescript/src/schema.d.ts @@ -578,6 +578,22 @@ export interface paths { patch?: never; trace?: never; }; + "/api/control/platform/problems": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["readPersonPlatformProblems"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/control/runs": { parameters: { query?: never; @@ -1010,6 +1026,14 @@ export interface components { * @enum {string} */ BatchState: "complete" | "gap" | "replay_required"; + /** @description Every instance of this location target has missed two check-ins. */ + BrokenLocation: { + /** Format: date-time */ + last_seen_at: string; + /** Format: uuid */ + location_id: string; + target: string; + }; /** @enum {string} */ BuiltinRole: "viewer" | "operator" | "developer" | "admin"; /** @description Browser cancellation cannot supply an actor, claim or lifecycle transition. */ @@ -1512,6 +1536,17 @@ export interface components { * @enum {string} */ OutputStream: "stdout" | "stderr"; + /** @description A running schedule whose next tick is more than 30 seconds late. */ + OverdueTick: { + /** Format: uuid */ + location_id: string; + name: string; + /** Format: date-time */ + scheduled_for: string; + target: string; + /** Format: uuid */ + trigger_id: string; + }; PasswordChangeRequest: { current_password: string; new_password: string; @@ -1576,6 +1611,12 @@ export interface components { }; /** @enum {string} */ PlatformEventKind: "heartbeat" | "scheduler_no_leader" | "tick_overdue" | "calendar_horizon_low" | "location_broken" | "launch_failed"; + /** @description Problems derived from live state when read; nothing here is stored. */ + PlatformProblems: { + broken_locations: components["schemas"]["BrokenLocation"][]; + overdue_ticks: components["schemas"]["OverdueTick"][]; + scheduler_leader: boolean; + }; /** @description A changed subject, such as `model:orders` or `role:analyst`, without its content hashes. */ PublishedChange: { change_kind: string; @@ -3689,6 +3730,33 @@ export interface operations { }; }; }; + readPersonPlatformProblems: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Live scheduler state, and broken locations and late ticks of targets the person lists */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PlatformProblems"]; + }; + }; + /** @description Session rejected */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; requestJobRun: { parameters: { query?: never; diff --git a/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs b/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs index bcc76af513..28ba723955 100644 --- a/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs +++ b/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs @@ -130,6 +130,10 @@ pub(crate) fn person_routes() -> Router { "/api/control/locations/{location_id}/targets/{target}/health", get(crate::api::_helpers::resources::person_reads::health), ) + .route( + "/api/control/platform/problems", + get(crate::api::_helpers::resources::person_reads::problems), + ) .route( "/api/control/locations/{location_id}/targets/{target}/catalog", get(crate::api::_helpers::resources::person_operations::catalog), diff --git a/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_reads.rs b/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_reads.rs index 181d5833d1..1b85c7a490 100644 --- a/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_reads.rs +++ b/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_reads.rs @@ -12,7 +12,8 @@ use crate::declarations::main::list_person_jobs::list_person_jobs; use crate::declarations::main::list_person_triggers::list_person_triggers; use crate::declarations::models::{JobRecord, TriggerRecord}; use crate::platform::main::read_person_location_health::read_person_location_health; -use crate::platform::models::LocationHealthObservation; +use crate::platform::main::read_person_problems::read_person_problems; +use crate::platform::models::{LocationHealthObservation, PlatformProblems}; use crate::runs::main::list_person_run_nodes::list_person_run_nodes; use crate::runs::models::RunNodeRecord; use crate::scheduler::main::list_person_ticks::list_person_ticks; @@ -185,6 +186,19 @@ pub(crate) async fn health( )) } +#[utoipa::path(get, path = "/api/control/platform/problems", operation_id = "readPersonPlatformProblems", + responses((status = 200, body = PlatformProblems, description = "Live scheduler state, and broken locations and late ticks of targets the person lists"), + (status = 401, description = "Session rejected")), security(("session" = []), ("cliSession" = [])))] +pub(crate) async fn problems( + State(store): State, + Extension(default_role): Extension, + Extension(AuthenticatedPerson(principal, _)): Extension, +) -> Result, ApiError> { + Ok(Json( + read_person_problems(&store, principal, default_role).await?, + )) +} + fn not_found(message: &str) -> ApiError { ApiError { status: StatusCode::NOT_FOUND, diff --git a/crates/sqlbuild-orchestration/src/api/models.rs b/crates/sqlbuild-orchestration/src/api/models.rs index c28dcbec2f..4868410cae 100644 --- a/crates/sqlbuild-orchestration/src/api/models.rs +++ b/crates/sqlbuild-orchestration/src/api/models.rs @@ -29,6 +29,7 @@ use crate::api::_helpers::resources::{ person_reads::ticks, person_reads::snapshots, person_reads::health, + person_reads::problems, person_operations::catalog, person_operations::statistics, person_operations::fingerprint, diff --git a/crates/sqlbuild-orchestration/src/platform/main/mod.rs b/crates/sqlbuild-orchestration/src/platform/main/mod.rs index fe0e22b625..d1c062ba19 100644 --- a/crates/sqlbuild-orchestration/src/platform/main/mod.rs +++ b/crates/sqlbuild-orchestration/src/platform/main/mod.rs @@ -6,6 +6,7 @@ pub mod location_work; pub(crate) mod read_location_work; pub mod read_person_connection_test; pub mod read_person_location_health; +pub mod read_person_problems; pub(crate) mod read_platform_events; pub mod report_location_health; pub mod report_remote_location_health; diff --git a/crates/sqlbuild-orchestration/src/platform/main/read_person_problems.rs b/crates/sqlbuild-orchestration/src/platform/main/read_person_problems.rs new file mode 100644 index 0000000000..cddec67364 --- /dev/null +++ b/crates/sqlbuild-orchestration/src/platform/main/read_person_problems.rs @@ -0,0 +1,108 @@ +//! People see the scheduler's state, and broken locations and late ticks only for targets they list. + +use std::collections::BTreeSet; + +use chrono::TimeDelta; +use uuid::Uuid; + +use crate::authorization::main::person_lists_target::person_lists_target; +use crate::credentials::models::DefaultRole; +use crate::platform::models::{BrokenLocation, LateTarget, OverdueTick, PlatformProblems}; +use crate::store::errors::ControlError; +use crate::store::main::decode_row::decode_row; +use crate::store::main::query::query; +use crate::store::main::{begin::begin, commit::commit}; +use crate::store::models::{ControlStore, Parameter, TransactionMode}; + +/// Uses the watchdog's thresholds; access is checked per target before each target's ticks are read. +pub async fn read_person_problems( + store: &ControlStore, + principal_id: Uuid, + default_role: DefaultRole, +) -> Result { + let transaction = begin(store, TransactionMode::Read).await?; + let now = transaction.now; + let overdue_before = now - TimeDelta::seconds(30); + let (transaction, leases) = query( + transaction, + "SELECT name FROM sqb_scheduler_lease WHERE name='scheduler' AND expires_at>$1", + vec![Parameter::Time(now)], + ) + .await?; + let (transaction, locations) = query( + transaction, + "SELECT i.location_id, i.target, MAX(i.last_seen_at) AS last_seen_at \ + FROM sqb_location_instances i JOIN sqb_locations l ON l.location_id=i.location_id \ + WHERE l.archived_at IS NULL GROUP BY i.location_id,i.target \ + HAVING MAX(i.last_seen_at)<$1 ORDER BY i.location_id,i.target", + vec![Parameter::Time(now - TimeDelta::seconds(90))], + ) + .await?; + let (transaction, late_targets) = query( + transaction, + "SELECT DISTINCT t.location_id, t.target FROM sqb_triggers t \ + JOIN sqb_trigger_state s ON s.trigger_id=t.trigger_id \ + JOIN sqb_locations l ON l.location_id=t.location_id \ + WHERE t.kind='schedule' AND s.status='running' AND t.removed_in_snapshot_id IS NULL \ + AND l.archived_at IS NULL AND s.next_due_at<$1", + vec![Parameter::Time(overdue_before)], + ) + .await?; + commit(transaction).await?; + let locations: Vec = locations + .into_iter() + .map(decode_row) + .collect::>()?; + let late_targets: BTreeSet<(Uuid, String)> = late_targets + .into_iter() + .map(|row| { + let target: LateTarget = decode_row(row)?; + Ok((target.location_id, target.target)) + }) + .collect::>()?; + let mut listed: BTreeSet<(Uuid, String)> = BTreeSet::new(); + for (location_id, target) in locations + .iter() + .map(|location| (location.location_id, location.target.clone())) + .chain(late_targets.iter().cloned()) + .collect::>() + { + if person_lists_target(store, principal_id, (location_id, &target), default_role).await? { + listed.insert((location_id, target)); + } + } + let mut overdue_ticks: Vec = Vec::new(); + let mut transaction = begin(store, TransactionMode::Read).await?; + for (location_id, target) in late_targets.intersection(&listed) { + let (next, ticks) = query( + transaction, + "SELECT t.trigger_id, t.name, t.location_id, t.target, s.next_due_at AS scheduled_for \ + FROM sqb_triggers t JOIN sqb_trigger_state s ON s.trigger_id=t.trigger_id \ + WHERE t.location_id=$1 AND t.target=$2 AND t.kind='schedule' AND s.status='running' \ + AND t.removed_in_snapshot_id IS NULL AND s.next_due_at<$3 \ + ORDER BY s.next_due_at, t.name LIMIT 100", + vec![ + Parameter::Id(*location_id), + Parameter::Text(target.clone()), + Parameter::Time(overdue_before), + ], + ) + .await?; + transaction = next; + overdue_ticks.extend( + ticks + .into_iter() + .map(decode_row) + .collect::, _>>()?, + ); + } + commit(transaction).await?; + Ok(PlatformProblems { + scheduler_leader: !leases.is_empty(), + broken_locations: locations + .into_iter() + .filter(|location| listed.contains(&(location.location_id, location.target.clone()))) + .collect(), + overdue_ticks, + }) +} diff --git a/crates/sqlbuild-orchestration/src/platform/models.rs b/crates/sqlbuild-orchestration/src/platform/models.rs index 543aaeb420..b6d47ea0c9 100644 --- a/crates/sqlbuild-orchestration/src/platform/models.rs +++ b/crates/sqlbuild-orchestration/src/platform/models.rs @@ -250,3 +250,35 @@ pub struct PlatformEventEnvelope { pub struct PlatformEventPayload { pub platform: PlatformEvent, } + +/// Problems derived from live state when read; nothing here is stored. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] +pub struct PlatformProblems { + pub scheduler_leader: bool, + pub broken_locations: Vec, + pub overdue_ticks: Vec, +} + +/// Every instance of this location target has missed two check-ins. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] +pub struct BrokenLocation { + pub location_id: Uuid, + pub target: String, + pub last_seen_at: DateTime, +} + +/// A running schedule whose next tick is more than 30 seconds late. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] +pub struct OverdueTick { + pub trigger_id: Uuid, + pub name: String, + pub location_id: Uuid, + pub target: String, + pub scheduled_for: DateTime, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct LateTarget { + pub(crate) location_id: Uuid, + pub(crate) target: String, +} diff --git a/crates/sqlbuild-orchestration/tests/platform/test_contracts.rs b/crates/sqlbuild-orchestration/tests/platform/test_contracts.rs index 0f5c53a679..eb9b98bffe 100644 --- a/crates/sqlbuild-orchestration/tests/platform/test_contracts.rs +++ b/crates/sqlbuild-orchestration/tests/platform/test_contracts.rs @@ -2,6 +2,8 @@ #[path = "helpers.rs"] mod helpers; +#[path = "test_problems.rs"] +mod test_problems; #[path = "test_types.rs"] mod test_types; #[path = "test_watchdog.rs"] diff --git a/crates/sqlbuild-orchestration/tests/platform/test_problems.rs b/crates/sqlbuild-orchestration/tests/platform/test_problems.rs new file mode 100644 index 0000000000..62fd90b10a --- /dev/null +++ b/crates/sqlbuild-orchestration/tests/platform/test_problems.rs @@ -0,0 +1,154 @@ +use crate::authorization::main::{ + assign_builtin_role::assign_builtin_role, bootstrap_user::bootstrap_user, +}; +use crate::credentials::main::identities::create_principal::create_principal; +use crate::credentials::models::{AuditContext, BuiltinRole, DefaultRole, RoleAssignmentRequest}; +use crate::platform::main::read_person_problems::read_person_problems; +use crate::platform::main::report_location_health::report_location_health; +use crate::platform::models::{LocationHealth, LocationHealthReport}; +use crate::scheduler::tests::contracts::helpers::acquire_lease; +use crate::snapshots::main::publish_snapshot::publish_snapshot; +use crate::store::main::begin::begin; +use crate::store::main::commit::commit; +use crate::store::main::migrate_store::migrate_store; +use crate::store::main::query::query; +use crate::store::models::{Parameter, TransactionMode}; +use chrono::TimeDelta; +use uuid::Uuid; + +use crate::credentials::tests::location_helpers::create_location; +use crate::platform::tests::helpers::snapshot; +use crate::platform::tests::test_types::ProblemsTestCase; +use crate::store::tests::store_helpers::{close_test_store, open_test_store, run}; +use crate::store::tests::store_test_types::Backend; + +#[test] +fn given_broken_locations_and_late_ticks_when_people_read_problems_then_only_listed_targets_appear() +{ + let test_cases = [ + ProblemsTestCase { + description: "SQLite without a leader", + backend: Backend::Sqlite, + leaders: 0, + }, + ProblemsTestCase { + description: "PostgreSQL with a leader", + backend: Backend::Postgres, + leaders: 1, + }, + ]; + for test_case in test_cases { + run(async { + let Some(fixture) = open_test_store(test_case.backend).await else { + return; + }; + let store = &fixture.store; + migrate_store(store).await.expect("migrate problems store"); + let context = AuditContext { + actor: "control-store-owner".into(), + granting_role: "control-store-owner".into(), + request_id: Uuid::now_v7(), + ip_address: None, + }; + let admin = bootstrap_user(store, &context, "orders-admin") + .await + .expect("administrator"); + let developer = create_principal(store, &context, "orders-developer") + .await + .expect("developer"); + let outsider = create_principal(store, &context, "orders-outsider") + .await + .expect("outsider"); + let mut locations = Vec::new(); + for name in ["orders", "inventory"] { + let location = create_location(store, name, "test-admin") + .await + .expect("location"); + publish_snapshot(store, &snapshot(location)) + .await + .expect("published triggers"); + report_location_health( + store, + location, + &LocationHealthReport { + event_id: Uuid::now_v7(), + session_id: Uuid::now_v7(), + target: "development".into(), + health: LocationHealth::Heartbeat {}, + }, + ) + .await + .expect("instance check-in"); + locations.push(location); + } + let transaction = begin(store, TransactionMode::Write) + .await + .expect("age check-ins"); + let stale = transaction.now - TimeDelta::minutes(5); + let (transaction, _) = query( + transaction, + "UPDATE sqb_location_instances SET last_seen_at=$1", + vec![Parameter::Time(stale)], + ) + .await + .expect("stale check-ins"); + commit(transaction).await.expect("aged check-ins"); + assign_builtin_role( + store, + (&context, admin), + &RoleAssignmentRequest { + principal_id: developer, + role: BuiltinRole::Developer.into(), + location_id: Some(locations[1]), + target: Some("development".into()), + revoke: false, + }, + ) + .await + .expect("developer on inventory only"); + for _ in 0..test_case.leaders { + acquire_lease(store, Uuid::now_v7()) + .await + .expect("acquire lease") + .expect("leader"); + } + let read = |principal: Uuid| read_person_problems(store, principal, DefaultRole::None); + let everything = read(admin).await.expect("administrator problems"); + assert_eq!( + everything.scheduler_leader, + test_case.leaders > 0, + "{}", + test_case.description + ); + assert_eq!(everything.broken_locations.len(), 2); + assert_eq!( + everything.overdue_ticks.len(), + 200, + "each listed target keeps its own 100 late ticks" + ); + let scoped = read(developer).await.expect("developer problems"); + assert_eq!(scoped.scheduler_leader, everything.scheduler_leader); + assert_eq!( + scoped + .broken_locations + .iter() + .map(|location| location.location_id) + .collect::>(), + vec![locations[1]], + "only the listed location is reported broken" + ); + assert_eq!(scoped.overdue_ticks.len(), 100); + assert!( + scoped + .overdue_ticks + .iter() + .all(|tick| tick.location_id == locations[1]), + "another location's late ticks stay hidden" + ); + let hidden = read(outsider).await.expect("outsider problems"); + assert_eq!(hidden.scheduler_leader, everything.scheduler_leader); + assert!(hidden.broken_locations.is_empty() && hidden.overdue_ticks.is_empty()); + close_test_store(fixture).await; + }); + } +} diff --git a/crates/sqlbuild-orchestration/tests/platform/test_types.rs b/crates/sqlbuild-orchestration/tests/platform/test_types.rs index 52ecb92e0c..003c26e766 100644 --- a/crates/sqlbuild-orchestration/tests/platform/test_types.rs +++ b/crates/sqlbuild-orchestration/tests/platform/test_types.rs @@ -51,3 +51,9 @@ pub(crate) struct WebhookState { pub(crate) status: u16, pub(crate) requests: Vec, } + +pub(crate) struct ProblemsTestCase { + pub(crate) description: &'static str, + pub(crate) backend: Backend, + pub(crate) leaders: usize, +} From 8929fae8b131044355038be800640a02e6239b21 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 14:10:31 +0000 Subject: [PATCH 049/106] feat(ui): show live platform problems in the alert banner The banner reads the new problems endpoint every 30 seconds, because missed check-ins and late ticks come from time passing, not change notices. A stopped scheduler and each broken location are critical, and late schedules are grouped per location. The read now names each location and dates a stopped scheduler from its last lease. Links go to the Locations list until location detail pages are supported. --- clients/typescript/src/schema.d.ts | 7 ++ .../src/platform/_helpers/mod.rs | 1 + .../src/platform/_helpers/problems.rs | 97 +++++++++++++++++++ .../src/platform/main/read_person_problems.rs | 94 +++--------------- .../src/platform/models.rs | 18 ++++ .../tests/platform/test_problems.rs | 21 +++- .../tests/platform/test_types.rs | 1 + ui/src/lib/api/http-client.ts | 12 ++- ui/src/lib/api/http-problems.test.ts | 48 +++++++++ ui/src/lib/api/http-problems.ts | 56 +++++++++++ ui/src/lib/api/mock/fixtures.ts | 2 +- .../shell/platform-event-banner.svelte | 8 +- ui/src/lib/platform/labels.ts | 2 +- ui/src/lib/state/app.svelte.ts | 4 + 14 files changed, 284 insertions(+), 87 deletions(-) create mode 100644 crates/sqlbuild-orchestration/src/platform/_helpers/problems.rs create mode 100644 ui/src/lib/api/http-problems.test.ts create mode 100644 ui/src/lib/api/http-problems.ts diff --git a/clients/typescript/src/schema.d.ts b/clients/typescript/src/schema.d.ts index 34a6de6854..4674ad9a07 100644 --- a/clients/typescript/src/schema.d.ts +++ b/clients/typescript/src/schema.d.ts @@ -1032,6 +1032,7 @@ export interface components { last_seen_at: string; /** Format: uuid */ location_id: string; + location_name: string; target: string; }; /** @enum {string} */ @@ -1540,6 +1541,7 @@ export interface components { OverdueTick: { /** Format: uuid */ location_id: string; + location_name: string; name: string; /** Format: date-time */ scheduled_for: string; @@ -1616,6 +1618,11 @@ export interface components { broken_locations: components["schemas"]["BrokenLocation"][]; overdue_ticks: components["schemas"]["OverdueTick"][]; scheduler_leader: boolean; + /** + * Format: date-time + * @description When the last scheduler lease ran out, or will run out; absent until a server first leads. + */ + scheduler_lease_expires_at?: string | null; }; /** @description A changed subject, such as `model:orders` or `role:analyst`, without its content hashes. */ PublishedChange: { diff --git a/crates/sqlbuild-orchestration/src/platform/_helpers/mod.rs b/crates/sqlbuild-orchestration/src/platform/_helpers/mod.rs index 791b027cff..a302fe0aee 100644 --- a/crates/sqlbuild-orchestration/src/platform/_helpers/mod.rs +++ b/crates/sqlbuild-orchestration/src/platform/_helpers/mod.rs @@ -2,6 +2,7 @@ pub(crate) mod connection_tests; pub(crate) mod liveness; pub(crate) mod location_heartbeat; pub(crate) mod observations; +pub(crate) mod problems; pub(crate) mod recent_health; pub(crate) mod retention; pub(crate) mod watchdog; diff --git a/crates/sqlbuild-orchestration/src/platform/_helpers/problems.rs b/crates/sqlbuild-orchestration/src/platform/_helpers/problems.rs new file mode 100644 index 0000000000..f8e57a9574 --- /dev/null +++ b/crates/sqlbuild-orchestration/src/platform/_helpers/problems.rs @@ -0,0 +1,97 @@ +//! Live problem queries, with the watchdog's thresholds; callers decide who may see each target's. + +use chrono::{DateTime, TimeDelta, Utc}; +use uuid::Uuid; + +use crate::platform::models::{LateTarget, OverdueTick, ProblemScan}; +use crate::store::errors::ControlError; +use crate::store::main::decode_row::decode_row; +use crate::store::main::query::query; +use crate::store::main::{begin::begin, commit::commit}; +use crate::store::models::{ControlStore, Parameter, TransactionMode}; + +pub(crate) async fn scan_problems(store: &ControlStore) -> Result { + let transaction = begin(store, TransactionMode::Read).await?; + let now = transaction.now; + let (transaction, leases) = query( + transaction, + "SELECT expires_at FROM sqb_scheduler_lease WHERE name='scheduler'", + vec![], + ) + .await?; + let (transaction, locations) = query( + transaction, + "SELECT i.location_id, l.name AS location_name, i.target, MAX(i.last_seen_at) AS last_seen_at \ + FROM sqb_location_instances i JOIN sqb_locations l ON l.location_id=i.location_id \ + WHERE l.archived_at IS NULL GROUP BY i.location_id,l.name,i.target \ + HAVING MAX(i.last_seen_at)<$1 ORDER BY i.location_id,i.target", + vec![Parameter::Time(now - TimeDelta::seconds(90))], + ) + .await?; + let (transaction, late_targets) = query( + transaction, + "SELECT DISTINCT t.location_id, t.target FROM sqb_triggers t \ + JOIN sqb_trigger_state s ON s.trigger_id=t.trigger_id \ + JOIN sqb_locations l ON l.location_id=t.location_id \ + WHERE t.kind='schedule' AND s.status='running' AND t.removed_in_snapshot_id IS NULL \ + AND l.archived_at IS NULL AND s.next_due_at<$1", + vec![Parameter::Time(overdue_before(now))], + ) + .await?; + commit(transaction).await?; + Ok(ProblemScan { + now, + lease: leases.into_iter().next().map(decode_row).transpose()?, + broken: locations + .into_iter() + .map(decode_row) + .collect::>()?, + late_targets: late_targets + .into_iter() + .map(|row| { + let target: LateTarget = decode_row(row)?; + Ok((target.location_id, target.target)) + }) + .collect::>()?, + }) +} + +/// At most 100 late ticks per target, oldest first. +pub(crate) async fn late_ticks( + store: &ControlStore, + targets: impl IntoIterator, + now: DateTime, +) -> Result, ControlError> { + let mut overdue: Vec = Vec::new(); + let mut transaction = begin(store, TransactionMode::Read).await?; + for (location_id, target) in targets { + let (next, ticks) = query( + transaction, + "SELECT t.trigger_id, t.name, t.location_id, l.name AS location_name, t.target, \ + s.next_due_at AS scheduled_for FROM sqb_triggers t \ + JOIN sqb_trigger_state s ON s.trigger_id=t.trigger_id \ + JOIN sqb_locations l ON l.location_id=t.location_id WHERE t.location_id=$1 AND t.target=$2 \ + AND t.kind='schedule' AND s.status='running' AND t.removed_in_snapshot_id IS NULL \ + AND s.next_due_at<$3 ORDER BY s.next_due_at, t.name LIMIT 100", + vec![ + Parameter::Id(*location_id), + Parameter::Text(target.clone()), + Parameter::Time(overdue_before(now)), + ], + ) + .await?; + transaction = next; + overdue.extend( + ticks + .into_iter() + .map(decode_row) + .collect::, _>>()?, + ); + } + commit(transaction).await?; + Ok(overdue) +} + +fn overdue_before(now: DateTime) -> DateTime { + now - TimeDelta::seconds(30) +} diff --git a/crates/sqlbuild-orchestration/src/platform/main/read_person_problems.rs b/crates/sqlbuild-orchestration/src/platform/main/read_person_problems.rs index cddec67364..5644e4f32c 100644 --- a/crates/sqlbuild-orchestration/src/platform/main/read_person_problems.rs +++ b/crates/sqlbuild-orchestration/src/platform/main/read_person_problems.rs @@ -2,107 +2,45 @@ use std::collections::BTreeSet; -use chrono::TimeDelta; use uuid::Uuid; use crate::authorization::main::person_lists_target::person_lists_target; use crate::credentials::models::DefaultRole; -use crate::platform::models::{BrokenLocation, LateTarget, OverdueTick, PlatformProblems}; +use crate::platform::_helpers::problems::{late_ticks, scan_problems}; +use crate::platform::models::PlatformProblems; use crate::store::errors::ControlError; -use crate::store::main::decode_row::decode_row; -use crate::store::main::query::query; -use crate::store::main::{begin::begin, commit::commit}; -use crate::store::models::{ControlStore, Parameter, TransactionMode}; +use crate::store::models::ControlStore; -/// Uses the watchdog's thresholds; access is checked per target before each target's ticks are read. +/// Access is checked per target before that target's late ticks are read, so one target cannot crowd out another. pub async fn read_person_problems( store: &ControlStore, principal_id: Uuid, default_role: DefaultRole, ) -> Result { - let transaction = begin(store, TransactionMode::Read).await?; - let now = transaction.now; - let overdue_before = now - TimeDelta::seconds(30); - let (transaction, leases) = query( - transaction, - "SELECT name FROM sqb_scheduler_lease WHERE name='scheduler' AND expires_at>$1", - vec![Parameter::Time(now)], - ) - .await?; - let (transaction, locations) = query( - transaction, - "SELECT i.location_id, i.target, MAX(i.last_seen_at) AS last_seen_at \ - FROM sqb_location_instances i JOIN sqb_locations l ON l.location_id=i.location_id \ - WHERE l.archived_at IS NULL GROUP BY i.location_id,i.target \ - HAVING MAX(i.last_seen_at)<$1 ORDER BY i.location_id,i.target", - vec![Parameter::Time(now - TimeDelta::seconds(90))], - ) - .await?; - let (transaction, late_targets) = query( - transaction, - "SELECT DISTINCT t.location_id, t.target FROM sqb_triggers t \ - JOIN sqb_trigger_state s ON s.trigger_id=t.trigger_id \ - JOIN sqb_locations l ON l.location_id=t.location_id \ - WHERE t.kind='schedule' AND s.status='running' AND t.removed_in_snapshot_id IS NULL \ - AND l.archived_at IS NULL AND s.next_due_at<$1", - vec![Parameter::Time(overdue_before)], - ) - .await?; - commit(transaction).await?; - let locations: Vec = locations - .into_iter() - .map(decode_row) - .collect::>()?; - let late_targets: BTreeSet<(Uuid, String)> = late_targets - .into_iter() - .map(|row| { - let target: LateTarget = decode_row(row)?; - Ok((target.location_id, target.target)) - }) - .collect::>()?; + let scan = scan_problems(store).await?; let mut listed: BTreeSet<(Uuid, String)> = BTreeSet::new(); - for (location_id, target) in locations + for (location_id, target) in scan + .broken .iter() .map(|location| (location.location_id, location.target.clone())) - .chain(late_targets.iter().cloned()) + .chain(scan.late_targets.iter().cloned()) .collect::>() { if person_lists_target(store, principal_id, (location_id, &target), default_role).await? { listed.insert((location_id, target)); } } - let mut overdue_ticks: Vec = Vec::new(); - let mut transaction = begin(store, TransactionMode::Read).await?; - for (location_id, target) in late_targets.intersection(&listed) { - let (next, ticks) = query( - transaction, - "SELECT t.trigger_id, t.name, t.location_id, t.target, s.next_due_at AS scheduled_for \ - FROM sqb_triggers t JOIN sqb_trigger_state s ON s.trigger_id=t.trigger_id \ - WHERE t.location_id=$1 AND t.target=$2 AND t.kind='schedule' AND s.status='running' \ - AND t.removed_in_snapshot_id IS NULL AND s.next_due_at<$3 \ - ORDER BY s.next_due_at, t.name LIMIT 100", - vec![ - Parameter::Id(*location_id), - Parameter::Text(target.clone()), - Parameter::Time(overdue_before), - ], - ) - .await?; - transaction = next; - overdue_ticks.extend( - ticks - .into_iter() - .map(decode_row) - .collect::, _>>()?, - ); - } - commit(transaction).await?; Ok(PlatformProblems { - scheduler_leader: !leases.is_empty(), - broken_locations: locations + scheduler_leader: scan + .lease + .as_ref() + .is_some_and(|lease| lease.expires_at > scan.now), + scheduler_lease_expires_at: scan.lease.map(|lease| lease.expires_at), + overdue_ticks: late_ticks(store, scan.late_targets.intersection(&listed), scan.now).await?, + broken_locations: scan + .broken .into_iter() .filter(|location| listed.contains(&(location.location_id, location.target.clone()))) .collect(), - overdue_ticks, }) } diff --git a/crates/sqlbuild-orchestration/src/platform/models.rs b/crates/sqlbuild-orchestration/src/platform/models.rs index b6d47ea0c9..1bb5c963c8 100644 --- a/crates/sqlbuild-orchestration/src/platform/models.rs +++ b/crates/sqlbuild-orchestration/src/platform/models.rs @@ -255,6 +255,8 @@ pub struct PlatformEventPayload { #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] pub struct PlatformProblems { pub scheduler_leader: bool, + /// When the last scheduler lease ran out, or will run out; absent until a server first leads. + pub scheduler_lease_expires_at: Option>, pub broken_locations: Vec, pub overdue_ticks: Vec, } @@ -263,6 +265,7 @@ pub struct PlatformProblems { #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] pub struct BrokenLocation { pub location_id: Uuid, + pub location_name: String, pub target: String, pub last_seen_at: DateTime, } @@ -273,12 +276,27 @@ pub struct OverdueTick { pub trigger_id: Uuid, pub name: String, pub location_id: Uuid, + pub location_name: String, pub target: String, pub scheduled_for: DateTime, } +#[derive(Debug, Deserialize)] +pub(crate) struct SchedulerLease { + pub(crate) expires_at: DateTime, +} + #[derive(Debug, Deserialize)] pub(crate) struct LateTarget { pub(crate) location_id: Uuid, pub(crate) target: String, } + +/// Problems found at one instant: the scheduler lease, broken location targets and targets with late ticks. +#[derive(Debug)] +pub(crate) struct ProblemScan { + pub(crate) now: DateTime, + pub(crate) lease: Option, + pub(crate) broken: Vec, + pub(crate) late_targets: std::collections::BTreeSet<(Uuid, String)>, +} diff --git a/crates/sqlbuild-orchestration/tests/platform/test_problems.rs b/crates/sqlbuild-orchestration/tests/platform/test_problems.rs index 62fd90b10a..3ebb270633 100644 --- a/crates/sqlbuild-orchestration/tests/platform/test_problems.rs +++ b/crates/sqlbuild-orchestration/tests/platform/test_problems.rs @@ -30,11 +30,13 @@ fn given_broken_locations_and_late_ticks_when_people_read_problems_then_only_lis description: "SQLite without a leader", backend: Backend::Sqlite, leaders: 0, + expected_leader: false, }, ProblemsTestCase { description: "PostgreSQL with a leader", backend: Backend::Postgres, leaders: 1, + expected_leader: true, }, ]; for test_case in test_cases { @@ -115,12 +117,22 @@ fn given_broken_locations_and_late_ticks_when_people_read_problems_then_only_lis let read = |principal: Uuid| read_person_problems(store, principal, DefaultRole::None); let everything = read(admin).await.expect("administrator problems"); assert_eq!( - everything.scheduler_leader, - test_case.leaders > 0, + everything.scheduler_leader, test_case.expected_leader, "{}", test_case.description ); - assert_eq!(everything.broken_locations.len(), 2); + assert_eq!( + everything.scheduler_lease_expires_at.is_some(), + test_case.expected_leader + ); + assert_eq!( + everything + .broken_locations + .iter() + .map(|location| location.location_name.as_str()) + .collect::>(), + std::collections::BTreeSet::from(["inventory", "orders"]) + ); assert_eq!( everything.overdue_ticks.len(), 200, @@ -142,7 +154,8 @@ fn given_broken_locations_and_late_ticks_when_people_read_problems_then_only_lis scoped .overdue_ticks .iter() - .all(|tick| tick.location_id == locations[1]), + .all(|tick| tick.location_id == locations[1] + && tick.location_name == "inventory"), "another location's late ticks stay hidden" ); let hidden = read(outsider).await.expect("outsider problems"); diff --git a/crates/sqlbuild-orchestration/tests/platform/test_types.rs b/crates/sqlbuild-orchestration/tests/platform/test_types.rs index 003c26e766..8fa6dd4a47 100644 --- a/crates/sqlbuild-orchestration/tests/platform/test_types.rs +++ b/crates/sqlbuild-orchestration/tests/platform/test_types.rs @@ -56,4 +56,5 @@ pub(crate) struct ProblemsTestCase { pub(crate) description: &'static str, pub(crate) backend: Backend, pub(crate) leaders: usize, + pub(crate) expected_leader: bool, } diff --git a/ui/src/lib/api/http-client.ts b/ui/src/lib/api/http-client.ts index b45308f9cb..1d7ce14601 100644 --- a/ui/src/lib/api/http-client.ts +++ b/ui/src/lib/api/http-client.ts @@ -7,10 +7,11 @@ import { auditMatches, toAuditEntry } from './http-audit'; import { toSnapshotChanges } from './http-changes'; import { toLocationSummary } from './http-locations'; import { toAccount, toRoleAssignments } from './http-people'; +import { toPlatformEvents } from './http-problems'; import { jobDetailOf, toJob, toTick, toTrigger, triggerDetailOf } from './http-jobs'; import { RUN_WINDOW, runDetailOf, runRowPage, runsMatching, runTabCountsOf, toRunNode, toRuns } from './http-runs'; import type { ChangeSubscription, ChangedChannel, ChangedSignal, ConnectionState } from './signals'; -import type { AuthMode, Bootstrap, Capability, Location, Scope } from './types'; +import type { AuthMode, Bootstrap, Capability, Location, PlatformEvent, Scope } from './types'; import type { Account, AuditEntry, AuditQuery, LocationSummary, RoleAssignment } from './types/platform'; import type { CatalogNode, Check, CheckDetail, CheckEvidence, CheckSql, SnapshotChange } from './types/build'; import type { Job, JobDetail, RerunMode, Run, RunDetail, RunListQuery, RunRowPage, RunRowQuery, RunTabCounts, Trigger, TriggerDetail, TriggerState } from './types/operate'; @@ -46,7 +47,8 @@ const SUPPORTED: ReadonlySet = new Set([ 'listAuditEntries', 'listAccounts', 'listRoleAssignments', - 'listLocations' + 'listLocations', + 'listPlatformEvents' ]); /** The server's largest audit page, and how many the audit log reads before it stops looking. */ @@ -284,6 +286,12 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt return found.slice(0, limit); }, /** Each location the person sees, with health only where they may list the whole target. */ + async listPlatformEvents(): Promise { + const { data, error, response } = await http.GET('/api/control/platform/problems'); + if (response.status === 401) onUnauthorized(); + if (!data) throw new ApiError(errorText(error, response), response.status); + return toPlatformEvents(data, Date.now()); + }, async listLocations(): Promise { const { data, error, response } = await http.GET('/api/control/bootstrap'); if (response.status === 401) onUnauthorized(); diff --git a/ui/src/lib/api/http-problems.test.ts b/ui/src/lib/api/http-problems.test.ts new file mode 100644 index 0000000000..9074446a06 --- /dev/null +++ b/ui/src/lib/api/http-problems.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from 'vitest'; +import { toPlatformEvents } from './http-problems'; + +const NOW = Date.parse('2026-11-08T12:00:00Z'); +const tick = (name: string, location: string, scheduledFor: string) => ({ + trigger_id: `t-${name}`, + name, + location_id: location, + location_name: location === 'l1' ? 'orders' : 'inventory', + target: 'development', + scheduled_for: scheduledFor +}); + +describe('toPlatformEvents', () => { + it('reports nothing while the scheduler leads and nothing is late', () => { + expect(toPlatformEvents({ scheduler_leader: true, scheduler_lease_expires_at: '2026-11-08T12:00:10Z', broken_locations: [], overdue_ticks: [] }, NOW)).toEqual([]); + }); + + it('dates a stopped scheduler from its lease expiry, and from now when no server has led', () => { + const [stopped] = toPlatformEvents({ scheduler_leader: false, scheduler_lease_expires_at: '2026-11-08T11:50:00Z', broken_locations: [], overdue_ticks: [] }, NOW); + expect(stopped).toMatchObject({ kind: 'scheduler_no_leader', severity: 'critical', locationId: null, occurredAt: '2026-11-08T11:50:00Z' }); + const [never] = toPlatformEvents({ scheduler_leader: false, broken_locations: [], overdue_ticks: [] }, NOW); + expect(never.occurredAt).toBe('2026-11-08T12:00:00.000Z'); + }); + + it('keys broken locations by location and target', () => { + const [broken] = toPlatformEvents( + { scheduler_leader: true, broken_locations: [{ location_id: 'l2', location_name: 'inventory', target: 'development', last_seen_at: '2026-11-08T11:55:00Z' }], overdue_ticks: [] }, + NOW + ); + expect(broken).toMatchObject({ kind: 'location_broken', locationId: 'l2:development', occurredAt: '2026-11-08T11:55:00Z', subject: 'inventory · development' }); + }); + + it('groups late ticks per location, dated and named by the oldest', () => { + const events = toPlatformEvents( + { + scheduler_leader: true, + broken_locations: [], + overdue_ticks: [tick('hourly_orders', 'l1', '2026-11-08T11:40:00Z'), tick('daily_orders', 'l1', '2026-11-08T11:10:00Z'), tick('stock_levels', 'l2', '2026-11-08T11:59:00Z')] + }, + NOW + ); + expect(events.map((e) => [e.locationId, e.occurredAt, e.message])).toEqual([ + ['l1:development', '2026-11-08T11:10:00Z', '2 schedules in orders · development are late, starting with daily_orders.'], + ['l2:development', '2026-11-08T11:59:00Z', 'stock_levels in inventory · development is late.'] + ]); + }); +}); diff --git a/ui/src/lib/api/http-problems.ts b/ui/src/lib/api/http-problems.ts new file mode 100644 index 0000000000..9b1e44dfb2 --- /dev/null +++ b/ui/src/lib/api/http-problems.ts @@ -0,0 +1,56 @@ +import type { components } from '$control/schema.d.ts'; +import { locationKey as keyOf } from './http-client'; +import type { PlatformEvent } from './types'; + +type Wire = components['schemas']; + +const placeOf = (name: string, target: string) => `${name} · ${target}`; + +/** Live problems as active banner events: a stopped scheduler, each broken location, and late schedules per location. */ +export function toPlatformEvents(problems: Wire['PlatformProblems'], now: number): PlatformEvent[] { + const events: PlatformEvent[] = []; + if (!problems.scheduler_leader) { + events.push({ + id: 'scheduler', + kind: 'scheduler_no_leader', + severity: 'critical', + occurredAt: problems.scheduler_lease_expires_at ?? new Date(now).toISOString(), + active: true, + locationId: null, + subject: null, + message: 'No server is running the scheduler, so schedules and sensors aren’t starting runs.' + }); + } + for (const location of problems.broken_locations) { + events.push({ + id: `broken:${keyOf(location.location_id, location.target)}`, + kind: 'location_broken', + severity: 'critical', + occurredAt: location.last_seen_at, + active: true, + locationId: keyOf(location.location_id, location.target), + subject: placeOf(location.location_name, location.target), + message: `${placeOf(location.location_name, location.target)} stopped checking in, so its scheduled runs aren’t starting.` + }); + } + const late = new Map(); + for (const tick of problems.overdue_ticks) { + const key = keyOf(tick.location_id, tick.target); + late.set(key, [...(late.get(key) ?? []), tick]); + } + for (const [key, ticks] of late) { + const oldest = ticks.reduce((a, b) => (a.scheduled_for <= b.scheduled_for ? a : b)); + const place = placeOf(oldest.location_name, oldest.target); + events.push({ + id: `late:${key}`, + kind: 'tick_overdue', + severity: 'warning', + occurredAt: oldest.scheduled_for, + active: true, + locationId: key, + subject: place, + message: ticks.length === 1 ? `${oldest.name} in ${place} is late.` : `${ticks.length} schedules in ${place} are late, starting with ${oldest.name}.` + }); + } + return events; +} diff --git a/ui/src/lib/api/mock/fixtures.ts b/ui/src/lib/api/mock/fixtures.ts index e4eea051b5..b56bb33124 100644 --- a/ui/src/lib/api/mock/fixtures.ts +++ b/ui/src/lib/api/mock/fixtures.ts @@ -82,7 +82,7 @@ export function createFixtures(now: number) { ]; const platformEvents: PlatformEvent[] = [ - { id: 'pe-1', kind: 'location_broken', severity: 'warning', occurredAt: ago(48), active: true, locationId: LOCATION_MARKETING, subject: 'marketing-analytics', message: 'The latest deploy failed to load; still running the previous version.' }, + { id: 'pe-1', kind: 'location_broken', severity: 'warning', occurredAt: ago(48), active: true, locationId: LOCATION_MARKETING, subject: 'marketing-analytics', message: 'marketing-analytics stopped checking in, so its scheduled runs aren’t starting.' }, { id: 'pe-2', kind: 'tick_overdue', severity: 'warning', occurredAt: ago(12), active: true, locationId: LOCATION_SHOP, subject: 'segments_hourly', message: 'The segments_hourly schedule should have started a run 12 minutes ago.' }, { id: 'pe-6', kind: 'warehouse_sync_failed', severity: 'warning', occurredAt: ago(95), active: true, locationId: LOCATION_SHOP_STAGING, subject: '_sqlbuild_janitor_events', message: 'Permission denied on _sqlbuild_janitor_events: the catalog connection’s role can’t use schema ARCHIVE, so cleanup history isn’t up to date.' }, { id: 'pe-3', kind: 'heartbeat', severity: 'info', occurredAt: ago(1), active: false, locationId: null, subject: null, message: 'Server heartbeat.' } diff --git a/ui/src/lib/components/shell/platform-event-banner.svelte b/ui/src/lib/components/shell/platform-event-banner.svelte index f376d06cbd..d1e7f2c484 100644 --- a/ui/src/lib/components/shell/platform-event-banner.svelte +++ b/ui/src/lib/components/shell/platform-event-banner.svelte @@ -11,6 +11,11 @@ app.platformEvents.filter((e) => e.active && e.severity !== 'info').sort((a, b) => rank(b) - rank(a)) ); const critical = $derived(events.some((e) => e.severity === 'critical')); + /** Location problems link to the location's page if it exists, else the Locations list; server problems only to server status. */ + function linkOf(event: PlatformEvent): { href: string; text: string } | null { + if (event.locationId) return { href: app.client.supports('getLocation') ? problemHref(event) : '/locations', text: 'View location →' }; + return app.client.supports('platformHealth') ? { href: problemHref(event), text: 'View server status →' } : null; + } {#if events.length} @@ -20,11 +25,12 @@ diff --git a/ui/src/lib/platform/labels.ts b/ui/src/lib/platform/labels.ts index c6702a11f0..9fe55a11fb 100644 --- a/ui/src/lib/platform/labels.ts +++ b/ui/src/lib/platform/labels.ts @@ -44,7 +44,7 @@ export function healthWords(location: Pick = { tick_overdue: 'Schedule overdue', scheduler_no_leader: 'Scheduler stopped', - location_broken: 'Deploy failed', + location_broken: 'Location not checking in', launch_failed: 'Couldn’t start runs', connection_test_failed: 'Connection test failed', warehouse_sync_failed: 'Warehouse sync failing', diff --git a/ui/src/lib/state/app.svelte.ts b/ui/src/lib/state/app.svelte.ts index 336731ad38..2fa0a7cf9f 100644 --- a/ui/src/lib/state/app.svelte.ts +++ b/ui/src/lib/state/app.svelte.ts @@ -18,6 +18,8 @@ import { } from '$lib/api'; const SCOPE_KEY = 'sb-scope'; +/** How often the banner rereads live problems; locations check in every 30 seconds. */ +const PROBLEM_POLL_MS = 30_000; /** * App-wide state: the bootstrap, the selected location, and the @@ -88,6 +90,8 @@ class AppState { this.#restoreScope(); this.client.changes.follow(this.scope); void this.#loadPlatformEvents(); + // Problems are derived from time passing, such as a missed check-in, so no change notice announces them. + if (this.client.supports('listPlatformEvents')) setInterval(() => void this.#loadPlatformEvents(), PROBLEM_POLL_MS); this.#loadScoped(); } From 58de3d619e4468a053bd20f66aea7c407bd7cf13 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 14:16:17 +0000 Subject: [PATCH 050/106] docs(ui): keep the locations doc comment on listLocations --- ui/src/lib/api/http-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/lib/api/http-client.ts b/ui/src/lib/api/http-client.ts index 1d7ce14601..cff0183d7e 100644 --- a/ui/src/lib/api/http-client.ts +++ b/ui/src/lib/api/http-client.ts @@ -285,13 +285,13 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt } return found.slice(0, limit); }, - /** Each location the person sees, with health only where they may list the whole target. */ async listPlatformEvents(): Promise { const { data, error, response } = await http.GET('/api/control/platform/problems'); if (response.status === 401) onUnauthorized(); if (!data) throw new ApiError(errorText(error, response), response.status); return toPlatformEvents(data, Date.now()); }, + /** Each location the person sees, with health only where they may list the whole target. */ async listLocations(): Promise { const { data, error, response } = await http.GET('/api/control/bootstrap'); if (response.status === 401) onUnauthorized(); From 35f7a3a3e6b9e1e26621a694ae9d113533b3a713 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 14:28:35 +0000 Subject: [PATCH 051/106] feat(server): let role administrators read a target's role definitions Add GET /api/admin/locations/{location_id}/targets/{target}/roles behind server-wide role.assign, like the role holdings read. It returns the built-in roles with their actions, then the target's published custom roles with their description, grants and the nodes each selector matched, and how many active people hold each role there. --- clients/typescript/src/schema.d.ts | 76 +++++++++++ .../authentication/password_sessions.rs | 4 + .../api/_helpers/resources/administrators.rs | 17 +++ .../sqlbuild-orchestration/src/api/models.rs | 1 + .../authorization/_helpers/custom_roles.rs | 17 +++ .../src/authorization/_helpers/mod.rs | 1 + .../authorization/_helpers/person_grants.rs | 8 ++ .../authorization/_helpers/role_overview.rs | 119 ++++++++++++++++++ .../main/list_role_definitions.rs | 30 +++++ .../src/authorization/main/mod.rs | 1 + .../src/authorization/models.rs | 19 +++ .../src/credentials/models.rs | 21 ++++ .../tests/api/test_person_sessions.rs | 8 +- .../tests/credentials/test_project_roles.rs | 54 ++++++++ fensu.toml | 2 +- 15 files changed, 375 insertions(+), 3 deletions(-) create mode 100644 crates/sqlbuild-orchestration/src/authorization/_helpers/role_overview.rs create mode 100644 crates/sqlbuild-orchestration/src/authorization/main/list_role_definitions.rs diff --git a/clients/typescript/src/schema.d.ts b/clients/typescript/src/schema.d.ts index 4674ad9a07..872f6a05cb 100644 --- a/clients/typescript/src/schema.d.ts +++ b/clients/typescript/src/schema.d.ts @@ -1,5 +1,21 @@ // Generated from sqb server --openapi. Run npm run generate; do not edit. export interface paths { + "/api/admin/locations/{location_id}/targets/{target}/roles": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["listRoleDefinitions"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/admin/people": { parameters: { query?: never; @@ -1707,6 +1723,28 @@ export interface components { }; /** @description A built-in role by name, or a custom role a location publishes. */ RoleKey: components["schemas"]["BuiltinRole"] | string; + /** @description A role usable on one location target: built in, or published by the location's project. */ + RoleOverview: { + builtin: boolean; + /** @description The project's description; absent for built-in roles. */ + description?: string | null; + /** @description Built-in roles: one grant without selectors, meaning every node. Custom roles: their published grants. */ + grants: components["schemas"]["RoleOverviewGrant"][]; + /** + * Format: int64 + * @description Active people assigned the role here or everywhere; SSO group members are not counted. + */ + holders: number; + key: string; + }; + /** @description Node selectors, the actions they grant, and the nodes each selector matched when published. */ + RoleOverviewGrant: { + matched: { + [key: string]: string[]; + }; + nodes: string[]; + permissions: components["schemas"]["Action"][]; + }; /** @description This is a server-owned instruction, not an arbitrary status supplied by a worker. */ RunChange: { container_id?: string | null; @@ -2295,6 +2333,44 @@ export interface components { } export type $defs = Record; export interface operations { + listRoleDefinitions: { + parameters: { + query?: never; + header?: never; + path: { + location_id: string; + target: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Built-in roles, then the target's published custom roles */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RoleOverview"][]; + }; + }; + /** @description Session rejected */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + }; + }; listPeople: { parameters: { query?: never; diff --git a/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs b/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs index 28ba723955..7aba640430 100644 --- a/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs +++ b/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs @@ -69,6 +69,10 @@ pub(crate) fn person_routes() -> Router { "/api/admin/people", get(crate::api::_helpers::resources::administrators::people), ) + .route( + "/api/admin/locations/{location_id}/targets/{target}/roles", + get(crate::api::_helpers::resources::administrators::definitions), + ) .route( "/api/admin/roles", post(crate::api::_helpers::resources::administrators::assign) diff --git a/crates/sqlbuild-orchestration/src/api/_helpers/resources/administrators.rs b/crates/sqlbuild-orchestration/src/api/_helpers/resources/administrators.rs index 4031c8df16..627719a819 100644 --- a/crates/sqlbuild-orchestration/src/api/_helpers/resources/administrators.rs +++ b/crates/sqlbuild-orchestration/src/api/_helpers/resources/administrators.rs @@ -9,10 +9,12 @@ use crate::api::models::MutationResponse; use crate::authorization::main::assign_builtin_role::assign_builtin_role; use crate::authorization::main::change_administrator::change_administrator; use crate::authorization::main::list_people::list_people; +use crate::authorization::main::list_role_definitions::list_role_definitions; use crate::authorization::main::list_role_holdings::list_role_holdings; use crate::credentials::models::AuthenticatedPerson; use crate::credentials::models::{ AdministratorChange, AuditContext, PersonSummary, RoleAssignmentRequest, RoleHoldings, + RoleOverview, }; use crate::store::models::ControlStore; @@ -70,3 +72,18 @@ pub(crate) async fn holdings( list_role_holdings(&store, (&context, principal)).await?, )) } + +#[utoipa::path(get, path = "/api/admin/locations/{location_id}/targets/{target}/roles", operation_id = "listRoleDefinitions", + params(("location_id" = Uuid, Path), ("target" = String, Path)), + responses((status = 200, body = Vec, description = "Built-in roles, then the target's published custom roles"), + (status = 401, description = "Session rejected"), + (status = 403, body = crate::api::models::ApiErrorBody)), security(("session" = []), ("cliSession" = [])))] +pub(crate) async fn definitions( + (State(store), Extension(context)): (State, Extension), + Path((location_id, target)): Path<(Uuid, String)>, + Extension(AuthenticatedPerson(principal, _)): Extension, +) -> Result>, ApiError> { + Ok(Json( + list_role_definitions(&store, (&context, principal), (location_id, &target)).await?, + )) +} diff --git a/crates/sqlbuild-orchestration/src/api/models.rs b/crates/sqlbuild-orchestration/src/api/models.rs index 4868410cae..574a1accc0 100644 --- a/crates/sqlbuild-orchestration/src/api/models.rs +++ b/crates/sqlbuild-orchestration/src/api/models.rs @@ -16,6 +16,7 @@ use crate::api::_helpers::resources::{ administrators::assign, administrators::people, administrators::holdings, + administrators::definitions, audit::read, person_operations::request_job, person_operations::cancel_run, diff --git a/crates/sqlbuild-orchestration/src/authorization/_helpers/custom_roles.rs b/crates/sqlbuild-orchestration/src/authorization/_helpers/custom_roles.rs index e62b97fb00..f363a71880 100644 --- a/crates/sqlbuild-orchestration/src/authorization/_helpers/custom_roles.rs +++ b/crates/sqlbuild-orchestration/src/authorization/_helpers/custom_roles.rs @@ -35,6 +35,23 @@ fn valid_grant(grant: &RoleGrant) -> bool { !grant.nodes.is_empty() && !blank_selector && !grant.permissions.is_empty() && !administrative } +/// Each published role's description by key; the document was validated when it was published. +pub(crate) fn decode_roles_descriptions( + document: Option<&Value>, +) -> Result, ControlError> { + let roles: BTreeMap = document.map_or_else( + || Ok(BTreeMap::new()), + |document| { + serde_json::from_value(document.clone()) + .map_err(|error| ControlError::Invalid(format!("roles: {error}"))) + }, + )?; + Ok(roles + .into_iter() + .map(|(key, role)| (key, role.description)) + .collect()) +} + pub(crate) fn decode_roles( document: Option<&Value>, ) -> Result>, ControlError> { diff --git a/crates/sqlbuild-orchestration/src/authorization/_helpers/mod.rs b/crates/sqlbuild-orchestration/src/authorization/_helpers/mod.rs index fd4b1031c4..2e33ac15ba 100644 --- a/crates/sqlbuild-orchestration/src/authorization/_helpers/mod.rs +++ b/crates/sqlbuild-orchestration/src/authorization/_helpers/mod.rs @@ -4,3 +4,4 @@ pub(crate) mod administrators; pub(crate) mod custom_roles; pub(crate) mod person_grants; pub(crate) mod principal_names; +pub(crate) mod role_overview; diff --git a/crates/sqlbuild-orchestration/src/authorization/_helpers/person_grants.rs b/crates/sqlbuild-orchestration/src/authorization/_helpers/person_grants.rs index a6d05a8f58..fd06e7352e 100644 --- a/crates/sqlbuild-orchestration/src/authorization/_helpers/person_grants.rs +++ b/crates/sqlbuild-orchestration/src/authorization/_helpers/person_grants.rs @@ -58,6 +58,14 @@ fn role_allows(role: BuiltinRole, action: Action) -> bool { } } +/// A built-in role's actions on every node, in vocabulary order. +pub(crate) fn builtin_actions(role: BuiltinRole) -> Vec { + ACTIONS + .into_iter() + .filter(|action| role_allows(role, *action)) + .collect() +} + /// The assigned and group roles of an active person for `target`; `None` when the person is disabled or unknown. pub(crate) async fn person_role_rows( transaction: ControlTransaction, diff --git a/crates/sqlbuild-orchestration/src/authorization/_helpers/role_overview.rs b/crates/sqlbuild-orchestration/src/authorization/_helpers/role_overview.rs new file mode 100644 index 0000000000..f5ae39f286 --- /dev/null +++ b/crates/sqlbuild-orchestration/src/authorization/_helpers/role_overview.rs @@ -0,0 +1,119 @@ +//! The facts behind a target's role overview, read in the caller's authorized transaction. + +use std::collections::BTreeMap; + +use uuid::Uuid; + +use crate::authorization::_helpers::custom_roles::{decode_grants, decode_roles_descriptions}; +use crate::authorization::_helpers::person_grants::builtin_actions; +use crate::authorization::models::{CurrentSnapshotId, RoleFacts, RoleHolderCount}; +use crate::credentials::models::{BuiltinRole, RoleGrant, RoleOverview, RoleOverviewGrant}; +use crate::snapshots::main::read_snapshot_documents::read_snapshot_documents; +use crate::store::errors::ControlError; +use crate::store::main::{decode_row::decode_row, query::query}; +use crate::store::models::{ControlTransaction, Parameter}; + +const BUILTIN: [(BuiltinRole, &str); 4] = [ + (BuiltinRole::Viewer, "viewer"), + (BuiltinRole::Operator, "operator"), + (BuiltinRole::Developer, "developer"), + (BuiltinRole::Admin, "admin"), +]; + +/// Published custom roles, active holders per role key, and the current snapshot's role descriptions. +pub(crate) async fn read_role_facts( + transaction: ControlTransaction, + (location_id, target): (Uuid, &str), +) -> Result<(ControlTransaction, RoleFacts), ControlError> { + let scope = vec![Parameter::Id(location_id), Parameter::Text(target.into())]; + let (transaction, custom) = query( + transaction, + "SELECT role_key, resolved_grants_json FROM sqb_effective_roles \ + WHERE location_id=$1 AND target=$2 ORDER BY role_key", + scope.clone(), + ) + .await?; + let (transaction, holders) = query( + transaction, + "SELECT a.role_key, COUNT(DISTINCT a.principal_id) AS holders FROM sqb_role_assignments a \ + JOIN sqb_principals p ON p.principal_id=a.principal_id WHERE p.disabled_at IS NULL \ + AND (a.location_id IS NULL OR a.location_id=$1) AND (a.target IS NULL OR a.target=$2) \ + GROUP BY a.role_key", + scope.clone(), + ) + .await?; + let (transaction, current) = query( + transaction, + "SELECT snapshot_id FROM sqb_current_snapshots WHERE location_id=$1 AND target=$2", + scope, + ) + .await?; + let current: Option = + current.into_iter().next().map(decode_row).transpose()?; + let (transaction, documents) = match current { + Some(current) => { + read_snapshot_documents(transaction, current.snapshot_id, Some(&["roles"])).await? + } + None => (transaction, BTreeMap::new()), + }; + let holders: Vec = holders + .into_iter() + .map(decode_row) + .collect::>()?; + Ok(( + transaction, + RoleFacts { + custom, + holders: holders + .into_iter() + .map(|count| (count.role_key, count.holders)) + .collect(), + descriptions: decode_roles_descriptions(documents.get("roles"))?, + }, + )) +} + +/// Built-in roles first, then custom roles by key. +pub(crate) fn role_overviews(facts: RoleFacts) -> Result, ControlError> { + let holders_of = |key: &str| facts.holders.get(key).copied().unwrap_or_default(); + let mut roles: Vec = BUILTIN + .into_iter() + .map(|(role, key)| RoleOverview { + key: key.into(), + builtin: true, + description: None, + grants: vec![RoleOverviewGrant { + nodes: Vec::new(), + permissions: builtin_actions(role), + matched: BTreeMap::new(), + }], + holders: holders_of(key), + }) + .collect(); + for row in &facts.custom { + let key = row["role_key"].as_str().unwrap_or_default().to_owned(); + roles.push(RoleOverview { + holders: holders_of(&key), + description: facts.descriptions.get(&key).cloned(), + builtin: false, + grants: decode_grants(&row["resolved_grants_json"])? + .into_iter() + .map(overview_grant) + .collect(), + key, + }); + } + Ok(roles) +} + +fn overview_grant(grant: RoleGrant) -> RoleOverviewGrant { + let mut matched: BTreeMap> = BTreeMap::new(); + for (selector, nodes) in grant.resolved_nodes { + matched.insert(selector, nodes.into_iter().collect()); + } + RoleOverviewGrant { + nodes: grant.nodes.into_iter().collect(), + permissions: grant.permissions.into_iter().collect(), + matched, + } +} diff --git a/crates/sqlbuild-orchestration/src/authorization/main/list_role_definitions.rs b/crates/sqlbuild-orchestration/src/authorization/main/list_role_definitions.rs new file mode 100644 index 0000000000..a84bfa99b8 --- /dev/null +++ b/crates/sqlbuild-orchestration/src/authorization/main/list_role_definitions.rs @@ -0,0 +1,30 @@ +//! Administrators who assign roles see what each role usable on a target grants and how many hold it. + +use uuid::Uuid; + +use crate::authorization::_helpers::role_overview::{read_role_facts, role_overviews}; +use crate::authorization::main::begin_authorized::begin_authorized; +use crate::credentials::models::{ + Action, AuditContext, DefaultRole, PermissionScope, RoleOverview, +}; +use crate::store::errors::ControlError; +use crate::store::main::commit::commit; +use crate::store::models::ControlStore; + +/// Built-in roles first, then custom roles by key; requires `role.assign` everywhere. +pub async fn list_role_definitions( + store: &ControlStore, + (context, credentials): (&AuditContext, Uuid), + (location_id, target): (Uuid, &str), +) -> Result, ControlError> { + let (transaction, _) = begin_authorized( + store, + (context, credentials), + &PermissionScope::action(Action::RoleAssign, None), + DefaultRole::None, + ) + .await?; + let (transaction, facts) = read_role_facts(transaction, (location_id, target)).await?; + commit(transaction).await?; + role_overviews(facts) +} diff --git a/crates/sqlbuild-orchestration/src/authorization/main/mod.rs b/crates/sqlbuild-orchestration/src/authorization/main/mod.rs index 408b92f28b..163b3b6f0c 100644 --- a/crates/sqlbuild-orchestration/src/authorization/main/mod.rs +++ b/crates/sqlbuild-orchestration/src/authorization/main/mod.rs @@ -8,6 +8,7 @@ pub mod compile_roles; pub(crate) mod deny_authorization; pub mod grant_admin; pub mod list_people; +pub mod list_role_definitions; pub mod list_role_holdings; pub(crate) mod person_lists_target; pub(crate) mod publish_custom_roles; diff --git a/crates/sqlbuild-orchestration/src/authorization/models.rs b/crates/sqlbuild-orchestration/src/authorization/models.rs index 2b2aeb7017..5e80963f3d 100644 --- a/crates/sqlbuild-orchestration/src/authorization/models.rs +++ b/crates/sqlbuild-orchestration/src/authorization/models.rs @@ -22,3 +22,22 @@ pub struct PersonAccess { pub actions: Vec, pub locations: Vec, } + +/// What a target's role overview is built from, read in one transaction. +#[derive(Debug)] +pub(crate) struct RoleFacts { + pub(crate) custom: Vec, + pub(crate) holders: std::collections::BTreeMap, + pub(crate) descriptions: std::collections::BTreeMap, +} + +#[derive(Debug, serde::Deserialize)] +pub(crate) struct RoleHolderCount { + pub(crate) role_key: String, + pub(crate) holders: u64, +} + +#[derive(Debug, serde::Deserialize)] +pub(crate) struct CurrentSnapshotId { + pub(crate) snapshot_id: Uuid, +} diff --git a/crates/sqlbuild-orchestration/src/credentials/models.rs b/crates/sqlbuild-orchestration/src/credentials/models.rs index cb9cb43fc1..e5a1ff1281 100644 --- a/crates/sqlbuild-orchestration/src/credentials/models.rs +++ b/crates/sqlbuild-orchestration/src/credentials/models.rs @@ -66,6 +66,27 @@ pub struct RoleHoldings { pub group_roles: Vec, } +/// A role usable on one location target: built in, or published by the location's project. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] +pub struct RoleOverview { + pub key: String, + pub builtin: bool, + /// The project's description; absent for built-in roles. + pub description: Option, + /// Built-in roles: one grant without selectors, meaning every node. Custom roles: their published grants. + pub grants: Vec, + /// Active people assigned the role here or everywhere; SSO group members are not counted. + pub holders: u64, +} + +/// Node selectors, the actions they grant, and the nodes each selector matched when published. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] +pub struct RoleOverviewGrant { + pub nodes: Vec, + pub permissions: Vec, + pub matched: std::collections::BTreeMap>, +} + /// A role assigned to one person, everywhere or on one location and target. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] pub struct PersonRole { diff --git a/crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs b/crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs index d1533549d9..b126e740e8 100644 --- a/crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs +++ b/crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs @@ -324,11 +324,15 @@ fn given_browser_session_roles_when_work_is_requested_then_scope_revocation_and_ } let granted = usize::from(role_case.expected_admin == StatusCode::OK.as_u16()); let mut admin_reads = Vec::new(); - for admin_read in ["/api/admin/people", "/api/admin/roles"] { + for admin_read in [ + "/api/admin/people".to_owned(), + "/api/admin/roles".to_owned(), + format!("/api/admin/locations/{location}/targets/development/roles"), + ] { let listed = request( &provider.client, session, - (Method::GET, admin_read), + (Method::GET, &admin_read), (&Value::Null, &session.csrf), ) .await; diff --git a/crates/sqlbuild-orchestration/tests/credentials/test_project_roles.rs b/crates/sqlbuild-orchestration/tests/credentials/test_project_roles.rs index 387778bc48..166aed5c93 100644 --- a/crates/sqlbuild-orchestration/tests/credentials/test_project_roles.rs +++ b/crates/sqlbuild-orchestration/tests/credentials/test_project_roles.rs @@ -5,6 +5,7 @@ use std::collections::{BTreeMap, BTreeSet}; use serde_json::json; use crate::authorization::main::compile_roles::compile_roles; +use crate::authorization::main::list_role_definitions::list_role_definitions; use crate::credentials::models::{Action, RoleAssignmentRequest, RoleKey}; use crate::credentials::tests::role_helpers::{ SUPPORT_ROLE, assign, authorize_member, developer_grants, member_listings, @@ -15,6 +16,7 @@ use crate::credentials::tests::test_types::{ProjectRoleTestCase, RolesFileTestCa use crate::declarations::models::TriggerChange; use crate::runs::tests::helpers::snapshot; use crate::snapshots::main::publish_snapshot::publish_snapshot; +use crate::store::errors::ControlError; use crate::store::main::migrate_store::migrate_store; use crate::store::tests::store_helpers::{close_test_store, open_test_store, run}; use crate::store::tests::store_test_types::Backend; @@ -91,6 +93,58 @@ fn given_published_selector_when_it_matches_more_nodes_then_coverage_grows_on_pu "{}: the new role, the dropped view grant and the grown coverage enter the change feed", test_case.description ); + let definitions = list_role_definitions( + store, + (&roles.context, roles.administrator.identity.principal_id), + (roles.location_id, "development"), + ) + .await + .expect("role definitions"); + assert_eq!( + definitions + .iter() + .map(|role| (role.key.as_str(), role.builtin, role.holders)) + .collect::>(), + [ + ("viewer", true, 0), + ("operator", true, 0), + ("developer", true, 0), + ("admin", true, 1), + (SUPPORT_ROLE, false, 1) + ], + "{}: built-in roles, then the published role, each with its active holders here", + test_case.description + ); + assert_eq!( + definitions[0].grants[0].permissions, + [Action::View], + "a viewer views every node" + ); + let support = &definitions[4]; + assert_eq!( + support.description.as_deref(), + Some("Answer order questions") + ); + assert_eq!( + support.grants[0].matched, + BTreeMap::from([( + "tag:orders".to_owned(), + vec!["customers".to_owned(), "orders".to_owned()] + )]), + "each selector shows the nodes it matched at the latest publish" + ); + assert!( + matches!( + list_role_definitions( + store, + (&roles.context, roles.member_id), + (roles.location_id, "development"), + ) + .await, + Err(ControlError::Denied { .. }) + ), + "people without role.assign everywhere cannot read role definitions" + ); close_test_store(fixture).await; }); } diff --git a/fensu.toml b/fensu.toml index 51249679c6..c7d29e389c 100644 --- a/fensu.toml +++ b/fensu.toml @@ -1381,7 +1381,7 @@ paths = ["crates/sqlbuild-orchestration/src/api/_helpers/resources/administrator reason = "Each function is one utoipa-annotated axum route: OpenAPI needs one annotated handler per operation, and each body only checks the caller's scope and calls one store operation. Scope checks and store work live in their shared owners." [[dupes.allowlist]] -paths = ["crates/sqlbuild-orchestration/src/authorization/main/read_security_audit.rs", "crates/sqlbuild-orchestration/src/credentials/_helpers/active_session.rs", "crates/sqlbuild-orchestration/src/declarations/_helpers/reads.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_person_jobs.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_person_triggers.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_triggers.rs", "crates/sqlbuild-orchestration/src/platform/main/read_location_work.rs", "crates/sqlbuild-orchestration/src/platform/main/read_person_location_health.rs", "crates/sqlbuild-orchestration/src/platform/main/location_health.rs", "crates/sqlbuild-orchestration/src/snapshots/main/current_snapshot_record.rs", "crates/sqlbuild-orchestration/src/runs/main/list_person_runs.rs", "crates/sqlbuild-orchestration/src/runs/main/list_person_run_nodes.rs", "crates/sqlbuild-orchestration/src/runs/main/list_run_nodes.rs", "crates/sqlbuild-orchestration/src/runs/main/list_runs.rs", "crates/sqlbuild-orchestration/src/runs/_helpers/lifecycle/reads.rs", "crates/sqlbuild-orchestration/src/warehouse_history/_helpers/checks.rs", "crates/sqlbuild-orchestration/src/scheduler/main/list_person_ticks.rs", "crates/sqlbuild-orchestration/src/store/main/read_records.rs", "crates/sqlbuild-orchestration/src/platform/main/location_work.rs", "crates/sqlbuild-orchestration/src/scheduler/main/list_ticks.rs", "crates/sqlbuild-orchestration/src/sensors/_helpers/attempts.rs", "crates/sqlbuild-orchestration/src/sensors/_helpers/facts.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_current_record.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_snapshot_documents.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_snapshot_nodes.rs", "crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_catalog.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_check.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_checks.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_maintenance.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_microbatch_coverage.rs", "crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_statistics.rs"] +paths = ["crates/sqlbuild-orchestration/src/authorization/main/list_role_definitions.rs", "crates/sqlbuild-orchestration/src/authorization/main/read_security_audit.rs", "crates/sqlbuild-orchestration/src/credentials/_helpers/active_session.rs", "crates/sqlbuild-orchestration/src/declarations/_helpers/reads.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_person_jobs.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_person_triggers.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_triggers.rs", "crates/sqlbuild-orchestration/src/platform/main/read_location_work.rs", "crates/sqlbuild-orchestration/src/platform/main/read_person_location_health.rs", "crates/sqlbuild-orchestration/src/platform/main/location_health.rs", "crates/sqlbuild-orchestration/src/snapshots/main/current_snapshot_record.rs", "crates/sqlbuild-orchestration/src/runs/main/list_person_runs.rs", "crates/sqlbuild-orchestration/src/runs/main/list_person_run_nodes.rs", "crates/sqlbuild-orchestration/src/runs/main/list_run_nodes.rs", "crates/sqlbuild-orchestration/src/runs/main/list_runs.rs", "crates/sqlbuild-orchestration/src/runs/_helpers/lifecycle/reads.rs", "crates/sqlbuild-orchestration/src/warehouse_history/_helpers/checks.rs", "crates/sqlbuild-orchestration/src/scheduler/main/list_person_ticks.rs", "crates/sqlbuild-orchestration/src/store/main/read_records.rs", "crates/sqlbuild-orchestration/src/platform/main/location_work.rs", "crates/sqlbuild-orchestration/src/scheduler/main/list_ticks.rs", "crates/sqlbuild-orchestration/src/sensors/_helpers/attempts.rs", "crates/sqlbuild-orchestration/src/sensors/_helpers/facts.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_current_record.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_snapshot_documents.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_snapshot_nodes.rs", "crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_catalog.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_check.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_checks.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_maintenance.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_microbatch_coverage.rs", "crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_statistics.rs"] reason = "Each is one control-store read with its own SQL and record type. Transactions, person authorization and decoding go through the shared begin_authorized, query_records, read_records and query_bodies helpers; what remains alike is the call shape." [[dupes.allowlist]] From a49d34610017232a2c92cecbb8154e9d12243026 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 14:35:57 +0000 Subject: [PATCH 052/106] feat(ui): show role definitions on the Roles pages The Roles list and role pages read the new role definitions endpoint. Built-in roles show their actions on every node, and custom roles show their published grants, the nodes each selector matched, the published definition and who holds them on this location. Holder counts include SSO group members, counted once per person. The SSO mappings tab and the base-role comparison stay hidden until the server provides them. --- clients/typescript/src/schema.d.ts | 2 +- .../authorization/_helpers/role_overview.rs | 13 +++-- .../src/credentials/models.rs | 2 +- .../tests/credentials/test_project_roles.rs | 39 ++++++++++++- ui/src/lib/api/http-client.ts | 21 ++++++- ui/src/lib/api/http-roles.test.ts | 56 +++++++++++++++++++ ui/src/lib/api/http-roles.ts | 45 +++++++++++++++ ui/src/lib/api/types/platform.ts | 8 +-- ui/src/lib/platform/access.ts | 6 +- ui/src/routes/admin/roles/+page.svelte | 4 +- ui/src/routes/admin/roles/[name]/+page.svelte | 12 ++-- ui/src/routes/admin/roles/roles-tab.svelte | 4 +- ui/src/routes/admin/roles/state.svelte.ts | 2 +- 13 files changed, 187 insertions(+), 27 deletions(-) create mode 100644 ui/src/lib/api/http-roles.test.ts create mode 100644 ui/src/lib/api/http-roles.ts diff --git a/clients/typescript/src/schema.d.ts b/clients/typescript/src/schema.d.ts index 872f6a05cb..d7144f9518 100644 --- a/clients/typescript/src/schema.d.ts +++ b/clients/typescript/src/schema.d.ts @@ -1732,7 +1732,7 @@ export interface components { grants: components["schemas"]["RoleOverviewGrant"][]; /** * Format: int64 - * @description Active people assigned the role here or everywhere; SSO group members are not counted. + * @description Active people holding the role here or everywhere, assigned directly or through an SSO group. */ holders: number; key: string; diff --git a/crates/sqlbuild-orchestration/src/authorization/_helpers/role_overview.rs b/crates/sqlbuild-orchestration/src/authorization/_helpers/role_overview.rs index f5ae39f286..dfd563abf1 100644 --- a/crates/sqlbuild-orchestration/src/authorization/_helpers/role_overview.rs +++ b/crates/sqlbuild-orchestration/src/authorization/_helpers/role_overview.rs @@ -35,10 +35,15 @@ pub(crate) async fn read_role_facts( .await?; let (transaction, holders) = query( transaction, - "SELECT a.role_key, COUNT(DISTINCT a.principal_id) AS holders FROM sqb_role_assignments a \ - JOIN sqb_principals p ON p.principal_id=a.principal_id WHERE p.disabled_at IS NULL \ - AND (a.location_id IS NULL OR a.location_id=$1) AND (a.target IS NULL OR a.target=$2) \ - GROUP BY a.role_key", + "SELECT h.role_key, COUNT(DISTINCT h.principal_id) AS holders FROM (\ + SELECT a.principal_id, a.role_key FROM sqb_role_assignments a \ + WHERE (a.location_id IS NULL OR a.location_id=$1) AND (a.target IS NULL OR a.target=$2) \ + UNION ALL \ + SELECT m.principal_id, g.role_key FROM sqb_group_roles g \ + JOIN sqb_person_groups m ON m.group_name=g.group_name \ + WHERE (g.location_id IS NULL OR g.location_id=$1) AND (g.target IS NULL OR g.target=$2)\ + ) h JOIN sqb_principals p ON p.principal_id=h.principal_id WHERE p.disabled_at IS NULL \ + GROUP BY h.role_key", scope.clone(), ) .await?; diff --git a/crates/sqlbuild-orchestration/src/credentials/models.rs b/crates/sqlbuild-orchestration/src/credentials/models.rs index e5a1ff1281..5020b8b587 100644 --- a/crates/sqlbuild-orchestration/src/credentials/models.rs +++ b/crates/sqlbuild-orchestration/src/credentials/models.rs @@ -75,7 +75,7 @@ pub struct RoleOverview { pub description: Option, /// Built-in roles: one grant without selectors, meaning every node. Custom roles: their published grants. pub grants: Vec, - /// Active people assigned the role here or everywhere; SSO group members are not counted. + /// Active people holding the role here or everywhere, assigned directly or through an SSO group. pub holders: u64, } diff --git a/crates/sqlbuild-orchestration/tests/credentials/test_project_roles.rs b/crates/sqlbuild-orchestration/tests/credentials/test_project_roles.rs index 166aed5c93..543013707a 100644 --- a/crates/sqlbuild-orchestration/tests/credentials/test_project_roles.rs +++ b/crates/sqlbuild-orchestration/tests/credentials/test_project_roles.rs @@ -4,8 +4,11 @@ use std::collections::{BTreeMap, BTreeSet}; use serde_json::json; +use crate::api::models::GroupRole; use crate::authorization::main::compile_roles::compile_roles; use crate::authorization::main::list_role_definitions::list_role_definitions; +use crate::authorization::main::publish_group_roles::publish_group_roles; +use crate::credentials::main::identities::create_principal::create_principal; use crate::credentials::models::{Action, RoleAssignmentRequest, RoleKey}; use crate::credentials::tests::role_helpers::{ SUPPORT_ROLE, assign, authorize_member, developer_grants, member_listings, @@ -18,6 +21,8 @@ use crate::runs::tests::helpers::snapshot; use crate::snapshots::main::publish_snapshot::publish_snapshot; use crate::store::errors::ControlError; use crate::store::main::migrate_store::migrate_store; +use crate::store::main::{begin::begin, commit::commit, query::query}; +use crate::store::models::{Parameter, TransactionMode}; use crate::store::tests::store_helpers::{close_test_store, open_test_store, run}; use crate::store::tests::store_test_types::Backend; @@ -93,6 +98,36 @@ fn given_published_selector_when_it_matches_more_nodes_then_coverage_grows_on_pu "{}: the new role, the dropped view grant and the grown coverage enter the change feed", test_case.description ); + publish_group_roles( + store, + &[GroupRole { + group: "support-team".into(), + role: RoleKey::Custom(SUPPORT_ROLE.into()), + location_id: Some(roles.location_id), + target: None, + }], + ) + .await + .expect("group mapping"); + let grouped = create_principal(store, &roles.context, "support-lead") + .await + .expect("group member"); + for principal in [grouped, roles.member_id] { + let transaction = begin(store, TransactionMode::Write) + .await + .expect("group membership"); + let (transaction, _) = query( + transaction, + "INSERT INTO sqb_person_groups (principal_id,group_name) VALUES ($1,$2)", + vec![ + Parameter::Id(principal), + Parameter::Text("support-team".into()), + ], + ) + .await + .expect("member of the group"); + commit(transaction).await.expect("membership committed"); + } let definitions = list_role_definitions( store, (&roles.context, roles.administrator.identity.principal_id), @@ -110,9 +145,9 @@ fn given_published_selector_when_it_matches_more_nodes_then_coverage_grows_on_pu ("operator", true, 0), ("developer", true, 0), ("admin", true, 1), - (SUPPORT_ROLE, false, 1) + (SUPPORT_ROLE, false, 2) ], - "{}: built-in roles, then the published role, each with its active holders here", + "{}: built-in roles, then the published role, each counting a person once whether assigned directly or through a group", test_case.description ); assert_eq!( diff --git a/ui/src/lib/api/http-client.ts b/ui/src/lib/api/http-client.ts index cff0183d7e..bb195ca1b3 100644 --- a/ui/src/lib/api/http-client.ts +++ b/ui/src/lib/api/http-client.ts @@ -8,11 +8,12 @@ import { toSnapshotChanges } from './http-changes'; import { toLocationSummary } from './http-locations'; import { toAccount, toRoleAssignments } from './http-people'; import { toPlatformEvents } from './http-problems'; +import { roleDetailOf, toRoleDefinition } from './http-roles'; import { jobDetailOf, toJob, toTick, toTrigger, triggerDetailOf } from './http-jobs'; import { RUN_WINDOW, runDetailOf, runRowPage, runsMatching, runTabCountsOf, toRunNode, toRuns } from './http-runs'; import type { ChangeSubscription, ChangedChannel, ChangedSignal, ConnectionState } from './signals'; import type { AuthMode, Bootstrap, Capability, Location, PlatformEvent, Scope } from './types'; -import type { Account, AuditEntry, AuditQuery, LocationSummary, RoleAssignment } from './types/platform'; +import type { Account, AuditEntry, AuditQuery, LocationSummary, RoleAssignment, RoleDefinition, RoleDetail } from './types/platform'; import type { CatalogNode, Check, CheckDetail, CheckEvidence, CheckSql, SnapshotChange } from './types/build'; import type { Job, JobDetail, RerunMode, Run, RunDetail, RunListQuery, RunRowPage, RunRowQuery, RunTabCounts, Trigger, TriggerDetail, TriggerState } from './types/operate'; @@ -48,7 +49,9 @@ const SUPPORTED: ReadonlySet = new Set([ 'listAccounts', 'listRoleAssignments', 'listLocations', - 'listPlatformEvents' + 'listPlatformEvents', + 'listRoles', + 'getRole' ]); /** The server's largest audit page, and how many the audit log reads before it stops looking. */ @@ -310,6 +313,12 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt ); }, listAccounts: async (): Promise => (await people()).map(toAccount), + listRoles: async (locationId: string): Promise => (await roleOverviews(locationId)).map(toRoleDefinition), + async getRole(locationId: string, name: string): Promise { + const [roles, assignments] = await Promise.all([roleOverviews(locationId), implemented.listRoleAssignments()]); + const role = roles.find((r) => r.key === name); + return role ? roleDetailOf(role, assignments, locationId) : null; + }, async listRoleAssignments(): Promise { const { data, error, response } = await http.GET('/api/admin/roles'); if (response.status === 401) onUnauthorized(); @@ -396,6 +405,14 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt return data; } + async function roleOverviews(locationKey: string) { + const { locationId, target } = parseLocationKey(locationKey); + const { data, error, response } = await http.GET('/api/admin/locations/{location_id}/targets/{target}/roles', { params: { path: { location_id: locationId, target } } }); + if (response.status === 401) onUnauthorized(); + if (!data) throw new ApiError(errorText(error, response), response.status); + return data; + } + async function people() { const { data, error, response } = await http.GET('/api/admin/people'); if (response.status === 401) onUnauthorized(); diff --git a/ui/src/lib/api/http-roles.test.ts b/ui/src/lib/api/http-roles.test.ts new file mode 100644 index 0000000000..189802e195 --- /dev/null +++ b/ui/src/lib/api/http-roles.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest'; +import { roleDetailOf, toRoleDefinition } from './http-roles'; +import type { RoleAssignment } from './types/platform'; + +const support = { + key: 'support', + builtin: false, + description: 'Answer order questions', + grants: [{ nodes: ['tag:orders'], permissions: ['view' as const, 'view.sql' as const], matched: { 'tag:orders': ['customers', 'orders'] } }], + holders: 2 +}; +const assignment = (id: string, role: string, locationId: string | null): RoleAssignment => ({ + id, + principal: { kind: 'user', name: id, displayName: id }, + role, + scope: { locationId }, + source: 'manual', + ssoGroup: null, + definedIn: null, + assignedBy: null, + assignedAt: null, + stale: false +}); + +describe('toRoleDefinition', () => { + it('describes built-in roles itself and keeps a custom role published description', () => { + const viewer = toRoleDefinition({ key: 'viewer', builtin: true, grants: [{ nodes: [], permissions: ['view'], matched: {} }], holders: 0 }); + expect(viewer).toMatchObject({ source: 'built_in', extends: null, recentlyMatched: null }); + expect(viewer.description).toContain('Sees'); + expect(toRoleDefinition(support)).toMatchObject({ source: 'project', description: 'Answer order questions', holders: 2 }); + }); +}); + +describe('roleDetailOf', () => { + it('lists assignments everywhere, on the whole location, or on this target only', () => { + const detail = roleDetailOf( + support, + [ + assignment('everywhere', 'support', null), + assignment('location', 'support', 'l1'), + assignment('target', 'support', 'l1:development'), + assignment('other-target', 'support', 'l1:production'), + assignment('other-location', 'support', 'l2'), + assignment('other-role', 'viewer', null) + ], + 'l1:development' + ); + expect(detail.assignments.map((a) => a.id)).toEqual(['everywhere', 'location', 'target']); + expect(detail.selectors).toEqual([{ selector: 'tag:orders', nodes: ['customers', 'orders'], newSinceLastWeek: [] }]); + expect(JSON.parse(detail.definition ?? '')).toEqual({ description: 'Answer order questions', grants: [{ nodes: ['tag:orders'], permissions: ['view', 'view.sql'] }] }); + }); + + it('has no definition text for built-in roles', () => { + expect(roleDetailOf({ key: 'admin', builtin: true, grants: [], holders: 1 }, [], 'l1:development').definition).toBeNull(); + }); +}); diff --git a/ui/src/lib/api/http-roles.ts b/ui/src/lib/api/http-roles.ts new file mode 100644 index 0000000000..2abbca8945 --- /dev/null +++ b/ui/src/lib/api/http-roles.ts @@ -0,0 +1,45 @@ +import type { components } from '$control/schema.d.ts'; +import type { BuiltInRole, Capability } from './types'; +import type { RoleAssignment, RoleDefinition, RoleDetail } from './types/platform'; + +type Wire = components['schemas']; + +/** What each built-in role is for; the server owns their actions. */ +export const BUILT_IN_DESCRIPTION: Record = { + viewer: 'Sees runs, jobs, triggers, checks and the catalog.', + operator: 'Runs jobs, cancels and reruns runs, and pauses or resumes triggers.', + developer: 'Everything an operator does, plus SQL, ad-hoc runs and connection tests.', + admin: 'Every action, including people, roles and the audit log.' +}; + +/** A role as the Roles list shows it; where it's defined and how it grew aren't known to the server. */ +export function toRoleDefinition(wire: Wire['RoleOverview']): RoleDefinition { + return { + name: wire.key, + source: wire.builtin ? 'built_in' : 'project', + definedIn: null, + description: wire.builtin ? BUILT_IN_DESCRIPTION[wire.key as BuiltInRole] : (wire.description ?? ''), + extends: null, + grants: wire.grants.map((g) => ({ nodes: g.nodes, permissions: g.permissions as Capability[] })), + holders: wire.holders, + recentlyMatched: null + }; +} + +/** Whether an assignment applies to this location key: everywhere, the whole location, or this target. */ +function covers(assignment: RoleAssignment, locationKey: string): boolean { + const scope = assignment.scope.locationId; + return scope === null || scope === locationKey || locationKey.startsWith(`${scope}:`); +} + +/** One role with what each selector matched at the latest publish and who holds it here. */ +export function roleDetailOf(wire: Wire['RoleOverview'], assignments: RoleAssignment[], locationKey: string): RoleDetail { + const role = toRoleDefinition(wire); + return { + ...role, + baseGrants: [], + selectors: wire.grants.flatMap((g) => g.nodes.map((selector) => ({ selector, nodes: g.matched[selector] ?? [], newSinceLastWeek: [] }))), + assignments: assignments.filter((a) => a.role === wire.key && covers(a, locationKey)), + definition: wire.builtin ? null : JSON.stringify({ description: wire.description, grants: wire.grants.map(({ nodes, permissions }) => ({ nodes, permissions })) }, null, 2) + }; +} diff --git a/ui/src/lib/api/types/platform.ts b/ui/src/lib/api/types/platform.ts index 4a87a54ab7..caf3aad99c 100644 --- a/ui/src/lib/api/types/platform.ts +++ b/ui/src/lib/api/types/platform.ts @@ -332,8 +332,8 @@ export type RoleDefinition = { grants: RoleGrant[]; /** People holding the role somewhere. */ holders: number; - /** Nodes the role's selectors started matching in the last 7 days. */ - recentlyMatched: number; + /** Nodes the role's selectors started matching in the last 7 days; null when unknown. */ + recentlyMatched: number | null; }; /** What one selector of a role matches in the current snapshot. */ @@ -349,8 +349,8 @@ export type RoleDetail = RoleDefinition & { baseGrants: RoleGrant[]; selectors: SelectorMatch[]; assignments: RoleAssignment[]; - /** The role's definition as written, read-only. */ - definition: string; + /** The role's definition as published, read-only; null for built-in roles. */ + definition: string | null; }; /** `locationId: null` applies everywhere. */ diff --git a/ui/src/lib/platform/access.ts b/ui/src/lib/platform/access.ts index dd2e3fc644..87cd630ed3 100644 --- a/ui/src/lib/platform/access.ts +++ b/ui/src/lib/platform/access.ts @@ -23,11 +23,11 @@ export function roleSummary(role: Pick = { built_in: 'Built-in', server_config: 'Server config', project: 'Policy file' }; +export const roleSourceLabel: Record = { built_in: 'Built-in', server_config: 'Server config', project: 'Project' }; export const assignmentSourceLabel: Record = { manual: 'Manual', sso: 'SSO group', policy: 'Server config', built_in: 'Default for everyone' }; diff --git a/ui/src/routes/admin/roles/+page.svelte b/ui/src/routes/admin/roles/+page.svelte index ac8cb2ab20..1a4525ef32 100644 --- a/ui/src/routes/admin/roles/+page.svelte +++ b/ui/src/routes/admin/roles/+page.svelte @@ -33,11 +33,11 @@ bind:value={() => tab, select} tabs={[ { value: 'roles', label: 'Roles', count: rs.roles.data?.length }, - { value: 'sso', label: 'SSO mappings', count: rs.sso.data?.length } + ...(app.client.supports('listSsoMappings') ? [{ value: 'sso' as const, label: 'SSO mappings', count: rs.sso.data?.length }] : []) ]} />
- {#if tab === 'roles'} + {#if tab === 'roles' || !app.client.supports('listSsoMappings')} {:else} diff --git a/ui/src/routes/admin/roles/[name]/+page.svelte b/ui/src/routes/admin/roles/[name]/+page.svelte index 3d998169d2..cb34c4354a 100644 --- a/ui/src/routes/admin/roles/[name]/+page.svelte +++ b/ui/src/routes/admin/roles/[name]/+page.svelte @@ -69,7 +69,7 @@ {:else} {#snippet actions()} - + {#if r.extends}{/if} {/snippet} {#if r.extends}

@@ -77,7 +77,7 @@ inherit further than one built-in role.

{:else} -

Starts from nothing; only the additions below apply.

+

Starts from nothing; only the additions below apply, on top of any built-in role a person also holds.

{/if} {#if compare} @@ -160,9 +160,11 @@ {/if} {/if} - - - + {#if r.definition} + + + + {/if}
diff --git a/ui/src/routes/admin/roles/roles-tab.svelte b/ui/src/routes/admin/roles/roles-tab.svelte index 38e0b1a219..cb3e8e993e 100644 --- a/ui/src/routes/admin/roles/roles-tab.svelte +++ b/ui/src/routes/admin/roles/roles-tab.svelte @@ -12,8 +12,8 @@

- Roles in {app.location?.name}. Each custom role is a built-in role plus additions on chosen nodes. Roles are defined in code (the project's policy file or the server - config) and are read-only here. + Roles in {app.location?.name}. Built-in roles act on every node; custom roles add actions on chosen nodes. Custom roles come from the project's roles file and are + read-only here.

diff --git a/ui/src/routes/admin/roles/state.svelte.ts b/ui/src/routes/admin/roles/state.svelte.ts index 098666121a..f7865b5afd 100644 --- a/ui/src/routes/admin/roles/state.svelte.ts +++ b/ui/src/routes/admin/roles/state.svelte.ts @@ -6,7 +6,7 @@ import { createResource } from '$lib/state/resource.svelte'; export function createRolesState() { const can = () => app.can('role.assign'); const roles = createResource((client, scope) => (can() ? client.listRoles(scope.locationId) : Promise.resolve([] as RoleDefinition[]))); - const sso = createResource((client) => (can() ? client.listSsoMappings() : Promise.resolve([] as SsoMapping[]))); + const sso = createResource((client) => (can() && client.supports('listSsoMappings') ? client.listSsoMappings() : Promise.resolve([] as SsoMapping[]))); return { roles, From d220b215539ec766bc1636c133778947b5777eb0 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 14:46:07 +0000 Subject: [PATCH 053/106] feat(ui): show warehouse sync status in the top bar The top bar reads the target's warehouse sync reports from the health read: when a table last synced cleanly, and which tables' newest sync failed and at what stage. It rereads every 30 seconds with the alert banner, because sync reports send no change notice. --- ui/src/lib/api/client.ts | 3 ++- ui/src/lib/api/http-client.ts | 14 +++++++++++--- ui/src/lib/api/http-locations.test.ts | 27 ++++++++++++++++++++++++++- ui/src/lib/api/http-locations.ts | 22 +++++++++++++++++++++- ui/src/lib/state/app.svelte.ts | 11 +++++++---- 5 files changed, 67 insertions(+), 10 deletions(-) diff --git a/ui/src/lib/api/client.ts b/ui/src/lib/api/client.ts index 11d6581757..e0fb906e31 100644 --- a/ui/src/lib/api/client.ts +++ b/ui/src/lib/api/client.ts @@ -122,7 +122,8 @@ export interface ApiClient extends PlatformApi { listPlatformEvents(query: { activeOnly: boolean }): Promise; - getWarehouseSync(scope: Scope): Promise; + /** Null when the location has reported no recent sync. */ + getWarehouseSync(scope: Scope): Promise; /** Live "changed" signals (SSE in the server client); consumers refetch. */ changes: ChangeSubscription; diff --git a/ui/src/lib/api/http-client.ts b/ui/src/lib/api/http-client.ts index bb195ca1b3..b79d2c2035 100644 --- a/ui/src/lib/api/http-client.ts +++ b/ui/src/lib/api/http-client.ts @@ -5,14 +5,14 @@ import { checkDetailOf, toCheck, toCheckEvidence, toCheckSql } from './http-chec import { toCatalogNodes } from './http-catalog'; import { auditMatches, toAuditEntry } from './http-audit'; import { toSnapshotChanges } from './http-changes'; -import { toLocationSummary } from './http-locations'; +import { toLocationSummary, warehouseSyncOf } from './http-locations'; import { toAccount, toRoleAssignments } from './http-people'; import { toPlatformEvents } from './http-problems'; import { roleDetailOf, toRoleDefinition } from './http-roles'; import { jobDetailOf, toJob, toTick, toTrigger, triggerDetailOf } from './http-jobs'; import { RUN_WINDOW, runDetailOf, runRowPage, runsMatching, runTabCountsOf, toRunNode, toRuns } from './http-runs'; import type { ChangeSubscription, ChangedChannel, ChangedSignal, ConnectionState } from './signals'; -import type { AuthMode, Bootstrap, Capability, Location, PlatformEvent, Scope } from './types'; +import type { AuthMode, Bootstrap, Capability, Location, PlatformEvent, Scope, WarehouseSync } from './types'; import type { Account, AuditEntry, AuditQuery, LocationSummary, RoleAssignment, RoleDefinition, RoleDetail } from './types/platform'; import type { CatalogNode, Check, CheckDetail, CheckEvidence, CheckSql, SnapshotChange } from './types/build'; import type { Job, JobDetail, RerunMode, Run, RunDetail, RunListQuery, RunRowPage, RunRowQuery, RunTabCounts, Trigger, TriggerDetail, TriggerState } from './types/operate'; @@ -51,7 +51,8 @@ const SUPPORTED: ReadonlySet = new Set([ 'listLocations', 'listPlatformEvents', 'listRoles', - 'getRole' + 'getRole', + 'getWarehouseSync' ]); /** The server's largest audit page, and how many the audit log reads before it stops looking. */ @@ -288,6 +289,13 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt } return found.slice(0, limit); }, + async getWarehouseSync(scope: Scope): Promise { + const { locationId, target } = parseLocationKey(scope.locationId); + const { data, error, response } = await http.GET('/api/control/locations/{location_id}/targets/{target}/health', { params: { path: { location_id: locationId, target } } }); + if (response.status === 401) onUnauthorized(); + if (!data) throw new ApiError(errorText(error, response), response.status); + return warehouseSyncOf(data); + }, async listPlatformEvents(): Promise { const { data, error, response } = await http.GET('/api/control/platform/problems'); if (response.status === 401) onUnauthorized(); diff --git a/ui/src/lib/api/http-locations.test.ts b/ui/src/lib/api/http-locations.test.ts index 05d99ab2b8..6d07abda32 100644 --- a/ui/src/lib/api/http-locations.test.ts +++ b/ui/src/lib/api/http-locations.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { connectionTestOf, healthOf } from './http-locations'; +import { connectionTestOf, healthOf, warehouseSyncOf } from './http-locations'; const now = Date.parse('2026-11-08T12:00:00Z'); const at = (secondsAgo: number) => new Date(now - secondsAgo * 1000).toISOString(); @@ -31,3 +31,28 @@ describe('location health', () => { expect(connectionTestOf(observations)).toMatchObject({ outcome: 'failed', at: at(5) }); }); }); + +describe('warehouseSyncOf', () => { + const sync = (at: string, table: 'catalog' | 'statistics', failure: 'read' | 'delivery' | null) => ({ + received_at: at, + report: { event_id: at, session_id: 's1', target: 'development', health: { kind: 'warehouse_sync' as const, generation: 1, location: { schema: 'main' }, table, failure, records_sent: 0, elapsed_millis: 5 } } + }); + + it('is null without any sync report', () => { + expect(warehouseSyncOf([])).toBeNull(); + }); + + it('dates the newest clean sync and names only tables whose newest sync failed', () => { + const result = warehouseSyncOf([ + sync('2026-11-08T12:03:00Z', 'catalog', 'read'), + sync('2026-11-08T12:02:00Z', 'statistics', null), + sync('2026-11-08T12:01:00Z', 'catalog', null), + sync('2026-11-08T12:00:00Z', 'statistics', 'delivery') + ]); + expect(result).toEqual({ lastSyncedAt: '2026-11-08T12:02:00Z', lastError: 'catalog: failed while reading the warehouse' }); + }); + + it('clears the error once the failing table syncs again', () => { + expect(warehouseSyncOf([sync('2026-11-08T12:05:00Z', 'catalog', null), sync('2026-11-08T12:03:00Z', 'catalog', 'read')])?.lastError).toBeNull(); + }); +}); diff --git a/ui/src/lib/api/http-locations.ts b/ui/src/lib/api/http-locations.ts index d08a1dadb8..4136768b3e 100644 --- a/ui/src/lib/api/http-locations.ts +++ b/ui/src/lib/api/http-locations.ts @@ -1,5 +1,5 @@ import type { components } from '$control/schema.d.ts'; -import type { Location, LocationHealth } from './types'; +import type { Location, LocationHealth, WarehouseSync } from './types'; import type { ConnectionTest, LocationSummary } from './types/platform'; type Wire = components['schemas']; @@ -60,3 +60,23 @@ export function toLocationSummary(location: Location, observations: Wire['Locati : null }; } + +const SYNC_STAGE: Record = { + watermark: 'reading where the last sync stopped', + read: 'reading the warehouse', + delivery: 'sending results to the server' +}; + +/** When a table last synced cleanly, and which tables' newest sync failed; null without any sync report. */ +export function warehouseSyncOf(observations: Wire['LocationHealthObservation'][]): WarehouseSync | null { + type Sync = Wire['LocationHealthObservation'] & { report: { health: Extract } }; + const syncs = observations.filter((o): o is Sync => o.report.health.kind === 'warehouse_sync').sort((a, b) => b.received_at.localeCompare(a.received_at)); + if (!syncs.length) return null; + const newest = new Map(); + for (const sync of syncs) if (!newest.has(sync.report.health.table)) newest.set(sync.report.health.table, sync); + const failing = [...newest.values()].flatMap((s) => (s.report.health.failure ? [`${s.report.health.table}: failed while ${SYNC_STAGE[s.report.health.failure]}`] : [])); + return { + lastSyncedAt: syncs.find((s) => !s.report.health.failure)?.received_at ?? null, + lastError: failing.length ? failing.sort().join('\n') : null + }; +} diff --git a/ui/src/lib/state/app.svelte.ts b/ui/src/lib/state/app.svelte.ts index 2fa0a7cf9f..74577ab37c 100644 --- a/ui/src/lib/state/app.svelte.ts +++ b/ui/src/lib/state/app.svelte.ts @@ -18,8 +18,8 @@ import { } from '$lib/api'; const SCOPE_KEY = 'sb-scope'; -/** How often the banner rereads live problems; locations check in every 30 seconds. */ -const PROBLEM_POLL_MS = 30_000; +/** How often the shell rereads live problems and warehouse sync; locations report every 30 seconds. */ +const POLL_MS = 30_000; /** * App-wide state: the bootstrap, the selected location, and the @@ -90,8 +90,11 @@ class AppState { this.#restoreScope(); this.client.changes.follow(this.scope); void this.#loadPlatformEvents(); - // Problems are derived from time passing, such as a missed check-in, so no change notice announces them. - if (this.client.supports('listPlatformEvents')) setInterval(() => void this.#loadPlatformEvents(), PROBLEM_POLL_MS); + // Problems and sync reports come from time passing and location reports, so no change notice announces them. + setInterval(() => { + void this.#loadPlatformEvents(); + void this.#loadWarehouseSync(); + }, POLL_MS); this.#loadScoped(); } From b12327f26e56fd8e4178cf409c3fb9c200bd45a8 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 15:53:41 +0000 Subject: [PATCH 054/106] fix(ui): stop rereading warehouse sync after a refused read Every refused health read writes an audit denial, so a person who sees a location but may not list all of its nodes would add one each poll. The shell now asks once per target per page load and keeps the indicator at its unknown state after a refusal. --- ui/src/lib/state/app.svelte.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/ui/src/lib/state/app.svelte.ts b/ui/src/lib/state/app.svelte.ts index 74577ab37c..48bf1990d1 100644 --- a/ui/src/lib/state/app.svelte.ts +++ b/ui/src/lib/state/app.svelte.ts @@ -46,6 +46,7 @@ class AppState { readonly capabilities = $derived([...(this.bootstrap?.capabilities ?? []), ...(this.location?.capabilities ?? [])]); #started = false; + #syncRefused = new Set(); constructor() { this.client = createClient(new URLSearchParams(browser ? window.location.search : ''), () => this.#signIn()); @@ -154,11 +155,17 @@ class AppState { if (!this.client.supports('getWarehouseSync')) return; const scope = this.scope; if (!scope) return; + // Each refusal is audited, so a target the person may not list is asked once per page load, not every poll. + if (this.#syncRefused.has(scope.locationId)) { + this.warehouseSync = null; + return; + } try { const sync = await this.client.getWarehouseSync(scope); if (this.scope === scope) this.warehouseSync = sync; - } catch { - this.warehouseSync = null; + } catch (error) { + if (error instanceof ApiError && error.status === 403) this.#syncRefused.add(scope.locationId); + if (this.scope === scope) this.warehouseSync = null; } } From 03b49ca93a12f664ebc6a59bb09b931c8e31378d Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 16:01:51 +0000 Subject: [PATCH 055/106] fix(rules): wrap Snowflake table functions in TABLE( ) before parsing The rules engine stubs __table_fn calls before parsing a model. SQLQuack's Snowflake grammar, like Snowflake, accepts a table function in FROM only inside TABLE( ), which is how SQLBuild renders the call, so a bare stub made every built-in Rule report the model unevaluated. Wrap the stub the same way unless the model already wrote TABLE( ), and assert in the Snowflake evaluation tests that the model was actually evaluated. --- .../src/engine/tests/test_evaluation.rs | 51 ++++++++++++++----- .../src/query_facts/_helpers/shape.rs | 51 +++++++++++++++++-- .../src/query_facts/constants.rs | 2 + 3 files changed, 86 insertions(+), 18 deletions(-) diff --git a/crates/sqlbuild-rules/src/engine/tests/test_evaluation.rs b/crates/sqlbuild-rules/src/engine/tests/test_evaluation.rs index 80c3f872c3..4557377612 100644 --- a/crates/sqlbuild-rules/src/engine/tests/test_evaluation.rs +++ b/crates/sqlbuild-rules/src/engine/tests/test_evaluation.rs @@ -510,20 +510,38 @@ fn given_scoped_and_global_declarations_when_evaluating_domain_rule_then_only_gl fn given_snowflake_expression_when_evaluating_rules_then_uses_project_dialect() -> Result<(), String> { let project_dir = TempDir::new().map_err(|error| error.to_string())?; - let test_cases = [test_types::DialectEvaluationTestCase { - description: "Snowflake compiler expressions use the resolved project dialect", - dialect: "snowflake", - query_sql: concat!( - "SELECT * EXCLUDE (ignored), ", - "TRANSFORM(values, leg INT -> leg + 1) AS adjusted, ", - "payload:product_id::STRING AS product_id, ", - "CAST(amounts AS ARRAY(NUMBER(38, 10))) AS amounts, ", - "OBJECT_CONSTRUCT_KEEP_NULL('group_id', group_id) AS details, ", - "FROM __table_fn(\"inventory_items\")(42)" - ) - .to_owned(), - expected_code: "SQBRCONTRACT101", - }]; + let test_cases = [ + test_types::DialectEvaluationTestCase { + description: "Snowflake compiler expressions use the resolved project dialect", + dialect: "snowflake", + query_sql: concat!( + "SELECT * EXCLUDE (ignored), ", + "TRANSFORM(values, leg INT -> leg + 1) AS adjusted, ", + "payload:product_id::STRING AS product_id, ", + "CAST(amounts AS ARRAY(NUMBER(38, 10))) AS amounts, ", + "OBJECT_CONSTRUCT_KEEP_NULL('group_id', group_id) AS details, ", + "FROM __table_fn(\"inventory_items\")(42)" + ) + .to_owned(), + expected_code: "SQBRCONTRACT101", + }, + test_types::DialectEvaluationTestCase { + description: "a Snowflake table function in an aliased CTE source parses as SQLBuild renders it", + dialect: "snowflake", + query_sql: concat!( + "WITH lines AS (SELECT * FROM __table_fn(\"lines_for\")('B-1', __ref(\"orders\").k) AS l)\n", + "SELECT l.item_id FROM lines AS l" + ) + .to_owned(), + expected_code: "SQBRCONTRACT101", + }, + test_types::DialectEvaluationTestCase { + description: "a Snowflake table function already written inside TABLE( ) is not wrapped twice", + dialect: "snowflake", + query_sql: "SELECT item_id FROM table ( __table_fn(\"inventory_items\")(42) )".to_owned(), + expected_code: "SQBRCONTRACT101", + }, + ]; for test_case in test_cases { let config = json!({"select": ["SQBRCONTRACT101"], "cache": {"enabled": false}}); @@ -538,6 +556,11 @@ fn given_snowflake_expression_when_evaluating_rules_then_uses_project_dialect() "{}", test_case.description ); + assert_ne!( + result["faults"][0]["unevaluated"], true, + "{}: {result}", + test_case.description + ); } Ok(()) } diff --git a/crates/sqlbuild-rules/src/query_facts/_helpers/shape.rs b/crates/sqlbuild-rules/src/query_facts/_helpers/shape.rs index f9fae3f4b4..547cc938e0 100644 --- a/crates/sqlbuild-rules/src/query_facts/_helpers/shape.rs +++ b/crates/sqlbuild-rules/src/query_facts/_helpers/shape.rs @@ -1,6 +1,6 @@ use crate::query_facts::constants::{ - CALL_STUB, PLACEHOLDER_PREFIX, PLACEHOLDER_VALUE, REF_STUB, SOURCE_STUB, TABLE_FUNCTION_CALL, - TABLE_FUNCTION_STUB, UDF_CALL, UDF_STUB, + CALL_STUB, PLACEHOLDER_PREFIX, PLACEHOLDER_VALUE, REF_STUB, SNOWFLAKE_DIALECT, SOURCE_STUB, + TABLE_FUNCTION_CALL, TABLE_FUNCTION_STUB, TABLE_WRAPPER, UDF_CALL, UDF_STUB, }; use crate::query_facts::models::{CallKind, ModelCall, ModelShape}; use crate::rules::main::compact_sql::compact_sql; @@ -16,7 +16,12 @@ pub(crate) fn model_shape(sql: &str, dialect: &str) -> Result = normalized @@ -118,10 +123,12 @@ fn quoted_name(sql: &str, open: usize) -> Option<(String, usize)> { (valid && sql.as_bytes().get(close) == Some(&b')')).then(|| (name.to_owned(), close)) } +/// `sql` with calls and placeholders stubbed, table functions optionally wrapped in `TABLE( )`. fn stub_calls( sql: &str, policy: QuotePolicy, syntax: &LexicalSyntax, + wrap_table_functions: bool, ) -> (String, Vec, Vec) { let bytes = sql.as_bytes(); let mut edits: Vec<(usize, usize, String)> = Vec::new(); @@ -178,7 +185,25 @@ fn stub_calls( } else { TABLE_FUNCTION_STUB }; - edits.push((start, open, format!("{stub}{name}"))); + let mut wrap_close = None; + if word == TABLE_FUNCTION_CALL + && wrap_table_functions + && !inside_table_wrapper(bytes, start) + { + let Ok(close) = dialect_matching_paren(bytes, open, syntax) else { + continue; + }; + wrap_close = Some(close); + } + let wrapper = if wrap_close.is_some() { + TABLE_WRAPPER + } else { + "" + }; + edits.push((start, open, format!("{wrapper}{stub}{name}"))); + if let Some(close) = wrap_close { + edits.push((close + 1, close + 1, ")".to_owned())); + } index = open; continue; } @@ -201,10 +226,28 @@ fn stub_calls( edits.push((start, close + 1, format!("{}{number}", stub_prefix(kind)))); index = close + 1; } + edits.sort_by_key(|(start, end, _)| (*start, *end)); let (stubbed, offsets) = apply_edits(sql, edits); (stubbed, offsets, calls) } +/// Whether the call at `start` is already written inside `TABLE( )`. +fn inside_table_wrapper(bytes: &[u8], start: usize) -> bool { + let before = bytes[..start].trim_ascii_end(); + let Some(before) = before.strip_suffix(b"(") else { + return false; + }; + let before = before.trim_ascii_end(); + let keyword = TABLE_WRAPPER.len() - 1; + before.len() >= keyword + && before[before.len() - keyword..] + .eq_ignore_ascii_case(&TABLE_WRAPPER.as_bytes()[..keyword]) + && before + .len() + .checked_sub(keyword + 1) + .is_none_or(|index| !is_word_byte(before[index])) +} + /// `sql` with each edit applied, and per output character the input character it came from. fn apply_edits(sql: &str, edits: Vec<(usize, usize, String)>) -> (String, Vec) { let mut stubbed = String::with_capacity(sql.len()); diff --git a/crates/sqlbuild-rules/src/query_facts/constants.rs b/crates/sqlbuild-rules/src/query_facts/constants.rs index 2ff3bfcad4..c675a5bf38 100644 --- a/crates/sqlbuild-rules/src/query_facts/constants.rs +++ b/crates/sqlbuild-rules/src/query_facts/constants.rs @@ -10,6 +10,8 @@ pub(crate) const MAX_FUNCTION_CALL_DEPTH: usize = 128; pub(crate) const MAX_AST_DEPTH: usize = 512; pub(crate) const UDF_CALL: &str = "__udf"; pub(crate) const TABLE_FUNCTION_CALL: &str = "__table_fn"; +pub(crate) const TABLE_WRAPPER: &str = "TABLE("; +pub(crate) const SNOWFLAKE_DIALECT: &str = "snowflake"; pub(crate) const MIN_CAST_TOKENS: usize = 2; pub(crate) const TRIPLE_QUOTE_LENGTH: usize = 3; /// Snowflake calls with a date-part argument, as (name, argument count, date-part index). From 689debaa240d5c3c5800a60e037278c76652ff38 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 16:30:20 +0000 Subject: [PATCH 056/106] fix(rules): read Snowflake table function markers inside TABLE( ) in rule trees Custom Rules parse a model through the rule tree, which renames each relation marker to a placeholder name first. SQLQuack's Snowflake grammar refuses the bare table function call that leaves, so a custom Rule on a Snowflake model with a table function failed to parse it. Wrap those calls in TABLE( ) over the marker's own padding and a following space, so node positions keep their line and column, and share the TABLE( ) detection with the built-in Rules' stubbing. --- .../src/relation_markers/constants.rs | 2 + .../src/sql_tree/_helpers/mod.rs | 3 +- .../src/sql_tree/_helpers/table_wrapped.rs | 70 +++++++++++++++++++ .../src/sql_tree/main/sql_tree_nodes.rs | 12 +++- .../src/sql_tree/tests/test_sql_tree.rs | 23 ++++++ .../src/query_facts/_helpers/shape.rs | 23 ++---- .../src/query_facts/constants.rs | 1 - .../src/sql_scan/constants.rs | 2 + .../sql_scan/main/follows_table_wrapper.rs | 20 ++++++ .../sqlbuild-sqltext/src/sql_scan/main/mod.rs | 1 + 10 files changed, 133 insertions(+), 24 deletions(-) create mode 100644 crates/sqlbuild-analysis/src/sql_tree/_helpers/table_wrapped.rs create mode 100644 crates/sqlbuild-sqltext/src/sql_scan/main/follows_table_wrapper.rs diff --git a/crates/sqlbuild-analysis/src/relation_markers/constants.rs b/crates/sqlbuild-analysis/src/relation_markers/constants.rs index 4fef92b98e..12db608387 100644 --- a/crates/sqlbuild-analysis/src/relation_markers/constants.rs +++ b/crates/sqlbuild-analysis/src/relation_markers/constants.rs @@ -7,3 +7,5 @@ pub(crate) const RELATION_MARKER_PATTERN: &str = r#"(?i)\b(__[a-z_]+)\(\s*"([^"]+)"\s*(?:,\s*"([^"]+)"\s*)?\)"#; /// The marker whose two names read as one, joined by `__`. pub(crate) const DBT_REF_FUNCTION: &str = "__dbt_ref"; +/// The marker of a table function call, which Snowflake reads only inside `TABLE( )`. +pub(crate) const TABLE_FUNCTION_MARKER: &str = "__table_fn"; diff --git a/crates/sqlbuild-analysis/src/sql_tree/_helpers/mod.rs b/crates/sqlbuild-analysis/src/sql_tree/_helpers/mod.rs index 0598c2c140..942bd41b25 100644 --- a/crates/sqlbuild-analysis/src/sql_tree/_helpers/mod.rs +++ b/crates/sqlbuild-analysis/src/sql_tree/_helpers/mod.rs @@ -1,7 +1,8 @@ -//! Parser selection, parsing, the tree walk, clause nodes and source positions. +//! Parser selection, parsing, the tree walk, clause nodes, source positions and table wrapping. pub(crate) mod clauses; pub(crate) mod parse; pub(crate) mod positions; pub(crate) mod rules_tree_parser; +pub(crate) mod table_wrapped; pub(crate) mod walk; diff --git a/crates/sqlbuild-analysis/src/sql_tree/_helpers/table_wrapped.rs b/crates/sqlbuild-analysis/src/sql_tree/_helpers/table_wrapped.rs new file mode 100644 index 0000000000..bd0779da30 --- /dev/null +++ b/crates/sqlbuild-analysis/src/sql_tree/_helpers/table_wrapped.rs @@ -0,0 +1,70 @@ +//! Snowflake table function markers wrapped in `TABLE( )`, as SQLBuild renders them. + +use std::cmp::Reverse; + +use sqlbuild_sqltext::sql_scan::constants::TABLE_WRAPPER; +use sqlbuild_sqltext::sql_scan::main::dialect_matching_paren::dialect_matching_paren; +use sqlbuild_sqltext::sql_scan::main::follows_table_wrapper::follows_table_wrapper; +use sqlbuild_sqltext::sql_scan::models::LexicalSyntax; + +use crate::relation_markers::constants::{MARKER_NAME_PREFIX, TABLE_FUNCTION_MARKER}; +use crate::relation_markers::types::MarkerCall; + +/// `named` with each table function marker call in `TABLE( )`, written over adjacent spaces where present. +pub(crate) fn table_wrapped(named: &str, calls: &[MarkerCall], syntax: &LexicalSyntax) -> String { + let bytes = named.as_bytes(); + let mut edits: Vec<(usize, usize, String)> = Vec::new(); + for (index, (function, _)) in calls.iter().enumerate() { + if function != TABLE_FUNCTION_MARKER { + continue; + } + let name = format!("{MARKER_NAME_PREFIX}{index}"); + let Some(start) = name_start(bytes, &name) else { + continue; + }; + let name_end = start + name.len(); + let open = name_end + + bytes[name_end..] + .iter() + .take_while(|byte| byte.is_ascii_whitespace()) + .count(); + if bytes.get(open) != Some(&b'(') || follows_table_wrapper(&bytes[..start]) { + continue; + } + let Ok(close) = dialect_matching_paren(bytes, open, syntax) else { + continue; + }; + let spaces = bytes[name_end..] + .iter() + .take_while(|byte| **byte == b' ') + .count(); + let borrowed = if spaces >= TABLE_WRAPPER.len() { + TABLE_WRAPPER.len() + } else { + 0 + }; + edits.push((start, name_end + borrowed, format!("{TABLE_WRAPPER}{name}"))); + let after = close + 1; + let replaced = usize::from(matches!(bytes.get(after), Some(b' ' | b'\t'))); + edits.push((after, after + replaced, ")".to_owned())); + } + edits.sort_by_key(|(start, _, _)| Reverse(*start)); + let mut wrapped = named.to_owned(); + for (start, end, replacement) in edits { + wrapped.replace_range(start..end, &replacement); + } + wrapped +} + +/// Where `name` first stands as a whole word in `bytes`. +fn name_start(bytes: &[u8], name: &str) -> Option { + let name = name.as_bytes(); + let word = |byte: Option<&u8>| { + byte.is_some_and(|value| value.is_ascii_alphanumeric() || *value == b'_') + }; + (0..bytes.len().saturating_sub(name.len() - 1)).find(|start| { + bytes[*start..].starts_with(name) + && !word(start.checked_sub(1).and_then(|index| bytes.get(index))) + && !word(bytes.get(start + name.len())) + }) +} diff --git a/crates/sqlbuild-analysis/src/sql_tree/main/sql_tree_nodes.rs b/crates/sqlbuild-analysis/src/sql_tree/main/sql_tree_nodes.rs index daad7f356f..c1a036e032 100644 --- a/crates/sqlbuild-analysis/src/sql_tree/main/sql_tree_nodes.rs +++ b/crates/sqlbuild-analysis/src/sql_tree/main/sql_tree_nodes.rs @@ -7,8 +7,9 @@ use crate::relation_markers::main::markers_as_names::markers_as_names; use crate::sql_tree::_helpers::parse::parse; use crate::sql_tree::_helpers::positions::line_starts; use crate::sql_tree::_helpers::rules_tree_parser::rules_tree_parser; +use crate::sql_tree::_helpers::table_wrapped::table_wrapped; use crate::sql_tree::_helpers::walk::walk; -use crate::sql_tree::constants::TREE_WORKER_STACK_BYTES; +use crate::sql_tree::constants::{SNOWFLAKE_DIALECT, TREE_WORKER_STACK_BYTES}; use crate::sql_tree::errors::SqlTreeError; use crate::sql_tree::models::SqlTreeNode; use crate::worker_stack::main::run_on_worker_stack::run_on_worker_stack; @@ -24,8 +25,13 @@ pub fn sql_tree_nodes(sql: &str, dialect: &str) -> Result, SqlT fn tree_nodes(sql: &str, dialect: &str) -> Result, SqlTreeError> { let parser = rules_tree_parser(dialect) .ok_or_else(|| SqlTreeError::UnavailableDialect(dialect.to_owned()))?; - let (named, _) = markers_as_names(sql, &dialect_lexical_syntax(dialect)) - .map_err(SqlTreeError::Unanalysable)?; + let syntax = dialect_lexical_syntax(dialect); + let (named, calls) = markers_as_names(sql, &syntax).map_err(SqlTreeError::Unanalysable)?; + let named = if dialect == SNOWFLAKE_DIALECT { + table_wrapped(&named, &calls, &syntax) + } else { + named + }; let statements: Vec = parse(&named, parser)?; walk(&statements, &line_starts(&named)) } diff --git a/crates/sqlbuild-analysis/src/sql_tree/tests/test_sql_tree.rs b/crates/sqlbuild-analysis/src/sql_tree/tests/test_sql_tree.rs index 5a0629d703..9d1d337ff3 100644 --- a/crates/sqlbuild-analysis/src/sql_tree/tests/test_sql_tree.rs +++ b/crates/sqlbuild-analysis/src/sql_tree/tests/test_sql_tree.rs @@ -67,6 +67,29 @@ fn given_sql_when_building_a_rule_tree_then_clauses_own_their_queries_at_their_s ("select", 4, 7, Some("in")), ], }, + TreeShapeTestCase { + description: "a Snowflake table function marker reads as SQLBuild renders it, inside TABLE( ), at its columns", + dialect: "snowflake", + sql: "SELECT l.*\nFROM __table_fn(\"lines_for\")('B-1', __ref(\"orders\").k) AS l JOIN (SELECT 1 AS k) s ON s.k = l.k", + expected_nodes: vec![ + ("select", 1, 1, None), + ("star", 1, 8, Some("select")), + ("join", 2, 61, Some("select")), + ("select", 2, 66, Some("subquery")), + ("subquery", 2, 66, Some("join")), + ], + }, + TreeShapeTestCase { + description: "a Snowflake table function marker already inside TABLE( ) is not wrapped twice", + dialect: "snowflake", + sql: "SELECT 1\nFROM TABLE(__table_fn(\"f\")(1)) t JOIN (SELECT 2 AS k) s ON s.k = t.k", + expected_nodes: vec![ + ("select", 1, 1, None), + ("join", 2, 34, Some("select")), + ("select", 2, 39, Some("subquery")), + ("subquery", 2, 39, Some("join")), + ], + }, TreeShapeTestCase { description: "a comma-separated FROM item after an explicit join is not a join", dialect: "duckdb", diff --git a/crates/sqlbuild-rules/src/query_facts/_helpers/shape.rs b/crates/sqlbuild-rules/src/query_facts/_helpers/shape.rs index 547cc938e0..f5dbfd7b6e 100644 --- a/crates/sqlbuild-rules/src/query_facts/_helpers/shape.rs +++ b/crates/sqlbuild-rules/src/query_facts/_helpers/shape.rs @@ -1,13 +1,15 @@ use crate::query_facts::constants::{ CALL_STUB, PLACEHOLDER_PREFIX, PLACEHOLDER_VALUE, REF_STUB, SNOWFLAKE_DIALECT, SOURCE_STUB, - TABLE_FUNCTION_CALL, TABLE_FUNCTION_STUB, TABLE_WRAPPER, UDF_CALL, UDF_STUB, + TABLE_FUNCTION_CALL, TABLE_FUNCTION_STUB, UDF_CALL, UDF_STUB, }; use crate::query_facts::models::{CallKind, ModelCall, ModelShape}; use crate::rules::main::compact_sql::compact_sql; use sqlbuild_analysis::semantic_validation::main::normalize_mapped::normalize_mapped; +use sqlbuild_sqltext::sql_scan::constants::TABLE_WRAPPER; use sqlbuild_sqltext::sql_scan::main::dialect_lexical_syntax::dialect_lexical_syntax; use sqlbuild_sqltext::sql_scan::main::dialect_matching_paren::dialect_matching_paren; use sqlbuild_sqltext::sql_scan::main::dialect_non_code_end::dialect_non_code_end; +use sqlbuild_sqltext::sql_scan::main::follows_table_wrapper::follows_table_wrapper; use sqlbuild_sqltext::sql_scan::main::quote_policy::quote_policy; use sqlbuild_sqltext::sql_scan::models::{LexicalSyntax, QuotePolicy}; @@ -188,7 +190,7 @@ fn stub_calls( let mut wrap_close = None; if word == TABLE_FUNCTION_CALL && wrap_table_functions - && !inside_table_wrapper(bytes, start) + && !follows_table_wrapper(&bytes[..start]) { let Ok(close) = dialect_matching_paren(bytes, open, syntax) else { continue; @@ -231,23 +233,6 @@ fn stub_calls( (stubbed, offsets, calls) } -/// Whether the call at `start` is already written inside `TABLE( )`. -fn inside_table_wrapper(bytes: &[u8], start: usize) -> bool { - let before = bytes[..start].trim_ascii_end(); - let Some(before) = before.strip_suffix(b"(") else { - return false; - }; - let before = before.trim_ascii_end(); - let keyword = TABLE_WRAPPER.len() - 1; - before.len() >= keyword - && before[before.len() - keyword..] - .eq_ignore_ascii_case(&TABLE_WRAPPER.as_bytes()[..keyword]) - && before - .len() - .checked_sub(keyword + 1) - .is_none_or(|index| !is_word_byte(before[index])) -} - /// `sql` with each edit applied, and per output character the input character it came from. fn apply_edits(sql: &str, edits: Vec<(usize, usize, String)>) -> (String, Vec) { let mut stubbed = String::with_capacity(sql.len()); diff --git a/crates/sqlbuild-rules/src/query_facts/constants.rs b/crates/sqlbuild-rules/src/query_facts/constants.rs index c675a5bf38..ae3601132a 100644 --- a/crates/sqlbuild-rules/src/query_facts/constants.rs +++ b/crates/sqlbuild-rules/src/query_facts/constants.rs @@ -10,7 +10,6 @@ pub(crate) const MAX_FUNCTION_CALL_DEPTH: usize = 128; pub(crate) const MAX_AST_DEPTH: usize = 512; pub(crate) const UDF_CALL: &str = "__udf"; pub(crate) const TABLE_FUNCTION_CALL: &str = "__table_fn"; -pub(crate) const TABLE_WRAPPER: &str = "TABLE("; pub(crate) const SNOWFLAKE_DIALECT: &str = "snowflake"; pub(crate) const MIN_CAST_TOKENS: usize = 2; pub(crate) const TRIPLE_QUOTE_LENGTH: usize = 3; diff --git a/crates/sqlbuild-sqltext/src/sql_scan/constants.rs b/crates/sqlbuild-sqltext/src/sql_scan/constants.rs index f7d11818a4..ffcd1a72a2 100644 --- a/crates/sqlbuild-sqltext/src/sql_scan/constants.rs +++ b/crates/sqlbuild-sqltext/src/sql_scan/constants.rs @@ -14,3 +14,5 @@ pub const GENERIC_LINE_COMMENT_PREFIX: &str = "--"; pub const TRIPLE_QUOTES: [&str; 2] = ["'''", "\"\"\""]; pub const ESCAPE_CHARACTER: char = '\\'; pub const BLOCK_COMMENT_OPEN: &str = "/*"; +/// The wrapper Snowflake takes around a table function call in FROM. +pub const TABLE_WRAPPER: &str = "TABLE("; diff --git a/crates/sqlbuild-sqltext/src/sql_scan/main/follows_table_wrapper.rs b/crates/sqlbuild-sqltext/src/sql_scan/main/follows_table_wrapper.rs new file mode 100644 index 0000000000..5eca2c2433 --- /dev/null +++ b/crates/sqlbuild-sqltext/src/sql_scan/main/follows_table_wrapper.rs @@ -0,0 +1,20 @@ +//! Whether SQL text ends inside an open `TABLE(` wrapper. + +use crate::sql_scan::constants::TABLE_WRAPPER; + +/// Whether `before`, ignoring trailing ASCII whitespace, ends with the word `TABLE` and `(`. +#[must_use] +pub fn follows_table_wrapper(before: &[u8]) -> bool { + let Some(before) = before.trim_ascii_end().strip_suffix(b"(") else { + return false; + }; + let before = before.trim_ascii_end(); + let keyword = &TABLE_WRAPPER.as_bytes()[..TABLE_WRAPPER.len() - 1]; + let Some(start) = before.len().checked_sub(keyword.len()) else { + return false; + }; + before[start..].eq_ignore_ascii_case(keyword) + && start.checked_sub(1).is_none_or(|index| { + !(before[index].is_ascii_alphanumeric() || matches!(before[index], b'_' | b'$')) + }) +} diff --git a/crates/sqlbuild-sqltext/src/sql_scan/main/mod.rs b/crates/sqlbuild-sqltext/src/sql_scan/main/mod.rs index 3cc4afefde..b75cc2205a 100644 --- a/crates/sqlbuild-sqltext/src/sql_scan/main/mod.rs +++ b/crates/sqlbuild-sqltext/src/sql_scan/main/mod.rs @@ -3,6 +3,7 @@ pub mod dialect_lexical_syntax; pub mod dialect_matching_paren; pub mod dialect_non_code_end; pub mod dialect_non_code_ranges; +pub mod follows_table_wrapper; pub mod lexical_syntax_key; pub mod matching_paren; pub mod non_code_end; From 5974f719c7d59217efb990228cb4c0b4982977dc Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 16:32:25 +0000 Subject: [PATCH 057/106] feat(ui): show catalog node detail from the control API and finish lineage labels The catalog node page, the lineage inspector and node links across Catalog, Checks, Changes and Lineage now load from the catalog listing, node status, node SQL and check reads. SQL is read only where the location grants view.sql, so viewers cause no refused, audited reads. Graph cards show the node name without its kind prefix. Sections the server does not report (run history, jobs, history, drift without built SQL, ad-hoc runs) are hidden instead of claiming there are none. --- ui/src/lib/api/http-catalog.test.ts | 45 +++++++++++++- ui/src/lib/api/http-catalog.ts | 59 ++++++++++++++++++- ui/src/lib/api/http-client.test.ts | 37 +++++++++++- ui/src/lib/api/http-client.ts | 28 ++++++++- ui/src/lib/api/types/build.ts | 9 ++- .../lib/components/lineage/asset-node.svelte | 4 +- ui/src/lib/lineage/catalog-graph.ts | 2 + ui/src/lib/lineage/scoped-lineage.svelte | 6 +- .../routes/catalog/[name]/history-tab.svelte | 5 +- ui/src/routes/catalog/[name]/node-view.svelte | 27 +++++---- .../routes/catalog/[name]/overview-tab.svelte | 32 +++++----- ui/src/routes/catalog/[name]/runs-tab.svelte | 2 +- ui/src/routes/lineage/+page.svelte | 32 ++++++---- 13 files changed, 233 insertions(+), 55 deletions(-) diff --git a/ui/src/lib/api/http-catalog.test.ts b/ui/src/lib/api/http-catalog.test.ts index 6c5452fd76..12323431a7 100644 --- a/ui/src/lib/api/http-catalog.test.ts +++ b/ui/src/lib/api/http-catalog.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import type { components } from '$control/schema.d.ts'; -import { toCatalogNodes } from './http-catalog'; +import { findNode, nodeDetailOf, toCatalogNodes } from './http-catalog'; +import { toCheck } from './http-checks'; type W = components['schemas']; @@ -53,4 +54,46 @@ describe('catalog node mapping', () => { expect([orders.description, orders.tags, orders.columnCount, orders.relation]).toEqual(['One row per order', ['marts'], 4, { database: null, schema: 'main', name: 'orders' }]); expect([orders.stale, orders.checks, orders.lastOutcome, orders.group, orders.owner]).toEqual([null, null, null, null, []]); }); + + it('finds a node by key, or by bare name preferring the model', () => { + const nodes = toCatalogNodes([node('source:orders'), node('source:customers'), node('model:orders', { materialization: 'view' })]); + expect([findNode(nodes, 'source:orders')?.key, findNode(nodes, 'orders')?.key, findNode(nodes, 'customers')?.key, findNode(nodes, 'order')]).toEqual(['source:orders', 'model:orders', 'source:customers', null]); + }); +}); + +function check(id: string, target: string, outcome: string): W['CheckResult'] { + return { event_id: `e-${id}`, binding_key: id, audit_name: id, audit_definition_name: 'not_null', attachment_kind: 'model', attached_target_name: target, severity: 'error', evaluation_mode: 'violations', run_scope_phase: 'final', occurred_at: '2026-11-07T06:00:00Z', run_id: 'r', invocation_id: 'i', outcome, reused: 'false' }; +} + +function status(definition: W['NodeStatus']['definition'], physical: W['NodeStatus']['physical']): W['NodeStatus'] { + const build = { definition_hash: 'd', event_id: 'e', observed_at: '2026-11-07T06:00:00Z', run_id: 'sqb-run-1', schema_fingerprint: 's', source_event_id: 'x', version_hash: 'v1' }; + return { node_key: 'model:orders', snapshot_id: 'snap', build, definition, physical, source_data: { status: 'unknown', reason: 'not tracked' } }; +} + +describe('catalog node detail', () => { + const nodes = toCatalogNodes([node('model:orders', { materialization: 'table' }), node('model:order_totals', { materialization: 'view', dependencies: ['model:orders'] }), node('model:orders_by_day', { materialization: 'view' })]); + const checks = [check('a', 'orders', 'pass'), check('b', 'model:orders', 'fail'), check('c', 'orders', 'warn'), check('d', 'orders_by_day', 'fail'), { ...check('e', 'orders', 'fail'), attachment_kind: 'source' }].map(toCheck); + const observed: W['NodeStatus']['physical'] = { status: 'observed', observation: { event_id: 'o', observed_at: '2026-11-07T06:00:00Z', record: { name: 'orders', relation_type: 'table', state_schema: 'main', columns: [{ name: 'order_id', data_type: 'INTEGER' }] } } }; + + it('keeps only its own checks, matched by key or by bare name of the same kind, and counts them by newest outcome', () => { + const d = nodeDetailOf(nodes, nodes[0], null, null, checks); + expect(d.checkResults.map((c) => c.id)).toEqual(['a', 'b', 'c']); + expect(d.checks).toEqual({ total: 3, passing: 1, warning: 1, failing: 1 }); + expect(d.downstream).toEqual(['model:order_totals']); + }); + + it('reports a changed definition as stale and maps the warehouse columns', () => { + const changed = nodeDetailOf(nodes, nodes[0], status({ status: 'changed', current_local_hash: 'b', recorded_local_hash: 'a' }, observed), null, []); + expect(changed.stale.map((r) => [r.reason, r.since])).toEqual([['changed', '2026-11-07T06:00:00Z']]); + expect(changed.columns).toEqual([{ name: 'order_id', type: 'INTEGER', description: '', audits: [] }]); + const matched = nodeDetailOf(nodes, nodes[0], status({ status: 'matched', local_hash: 'a' }, { status: 'unavailable', reason: 'no catalog' }), null, []); + expect([matched.stale, matched.columns]).toEqual([[], []]); + }); + + it('leaves refused SQL out and unreported history unknown rather than empty', () => { + const refused = nodeDetailOf(nodes, nodes[0], null, null, []); + expect([refused.sql, refused.runs, refused.jobs, refused.history]).toEqual([null, null, null, null]); + const withSql = nodeDetailOf(nodes, nodes[0], null, { node_key: 'model:orders', snapshot_id: 'snap', raw_sql: 'select {{ x }}', compiled_sql: null }, []); + expect(withSql.sql).toEqual({ raw: 'select {{ x }}', compiled: 'select {{ x }}' }); + }); }); diff --git a/ui/src/lib/api/http-catalog.ts b/ui/src/lib/api/http-catalog.ts index bba6472397..def4b6e40a 100644 --- a/ui/src/lib/api/http-catalog.ts +++ b/ui/src/lib/api/http-catalog.ts @@ -1,5 +1,5 @@ import type { components } from '$control/schema.d.ts'; -import type { CatalogNode } from './types/build'; +import type { CatalogNode, CatalogNodeDetail, Check, CheckCounts, StaleReason } from './types/build'; import type { NodeKind } from './types/operate'; type Wire = components['schemas']; @@ -48,3 +48,60 @@ export function toCatalogNodes(wire: Wire['CatalogNodeRecord'][]): CatalogNode[] const listed = new Set(nodes.map((node) => node.key)); return nodes.map((node) => ({ ...node, upstream: node.upstream.filter((key) => listed.has(key)) })); } + +/** Passing, warning and failing by each check's newest result; checks without one count only towards the total. */ +export function checkCountsOf(checks: Check[]): CheckCounts { + const outcome = (c: Check) => c.latest?.outcome; + return { + total: checks.length, + passing: checks.filter((c) => outcome(c) === 'pass').length, + warning: checks.filter((c) => outcome(c) === 'warn').length, + failing: checks.filter((c) => ['fail', 'error'].includes(outcome(c) ?? '')).length + }; +} + +/** A definition that changed since its last recorded build here; other staleness is not reported by the server. */ +function staleOf(status: Wire['NodeStatus'] | null): StaleReason[] { + if (status?.definition.status !== 'changed' || !status.build) return []; + return [{ reason: 'changed', change: 'definition', detail: 'The definition changed since it was last built here.', since: status.build.observed_at }]; +} + +/** A node key without its kind, e.g. `customer_summary` for `model:customer_summary`. */ +export const shortName = (key: string) => key.slice(key.indexOf(':') + 1); + +/** The listed node a key or bare name refers to, preferring a model; migrations name models without their kind. */ +export function findNode(nodes: CatalogNode[], ref: string): CatalogNode | null { + return nodes.find((n) => n.key === ref) ?? nodes.find((n) => n.key === `model:${ref}`) ?? nodes.find((n) => shortName(n.key) === ref) ?? null; +} + +/** + * A listed node with its status and SQL reads and its checks. The server keeps no + * per-node run history, row counts, cost, jobs or migrations, so those stay empty or null and the + * pages hide them. `sql` is null when the person may not see SQL. + */ +export function nodeDetailOf(nodes: CatalogNode[], node: CatalogNode, status: Wire['NodeStatus'] | null, sql: Wire['NodeSql'] | null, checks: Check[]): CatalogNodeDetail { + // Checks name their node without its kind, so the attachment tells a source from a model of the same name. + const own = checks.filter((c) => c.node === node.key || `${c.attachment}:${c.node}` === node.key); + const physical = status?.physical.status === 'observed' ? status.physical.observation.record : null; + return { + ...node, + stale: staleOf(status), + checks: checkCountsOf(own), + downstream: nodes.filter((n) => n.upstream.includes(node.key)).map((n) => n.key), + recentOutcomes: [], + lastRows: null, + rowsDelta: null, + lastDurationMs: null, + cost: null, + columns: (physical?.columns ?? []).map((c) => ({ name: c.name, type: c.data_type, description: '', audits: [] })), + sql: sql?.raw_sql ? { raw: sql.raw_sql, compiled: sql.compiled_sql ?? sql.raw_sql } : null, + code: null, + builds: [], + jobs: null, + runs: null, + checkResults: own, + warehouse: null, + microbatch: null, + history: null + }; +} diff --git a/ui/src/lib/api/http-client.test.ts b/ui/src/lib/api/http-client.test.ts index 8d05b8dc33..5c146edf0f 100644 --- a/ui/src/lib/api/http-client.test.ts +++ b/ui/src/lib/api/http-client.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { HttpChangeFeed, feedAllowed } from './http-client'; +import { HttpChangeFeed, createHttpClient, feedAllowed } from './http-client'; import type { ChangedSignal, ConnectionState } from './signals'; /** A stand-in `EventSource` the test opens, fails and feeds by hand. */ @@ -237,3 +237,38 @@ describe('feedAllowed', () => { await expect(feedAllowed('', ORDERS, () => {})).rejects.toThrow('network down'); }); }); + +describe('getCatalogNode', () => { + /** Answers the reads a node page makes; anything else, such as node status, is refused. */ + function serve(actions: string[]) { + const paths: string[] = []; + const answers: [string, unknown][] = [ + ['/bootstrap', { login_mode: 'password', access: { name: 'vic', principal_id: 'p', actions: [], locations: [{ location_id: '0191f0aa-orders', name: 'Orders', target: 'development', actions }] } }], + ['/nodes', [{ node_key: 'model:orders', kind: 'model', name: 'orders', materialization: 'table', tags: [], dependencies: [], snapshot_id: 's' }]], + ['/checks', []], + ['/sql', { node_key: 'model:orders', snapshot_id: 's', raw_sql: 'select 1', compiled_sql: 'select 1' }] + ]; + vi.stubGlobal( + 'fetch', + vi.fn(async (request: Request) => { + const path = new URL(request.url).pathname; + paths.push(path); + const answer = answers.find(([end]) => path.endsWith(end)); + return answer ? Response.json(answer[1]) : Response.json({ error: 'refused' }, { status: 403 }); + }) + ); + return paths; + } + + it.each([ + [['view', 'view.sql'], 'select 1'], + [['view'], null] + ])('with %j reads SQL only when the location allows it, so a viewer causes no refused read', async (actions, raw) => { + const paths = serve(actions); + const client = createHttpClient({ baseUrl: 'http://ui.test', onUnauthorized: () => {} }); + await client.bootstrap(); + const detail = await client.getCatalogNode(ORDERS, 'orders'); + expect([detail?.key, detail?.sql?.raw ?? null]).toEqual(['model:orders', raw]); + expect(paths.filter((p) => p.endsWith('/sql'))).toHaveLength(raw ? 1 : 0); + }); +}); diff --git a/ui/src/lib/api/http-client.ts b/ui/src/lib/api/http-client.ts index 03baee8ca5..ba3c0cf2b6 100644 --- a/ui/src/lib/api/http-client.ts +++ b/ui/src/lib/api/http-client.ts @@ -2,13 +2,13 @@ import createFetch from 'openapi-fetch'; import type { components, paths } from '$control/schema.d.ts'; import { ApiError, type ApiClient } from './client'; import { checkDetailOf, toCheck, toCheckEvidence, toCheckSql } from './http-checks'; -import { toCatalogNodes } from './http-catalog'; +import { findNode, nodeDetailOf, toCatalogNodes } from './http-catalog'; import { toSnapshotChanges } from './http-changes'; import { jobDetailOf, toJob, toTick, toTrigger, triggerDetailOf } from './http-jobs'; import { RUN_WINDOW, runDetailOf, runRowPage, runsMatching, runTabCountsOf, toRunNode, toRuns } from './http-runs'; import type { ChangeSubscription, ChangedChannel, ChangedSignal, ConnectionState } from './signals'; import type { AuthMode, Bootstrap, Capability, Location, Scope } from './types'; -import type { CatalogNode, Check, CheckDetail, CheckEvidence, CheckSql, SnapshotChange } from './types/build'; +import type { CatalogNode, CatalogNodeDetail, Check, CheckDetail, CheckEvidence, CheckSql, SnapshotChange } from './types/build'; import type { Job, JobDetail, RerunMode, Run, RunDetail, RunListQuery, RunRowPage, RunRowQuery, RunTabCounts, Trigger, TriggerDetail, TriggerState } from './types/operate'; type Wire = components['schemas']; @@ -38,6 +38,7 @@ const SUPPORTED: ReadonlySet = new Set([ 'getCheckSql', 'getCheckEvidence', 'listCatalogNodes', + 'getCatalogNode', 'listSnapshotChanges' ]); @@ -186,6 +187,8 @@ export type HttpClientOptions = { */ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOptions): ApiClient { const http = createFetch({ baseUrl, credentials: 'same-origin' }); + // Locations whose bootstrap actions include view.sql; reading SQL without it is refused and audited. + let sqlLocations = new Set(); const implemented = { supports: (method: keyof ApiClient) => SUPPORTED.has(method), changes: new HttpChangeFeed(baseUrl, (scope) => feedAllowed(baseUrl, scope, onUnauthorized)), @@ -193,7 +196,9 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt const { data, error, response } = await http.GET('/api/control/bootstrap'); if (response.status === 401) onUnauthorized(); if (!data) throw new ApiError(errorText(error, response), response.status); - return toBootstrap(data); + const bootstrap = toBootstrap(data); + sqlLocations = new Set(bootstrap.locations.filter((l) => l.capabilities?.includes('view.sql')).map((l) => l.id)); + return bootstrap; }, listRunRows: async (scope: Scope, query: RunRowQuery = {}): Promise => runRowPage(await listedRuns(scope), scope.locationId, query), runTabCounts: async (scope: Scope, query: RunRowQuery = {}): Promise => runTabCountsOf(await listedRuns(scope), scope.locationId, query), @@ -256,6 +261,23 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt if (!snapshots.data) throw new ApiError(errorText(snapshots.error, snapshots.response), snapshots.response.status); return toSnapshotChanges(snapshots.data, runs); }, + async getCatalogNode(scope: Scope, ref: string): Promise { + const nodes = await implemented.listCatalogNodes(scope); + const node = findNode(nodes, ref); + if (!node) return null; + const { locationId, target } = parseLocationKey(scope.locationId); + const path = { location_id: locationId, target }; + const [status, sql, checks] = await Promise.all([ + http.GET('/api/control/locations/{location_id}/targets/{target}/node-status', { params: { path, query: { node_key: node.key } } }), + sqlLocations.has(scope.locationId) + ? http.GET('/api/control/locations/{location_id}/targets/{target}/nodes/{node_key}/sql', { params: { path: { ...path, node_key: node.key } } }) + : null, + checkResults(scope).catch(() => []) + ]); + if (status.response.status === 401 || sql?.response.status === 401) onUnauthorized(); + // Status and SQL have their own permissions; a refusal leaves that part out rather than failing the page. + return nodeDetailOf(nodes, node, status.data ?? null, sql?.data ?? null, checks.map(toCheck)); + }, listChecks: async (scope: Scope): Promise => (await checkResults(scope)).map(toCheck), getCheck: async (scope: Scope, id: string): Promise => checkDetailOf(await checkResults(scope, id)), async getCheckSql(scope: Scope, checkId: string): Promise { diff --git a/ui/src/lib/api/types/build.ts b/ui/src/lib/api/types/build.ts index 2eb5eb15b8..906f9e77df 100644 --- a/ui/src/lib/api/types/build.ts +++ b/ui/src/lib/api/types/build.ts @@ -177,12 +177,15 @@ export type CatalogNodeDetail = Omit & { code: { file: string; source: string } | null; /** As built in this target, newest first. */ builds: BuiltDefinition[]; - jobs: NodeJob[]; - runs: NodeRunEntry[]; + /** Null when the server does not report which jobs select the node. */ + jobs: NodeJob[] | null; + /** Null when the server does not report the node's run history. */ + runs: NodeRunEntry[] | null; checkResults: Check[]; warehouse: WarehouseObject | null; microbatch: MicrobatchCoverage | null; - history: NodeHistory; + /** Null when the server does not report renames and moves. */ + history: NodeHistory | null; }; // ---- column lineage ---- diff --git a/ui/src/lib/components/lineage/asset-node.svelte b/ui/src/lib/components/lineage/asset-node.svelte index 182c52609f..4954794647 100644 --- a/ui/src/lib/components/lineage/asset-node.svelte +++ b/ui/src/lib/components/lineage/asset-node.svelte @@ -22,6 +22,8 @@ type AssetData = { name: string; + /** Shown instead of the key when set; the key stays in the tooltip. */ + label?: string; kind: string; /** The last build outcome; staleness is shown separately. */ status: 'ok' | 'fail' | 'warn' | 'run' | 'src' | 'none'; @@ -98,7 +100,7 @@
- {d.name} + {d.label ?? d.name} {#if f.kind}{kindShort}{/if}
diff --git a/ui/src/lib/lineage/catalog-graph.ts b/ui/src/lib/lineage/catalog-graph.ts index aa96ee033f..ca9ae070bf 100644 --- a/ui/src/lib/lineage/catalog-graph.ts +++ b/ui/src/lib/lineage/catalog-graph.ts @@ -2,6 +2,7 @@ import type { Edge, Node } from '@xyflow/svelte'; import type { CatalogNode, StaleReason } from '$lib/api'; +import { shortName } from '$lib/api/http-catalog'; import { kindLine, nodeIconKey } from '$lib/build-area/labels'; export type CardStatus = 'ok' | 'fail' | 'warn' | 'src' | 'none'; @@ -35,6 +36,7 @@ export function checksBadge(node: CatalogNode): { checks?: string; checksColor?: export function cardData(node: CatalogNode): Record { return { name: node.key, + label: shortName(node.key), kind: kindLine(node), group: node.group, nodeKind: node.kind, diff --git a/ui/src/lib/lineage/scoped-lineage.svelte b/ui/src/lib/lineage/scoped-lineage.svelte index 9db90a9060..127c5ed0d4 100644 --- a/ui/src/lib/lineage/scoped-lineage.svelte +++ b/ui/src/lib/lineage/scoped-lineage.svelte @@ -181,12 +181,12 @@
--select {selectExpr} - + onclick={() => onExecute(effectiveSelection.map((id) => graphNodes.find((n) => n.id === id)?.name ?? id))}>▷ Execute {effectiveSelection.length > 1 ? `(${effectiveSelection.length})` : ''}{/if}
diff --git a/ui/src/routes/catalog/[name]/history-tab.svelte b/ui/src/routes/catalog/[name]/history-tab.svelte index 8cc25043b3..26a1301a63 100644 --- a/ui/src/routes/catalog/[name]/history-tab.svelte +++ b/ui/src/routes/catalog/[name]/history-tab.svelte @@ -1,5 +1,5 @@
diff --git a/ui/src/routes/catalog/[name]/node-view.svelte b/ui/src/routes/catalog/[name]/node-view.svelte index a2c0cec463..0e360812bd 100644 --- a/ui/src/routes/catalog/[name]/node-view.svelte +++ b/ui/src/routes/catalog/[name]/node-view.svelte @@ -29,6 +29,10 @@ const res = createResource((client, scope) => client.getCatalogNode(scope, name), ['runs', 'checks', 'snapshot', 'node_status', 'warehouse_sync']); const n = $derived(res.data ?? null); + // Checks and migrations link nodes by bare name; settle on the node key so the graph and links agree. + $effect(() => { + if (n && n.key !== name) goto(`/catalog/${n.key}${page.url.search}`, { replaceState: true }); + }); const catalog = createResource((client, scope) => client.listCatalogNodes(scope), ['snapshot', 'runs']); const colLineage = createResource((client, scope) => client.getColumnLineage(scope, name), ['snapshot']); @@ -38,13 +42,13 @@ if (n.columns.length) t.push(['columns', 'Columns']); if (n.sql) t.push(['sql', 'SQL']); else if (n.code) t.push(['sql', 'Code']); - if (n.sql) t.push(['drift', 'Drift']); + if (n.sql && n.builds.length) t.push(['drift', 'Drift']); t.push(['lineage', 'Lineage']); - if (n.columns.length) t.push(['collineage', 'Column lineage']); - if (n.kind !== 'source') t.push(['runs', 'Runs']); + if (n.columns.length && app.client.supports('getColumnLineage')) t.push(['collineage', 'Column lineage']); + if (n.kind !== 'source' && n.runs) t.push(['runs', 'Runs']); t.push(['checks', `Checks${n.checkResults.length ? ` ${n.checkResults.length}` : ''}`]); if (n.warehouse) t.push(['warehouse', 'Warehouse']); - t.push(['history', 'History']); + if (n.history) t.push(['history', 'History']); return t; }); // The URL is the single source of truth for the tab, so back/forward and reloads restore it. @@ -69,6 +73,7 @@ }; }); + const canRun = app.client.supports('launchAdhocRun'); let runOpen = $state(false); let runSelection = $state([]); function execute(selection: string[]) { @@ -80,12 +85,12 @@ - {#if n && n.kind !== 'source'} + {#if n && n.kind !== 'source' && canRun} execute([name])}>▷ Build {/if} - +{#if canRun}{/if}
@@ -98,7 +103,7 @@ {:else if n.lastOutcome} - {:else} + {:else if !n.lastBuild} {/if} @@ -109,7 +114,7 @@
{#if n}
- {kindLine(n)}{n.group}{#if n.owner.length}{n.owner.join(', ')}{/if} + {kindLine(n)}{#if n.group}{n.group}{/if}{#if n.owner.length}{n.owner.join(', ')}{/if} {#if n.kind === 'source'}{n.sourceFreshness?.loadedAt ? `Loaded ${formatAgo(n.sourceFreshness.loadedAt, app.now)}` : 'No freshness data'} {:else}{n.lastBuild ? `Last built ${formatAgo(n.lastBuild.builtAt, app.now)}` : 'Never built'}{/if}
@@ -153,7 +158,7 @@ {:else if tab === 'lineage'} {#if graph.nodes.length} - + {/if} {:else if tab === 'collineage'} {#if colLineage.data} @@ -165,8 +170,8 @@ {:else if tab === 'warehouse' && n.warehouse} - {:else if tab === 'history'} - + {:else if tab === 'history' && n.history} + {/if}
{/if} diff --git a/ui/src/routes/catalog/[name]/overview-tab.svelte b/ui/src/routes/catalog/[name]/overview-tab.svelte index 270ad9db38..abf7e75a32 100644 --- a/ui/src/routes/catalog/[name]/overview-tab.svelte +++ b/ui/src/routes/catalog/[name]/overview-tab.svelte @@ -18,6 +18,8 @@ const source = $derived(node.kind === 'source'); const unique = $derived(node.sql?.raw.match(/unique_key\s+([^,\n]+)/)?.[1] ?? null); const freshnessTone = $derived(!node.sourceFreshness ? 'var(--sb-text-faint)' : node.sourceFreshness.status === 'pass' ? 'var(--sb-success)' : node.sourceFreshness.status === 'warn' ? 'var(--sb-warning)' : 'var(--sb-error)'); + // Drift compares the current SQL with the SQL of the last build, so it needs both. + const hasDrift = $derived(!!node.sql && node.builds.length > 0); const minutes = (s: number | null) => (s === null ? '—' : s >= 3600 && s % 3600 === 0 ? `${s / 3600}h` : `${Math.round(s / 60)}m`); @@ -41,24 +43,24 @@ {:else}
- {#if node.lastOutcome}{:else}{/if} + {#if node.lastOutcome}{:else if !node.lastBuild}{/if} Last built:
Rows{formatNumber(node.lastRows)}{#if node.rowsDelta} 0}>{node.rowsDelta > 0 ? '+' : ''}{formatNumber(node.rowsDelta)}{/if}
Duration{formatMs(node.lastDurationMs)}
Checks{node.checks.total ? `${node.checks.passing}/${node.checks.total}` : '—'}
-
Builds · 40{node.recentOutcomes.filter((o) => o === 'failed').length} failed
+ {#if node.recentOutcomes.length}
Builds · {node.recentOutcomes.length}{node.recentOutcomes.filter((o) => o === 'failed').length} failed
{/if} {#if node.cost}
Cost per build{formatCredits(node.cost.perBuild)}{formatCredits(node.cost.last24h)} / 24h
{/if}
{#if node.recentOutcomes.length}
{/if} {#each node.stale as r, i (i)} - {/each}
@@ -122,7 +124,7 @@
Kind
{kindLine(node)}
{#if unique}
Unique key
{unique}
{/if} -
Group
{node.group}
+ {#if node.group}
Group
{node.group}
{/if}
Owner
{node.owner.join(', ') || '—'}
Path
{node.path}
Relation
{relationName(node.relation)}
@@ -136,14 +138,16 @@
{#each node.tags as t (t)}{t}{/each}
{/if} -
-

Jobs

- {#each node.jobs as j (j.key)} - {j.key}{j.kind === 'group' ? 'group' : 'job'}{j.triggerIds.length ? ` · ${j.triggerIds.length} ${j.triggerIds.length === 1 ? 'trigger' : 'triggers'}` : ' · manual'} - {:else} -

No job selects this node.

- {/each} -
+ {#if node.jobs} +
+

Jobs

+ {#each node.jobs as j (j.key)} + {j.key}{j.kind === 'group' ? 'group' : 'job'}{j.triggerIds.length ? ` · ${j.triggerIds.length} ${j.triggerIds.length === 1 ? 'trigger' : 'triggers'}` : ' · manual'} + {:else} +

No job selects this node.

+ {/each} +
+ {/if} @@ -188,7 +192,7 @@ .job-row span { color: var(--primary); } .job-row small { margin-left: auto; color: var(--muted-foreground); font-size: 12px; } .drift-hint { display: flex; width: 100%; align-items: center; gap: 9px; margin-top: 12px; padding: 8px 10px; border: 1px solid color-mix(in srgb, var(--sb-stale) 35%, var(--border)); border-radius: 2px; background: transparent; color: var(--foreground); font-size: 12.5px; text-align: left; } - .drift-hint:hover { background: var(--sb-hover); } + .drift-hint:not(:disabled):hover { background: var(--sb-hover); } .drift-hint :global(svg) { flex: 0 0 auto; color: var(--sb-stale); } .drift-hint.changed_upstream { border-style: dashed; } .drift-hint.behind { border-color: color-mix(in srgb, var(--sb-warning) 35%, var(--border)); } diff --git a/ui/src/routes/catalog/[name]/runs-tab.svelte b/ui/src/routes/catalog/[name]/runs-tab.svelte index c736199e6b..23005d37ab 100644 --- a/ui/src/routes/catalog/[name]/runs-tab.svelte +++ b/ui/src/routes/catalog/[name]/runs-tab.svelte @@ -15,7 +15,7 @@
- {#each node.runs as r, i (`${r.runId ?? r.fingerprintRunId}-${i}`)} + {#each node.runs ?? [] as r, i (`${r.runId ?? r.fingerprintRunId}-${i}`)}
OutcomeRunJobRowsDurationStarted
diff --git a/ui/src/routes/lineage/+page.svelte b/ui/src/routes/lineage/+page.svelte index 3bb38719e1..18702af834 100644 --- a/ui/src/routes/lineage/+page.svelte +++ b/ui/src/routes/lineage/+page.svelte @@ -63,7 +63,7 @@ untrack(() => { if (!nodes.length) { const graph = catalogGraph(data); - const focus = data.some((n) => n.key === initialFocus) ? initialFocus : 'fact_orders'; + const focus = data.some((n) => n.key === initialFocus) ? initialFocus : null; nodes = graph.nodes.map((n) => (n.id === focus ? { ...n, selected: true } : n)); edges = graph.edges; return; @@ -83,7 +83,7 @@ const scaleModels = Number(page.url.searchParams.get('models') ?? 0); const scaleMode = scaleModels > 0; const FAR_ZOOM = 0.5; - let scaleStartId = 'fact_orders'; + let scaleStartId = ''; const scaleLayout = scaleMode ? (() => { const synthetic = scaleGraph(scaleModels); const t = performance.now(); @@ -253,6 +253,8 @@ const listed = $derived(detailSupported ? null : (catalogByKey.get(String(primarySlug)) ?? null)); const isSource = $derived(d?.kind === 'source'); const hasSql = $derived(!!d?.sql); + // Drift compares the current SQL with the SQL of the last build, so it needs both. + const hasDrift = $derived(hasSql && !!d?.builds.length); // ---- toolbar filters: non-matching nodes are dimmed ---- let kindFilter = $state(''); @@ -484,7 +486,7 @@ {#each browserNodes.slice(0, 8) as node (node.id)} {:else} @@ -496,8 +498,8 @@ {#if !scaleMode}
- - + {#if groupOptions.length}{/if} + {#if ownerOptions.length}{/if} ({ ...o, count: catalogNodes.filter((c) => matchesFilterStatus(c, o.value)).length }))} />
{/if} @@ -550,7 +552,7 @@ title={String(node.data.name)} > - {node.data.name} + {node.data.label ?? node.data.name} {/each} {/if} @@ -621,16 +623,19 @@ {:else}
- {primary?.data.name} + {primary?.data.label ?? primary?.data.name} {#if d}Catalog ↗{/if}
{d ? kindLine(d) : primary?.data.kind} - {#if d}{d.group}{#if d.owner.length}{d.owner.join(', ')}{/if}{/if} + {#if d}{#if d.group}{d.group}{/if}{#if d.owner.length}{d.owner.join(', ')}{/if}{/if}
{/if}
+ {#if !scaleMode && !primary} +

Select a node to see its details.

+ {:else} {#if scaleMode}

A synthetic graph for layout and rendering at scale; nodes have no catalog detail.

@@ -660,7 +665,7 @@ {#snippet meta()}{#if d.lastBuild?.runId}View run →{/if}{/snippet}
- {#if d.lastOutcome}{:else}{/if} + {#if d.lastOutcome}{:else if !d.lastBuild}{/if}
@@ -673,10 +678,10 @@
{/if} {#each d.stale as r, i (i)} - {/each}
@@ -728,7 +733,7 @@ {/if} - {#if d && !isSource} + {#if d && !isSource && d.runs} {#snippet meta()}All →{/snippet} {#each d.runs.slice(0, 3) as run, i (i)} @@ -767,13 +772,14 @@
Kind
{kindLine(d)}
Unique key
{sqlOutline.config.find((c) => c.key === 'unique_key')?.value ?? '—'}
-
Group
{d.group}
+
Group
{d.group ?? '—'}
Owner
{d.owner.join(', ') || '—'}
Tags
{d.tags.join(', ') || '—'}
Build version
{d.lastBuild?.fingerprint.slice(0, 12) ?? '—'}
{/if} + {/if}
From f729c4c064767ff9584cae578d49ce1ecee7cb02 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 16:37:28 +0000 Subject: [PATCH 058/106] fix(ui): ask for refused node SQL once until reload view.sql in a location's bootstrap actions may hold on only some nodes, so the node SQL read can still be refused. Each refusal is audited and node pages refetch on change signals, so remember the refused node and skip its SQL read until the page reloads. --- ui/src/lib/api/http-client.test.ts | 14 ++++++++++++-- ui/src/lib/api/http-client.ts | 7 +++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/ui/src/lib/api/http-client.test.ts b/ui/src/lib/api/http-client.test.ts index 5c146edf0f..2105944003 100644 --- a/ui/src/lib/api/http-client.test.ts +++ b/ui/src/lib/api/http-client.test.ts @@ -240,7 +240,7 @@ describe('feedAllowed', () => { describe('getCatalogNode', () => { /** Answers the reads a node page makes; anything else, such as node status, is refused. */ - function serve(actions: string[]) { + function serve(actions: string[], sqlStatus = 200) { const paths: string[] = []; const answers: [string, unknown][] = [ ['/bootstrap', { login_mode: 'password', access: { name: 'vic', principal_id: 'p', actions: [], locations: [{ location_id: '0191f0aa-orders', name: 'Orders', target: 'development', actions }] } }], @@ -254,7 +254,8 @@ describe('getCatalogNode', () => { const path = new URL(request.url).pathname; paths.push(path); const answer = answers.find(([end]) => path.endsWith(end)); - return answer ? Response.json(answer[1]) : Response.json({ error: 'refused' }, { status: 403 }); + const refused = !answer || (answer[0] === '/sql' && sqlStatus === 403); + return refused ? Response.json({ error: 'refused' }, { status: 403 }) : Response.json(answer[1]); }) ); return paths; @@ -271,4 +272,13 @@ describe('getCatalogNode', () => { expect([detail?.key, detail?.sql?.raw ?? null]).toEqual(['model:orders', raw]); expect(paths.filter((p) => p.endsWith('/sql'))).toHaveLength(raw ? 1 : 0); }); + + it('asks for a refused node SQL once, when view.sql holds on only some nodes', async () => { + const paths = serve(['view', 'view.sql'], 403); + const client = createHttpClient({ baseUrl: 'http://ui.test', onUnauthorized: () => {} }); + await client.bootstrap(); + const loads = [await client.getCatalogNode(ORDERS, 'orders'), await client.getCatalogNode(ORDERS, 'orders')]; + expect(loads.map((d) => [d?.key, d?.sql])).toEqual([['model:orders', null], ['model:orders', null]]); + expect(paths.filter((p) => p.endsWith('/sql'))).toHaveLength(1); + }); }); diff --git a/ui/src/lib/api/http-client.ts b/ui/src/lib/api/http-client.ts index ba3c0cf2b6..952caa2133 100644 --- a/ui/src/lib/api/http-client.ts +++ b/ui/src/lib/api/http-client.ts @@ -187,8 +187,10 @@ export type HttpClientOptions = { */ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOptions): ApiClient { const http = createFetch({ baseUrl, credentials: 'same-origin' }); - // Locations whose bootstrap actions include view.sql; reading SQL without it is refused and audited. + // Locations whose bootstrap actions include view.sql, which may hold on only some nodes, and the + // nodes there whose SQL was refused; each refused read is audited, so neither is asked again until reload. let sqlLocations = new Set(); + const sqlRefused = new Set(); const implemented = { supports: (method: keyof ApiClient) => SUPPORTED.has(method), changes: new HttpChangeFeed(baseUrl, (scope) => feedAllowed(baseUrl, scope, onUnauthorized)), @@ -269,12 +271,13 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt const path = { location_id: locationId, target }; const [status, sql, checks] = await Promise.all([ http.GET('/api/control/locations/{location_id}/targets/{target}/node-status', { params: { path, query: { node_key: node.key } } }), - sqlLocations.has(scope.locationId) + sqlLocations.has(scope.locationId) && !sqlRefused.has(`${scope.locationId}|${node.key}`) ? http.GET('/api/control/locations/{location_id}/targets/{target}/nodes/{node_key}/sql', { params: { path: { ...path, node_key: node.key } } }) : null, checkResults(scope).catch(() => []) ]); if (status.response.status === 401 || sql?.response.status === 401) onUnauthorized(); + if (sql?.response.status === 403) sqlRefused.add(`${scope.locationId}|${node.key}`); // Status and SQL have their own permissions; a refusal leaves that part out rather than failing the page. return nodeDetailOf(nodes, node, status.data ?? null, sql?.data ?? null, checks.map(toCheck)); }, From 5b2b272098e80b339f980136ab79eb19ab900e3f Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 16:41:50 +0000 Subject: [PATCH 059/106] fix(rules): keep columns after a wrapped table function in rule trees When no space or tab follows a Snowflake table function call, the rule tree inserts its closing parenthesis. Write the TABLE( ) wrapper over one more padding space in that case, so text after the call keeps its column. --- .../src/sql_tree/_helpers/table_wrapped.rs | 10 +++------- .../src/sql_tree/tests/test_sql_tree.rs | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/crates/sqlbuild-analysis/src/sql_tree/_helpers/table_wrapped.rs b/crates/sqlbuild-analysis/src/sql_tree/_helpers/table_wrapped.rs index bd0779da30..f55eeb3381 100644 --- a/crates/sqlbuild-analysis/src/sql_tree/_helpers/table_wrapped.rs +++ b/crates/sqlbuild-analysis/src/sql_tree/_helpers/table_wrapped.rs @@ -10,7 +10,7 @@ use sqlbuild_sqltext::sql_scan::models::LexicalSyntax; use crate::relation_markers::constants::{MARKER_NAME_PREFIX, TABLE_FUNCTION_MARKER}; use crate::relation_markers::types::MarkerCall; -/// `named` with each table function marker call in `TABLE( )`, written over adjacent spaces where present. +/// `named` with each table function marker call in `TABLE( )`, written over padding so later text keeps its column. pub(crate) fn table_wrapped(named: &str, calls: &[MarkerCall], syntax: &LexicalSyntax) -> String { let bytes = named.as_bytes(); let mut edits: Vec<(usize, usize, String)> = Vec::new(); @@ -38,14 +38,10 @@ pub(crate) fn table_wrapped(named: &str, calls: &[MarkerCall], syntax: &LexicalS .iter() .take_while(|byte| **byte == b' ') .count(); - let borrowed = if spaces >= TABLE_WRAPPER.len() { - TABLE_WRAPPER.len() - } else { - 0 - }; - edits.push((start, name_end + borrowed, format!("{TABLE_WRAPPER}{name}"))); let after = close + 1; let replaced = usize::from(matches!(bytes.get(after), Some(b' ' | b'\t'))); + let borrowed = spaces.min(TABLE_WRAPPER.len() + 1 - replaced); + edits.push((start, name_end + borrowed, format!("{TABLE_WRAPPER}{name}"))); edits.push((after, after + replaced, ")".to_owned())); } edits.sort_by_key(|(start, _, _)| Reverse(*start)); diff --git a/crates/sqlbuild-analysis/src/sql_tree/tests/test_sql_tree.rs b/crates/sqlbuild-analysis/src/sql_tree/tests/test_sql_tree.rs index 9d1d337ff3..21fda1db44 100644 --- a/crates/sqlbuild-analysis/src/sql_tree/tests/test_sql_tree.rs +++ b/crates/sqlbuild-analysis/src/sql_tree/tests/test_sql_tree.rs @@ -79,6 +79,20 @@ fn given_sql_when_building_a_rule_tree_then_clauses_own_their_queries_at_their_s ("subquery", 2, 66, Some("join")), ], }, + TreeShapeTestCase { + description: "a Snowflake table function marker closing a subquery keeps the columns after it", + dialect: "snowflake", + sql: "SELECT 1\nFROM (SELECT * FROM __table_fn(\"f\")(1)) t JOIN (SELECT 2 AS k) s ON s.k = t.k", + expected_nodes: vec![ + ("select", 1, 1, None), + ("select", 2, 6, Some("subquery")), + ("star", 2, 6, Some("select")), + ("subquery", 2, 6, Some("select")), + ("join", 2, 43, Some("select")), + ("select", 2, 48, Some("subquery")), + ("subquery", 2, 48, Some("join")), + ], + }, TreeShapeTestCase { description: "a Snowflake table function marker already inside TABLE( ) is not wrapped twice", dialect: "snowflake", From 001d78c60a69a65fb84a5b19bbbbc9aea7ee457b Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 16:42:16 +0000 Subject: [PATCH 060/106] fix(ui): wrap SQL previews and drop the kind prefix from SQL file labels Preview lines scrolled sideways with no visible scrollbar, so long lines looked cut off; previews now wrap and the full viewer still scrolls. SQL file labels show the node name, like the graph cards. --- ui/src/lib/api/http-catalog.ts | 3 +-- ui/src/lib/build-area/labels.ts | 3 +++ ui/src/lib/components/code/code-preview.svelte | 7 ++++--- ui/src/lib/lineage/catalog-graph.ts | 3 +-- ui/src/routes/catalog/[name]/node-view.svelte | 6 +++--- ui/src/routes/catalog/[name]/overview-tab.svelte | 4 ++-- ui/src/routes/lineage/+page.svelte | 6 +++--- 7 files changed, 17 insertions(+), 15 deletions(-) diff --git a/ui/src/lib/api/http-catalog.ts b/ui/src/lib/api/http-catalog.ts index def4b6e40a..5c9c492fbd 100644 --- a/ui/src/lib/api/http-catalog.ts +++ b/ui/src/lib/api/http-catalog.ts @@ -1,4 +1,5 @@ import type { components } from '$control/schema.d.ts'; +import { shortName } from '$lib/build-area/labels'; import type { CatalogNode, CatalogNodeDetail, Check, CheckCounts, StaleReason } from './types/build'; import type { NodeKind } from './types/operate'; @@ -66,8 +67,6 @@ function staleOf(status: Wire['NodeStatus'] | null): StaleReason[] { return [{ reason: 'changed', change: 'definition', detail: 'The definition changed since it was last built here.', since: status.build.observed_at }]; } -/** A node key without its kind, e.g. `customer_summary` for `model:customer_summary`. */ -export const shortName = (key: string) => key.slice(key.indexOf(':') + 1); /** The listed node a key or bare name refers to, preferring a model; migrations name models without their kind. */ export function findNode(nodes: CatalogNode[], ref: string): CatalogNode | null { diff --git a/ui/src/lib/build-area/labels.ts b/ui/src/lib/build-area/labels.ts index b0bbdb1b2b..c6e5747701 100644 --- a/ui/src/lib/build-area/labels.ts +++ b/ui/src/lib/build-area/labels.ts @@ -174,3 +174,6 @@ export function relationName(relation: { database: string | null; schema: string if (!relation) return '—'; return [relation.database, relation.schema, relation.name].filter(Boolean).join('.'); } + +/** A node key without its kind, e.g. `customer_summary` for `model:customer_summary`. */ +export const shortName = (key: string) => key.slice(key.indexOf(':') + 1); diff --git a/ui/src/lib/components/code/code-preview.svelte b/ui/src/lib/components/code/code-preview.svelte index 326d87c0cd..8cd063d2fa 100644 --- a/ui/src/lib/components/code/code-preview.svelte +++ b/ui/src/lib/components/code/code-preview.svelte @@ -114,15 +114,14 @@ .ml-auto { margin-left: auto; } + /* A preview is read at a glance, so long lines wrap; the full viewer scrolls. */ .body { padding: 8px 0; - overflow-x: auto; color: var(--sb-mono); font: 12px/1.7 var(--font-mono); } .line { display: flex; - min-width: max-content; padding-right: 12px; } .ln { @@ -134,8 +133,10 @@ user-select: none; } .line code { + min-width: 0; font: inherit; - white-space: pre; + white-space: pre-wrap; + overflow-wrap: anywhere; } .more { display: block; diff --git a/ui/src/lib/lineage/catalog-graph.ts b/ui/src/lib/lineage/catalog-graph.ts index ca9ae070bf..04ff84ddda 100644 --- a/ui/src/lib/lineage/catalog-graph.ts +++ b/ui/src/lib/lineage/catalog-graph.ts @@ -2,8 +2,7 @@ import type { Edge, Node } from '@xyflow/svelte'; import type { CatalogNode, StaleReason } from '$lib/api'; -import { shortName } from '$lib/api/http-catalog'; -import { kindLine, nodeIconKey } from '$lib/build-area/labels'; +import { kindLine, nodeIconKey, shortName } from '$lib/build-area/labels'; export type CardStatus = 'ok' | 'fail' | 'warn' | 'src' | 'none'; export type CardStale = 'changed' | 'compiled' | 'upstream' | 'behind'; diff --git a/ui/src/routes/catalog/[name]/node-view.svelte b/ui/src/routes/catalog/[name]/node-view.svelte index 0e360812bd..1191e7eb7a 100644 --- a/ui/src/routes/catalog/[name]/node-view.svelte +++ b/ui/src/routes/catalog/[name]/node-view.svelte @@ -15,7 +15,7 @@ import { createResource } from '$lib/state/resource.svelte'; import { formatAgo } from '$lib/format/time'; import { nodeOutcomeLabel, nodeOutcomeTone } from '$lib/status'; - import { kindLine, nodeIcon, nodeIconKey, nodeKindPlural, sqlVersions } from '$lib/build-area/labels'; + import { kindLine, nodeIcon, nodeIconKey, nodeKindPlural, shortName, sqlVersions } from '$lib/build-area/labels'; import { cardData } from '$lib/lineage/catalog-graph'; import OverviewTab from './overview-tab.svelte'; import DriftTab from './drift-tab.svelte'; @@ -148,13 +148,13 @@
{:else if tab === 'sql'} {#if n.sql} - goto(`/catalog/${m}?tab=sql`)} /> + goto(`/catalog/${m}?tab=sql`)} /> {:else if n.code}
{/if} {:else if tab === 'drift' && n.sql} - goto(`/catalog/${m}?tab=drift`)} /> + goto(`/catalog/${m}?tab=drift`)} /> {:else if tab === 'lineage'} {#if graph.nodes.length} diff --git a/ui/src/routes/catalog/[name]/overview-tab.svelte b/ui/src/routes/catalog/[name]/overview-tab.svelte index abf7e75a32..63e874ca49 100644 --- a/ui/src/routes/catalog/[name]/overview-tab.svelte +++ b/ui/src/routes/catalog/[name]/overview-tab.svelte @@ -8,7 +8,7 @@ import { app } from '$lib/state/app.svelte'; import { formatAgo, formatStamp } from '$lib/format/time'; import { checkOutcomeLabel, checkOutcomeTone, nodeOutcomeTone, staleReasonDetail, staleReasonLabel } from '$lib/status'; - import { formatCredits, formatMs, formatNumber, kindLine, relationName, runScopePhaseLabel } from '$lib/build-area/labels'; + import { formatCredits, formatMs, formatNumber, kindLine, relationName, runScopePhaseLabel, shortName } from '$lib/build-area/labels'; import { driftLabel, driftOf, type Tab } from './utils'; let { node, onTab, onSql }: { node: CatalogNodeDetail; onTab: (tab: Tab) => void; onSql: (mode: 'raw' | 'compiled' | 'drift') => void } = $props(); @@ -75,7 +75,7 @@ {#if node.sql}
- onSql(sqlMode)} /> + onSql(sqlMode)} />
{:else if node.code}
diff --git a/ui/src/routes/lineage/+page.svelte b/ui/src/routes/lineage/+page.svelte index 18702af834..51ace19edc 100644 --- a/ui/src/routes/lineage/+page.svelte +++ b/ui/src/routes/lineage/+page.svelte @@ -38,7 +38,7 @@ import { createResource } from '$lib/state/resource.svelte'; import { formatAgo, formatStamp } from '$lib/format/time'; import { checkOutcomeTone, nodeOutcomeLabel, nodeOutcomeTone, staleReasonDetail, staleReasonLabel } from '$lib/status'; - import { NODE_KINDS, formatCredits, formatMs, formatNumber, kindLine, nodeKindPlural, relationName, runScopePhaseLabel, shortRunId, sqlVersions } from '$lib/build-area/labels'; + import { NODE_KINDS, formatCredits, formatMs, formatNumber, kindLine, nodeKindPlural, relationName, runScopePhaseLabel, shortName, shortRunId, sqlVersions } from '$lib/build-area/labels'; import { catalogGraph, cardData } from '$lib/lineage/catalog-graph'; let runOpen = $state(false); @@ -594,7 +594,7 @@ {#if sqlOpen && d?.sql}
- (sqlOpen = false)} /> + (sqlOpen = false)} />
{/if} @@ -702,7 +702,7 @@ {#if d?.sql} {#snippet meta()}{sqlRaw.split('\n').length} lines{/snippet} - openSql()} /> + openSql()} /> {:else if d?.code} From 2994b2a064db2caf1a6e7f23315f13410683f456 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 17:06:31 +0000 Subject: [PATCH 061/106] feat: publish column lineage and show it on catalog node pages Location publication now adds a lineage snapshot document: the compiler's FAST column lineage edges for every model, as source and target node keys, columns and transform kind. A person read returns the edges reaching or leaving one node, with each node's inferred columns, behind the same view-on-every-node gate as the catalog listing. It carries no SQL or expressions, which need view.sql. The catalog node page's Column lineage tab maps the read onto the existing column lineage view. --- clients/typescript/src/schema.d.ts | 81 +++++++++ .../authentication/password_sessions.rs | 4 + .../_helpers/resources/person_operations.rs | 27 ++- .../sqlbuild-orchestration/src/api/models.rs | 1 + .../_helpers/catalog_nodes.rs | 126 +++++++++++++- .../src/warehouse_metadata/main/mod.rs | 1 + .../main/read_person_column_lineage.rs | 51 ++++++ .../src/warehouse_metadata/models.rs | 32 ++++ .../tests/api/test_column_lineage.rs | 156 ++++++++++++++++++ .../tests/api/test_contracts.rs | 2 + .../tests/api/test_types.rs | 7 + .../bindings/_helpers/location_command/mod.rs | 1 + .../_helpers/location_command/snapshot.rs | 7 +- .../location_command/snapshot_lineage.rs | 39 +++++ .../compiler/compiled_project/_test_types.py | 7 + .../test_orchestration_snapshot.py | 54 ++++++ ui/src/lib/api/http-catalog.test.ts | 37 ++++- ui/src/lib/api/http-catalog.ts | 28 +++- ui/src/lib/api/http-client.ts | 17 +- ui/src/lib/api/types/build.ts | 2 +- .../components/lineage/col-cell-node.svelte | 5 +- ui/src/lib/lineage/column-lineage.svelte | 4 +- ui/src/routes/catalog/[name]/node-view.svelte | 2 +- 23 files changed, 676 insertions(+), 15 deletions(-) create mode 100644 crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_column_lineage.rs create mode 100644 crates/sqlbuild-orchestration/tests/api/test_column_lineage.rs create mode 100644 crates/sqlbuild-python/src/bindings/_helpers/location_command/snapshot_lineage.rs diff --git a/clients/typescript/src/schema.d.ts b/clients/typescript/src/schema.d.ts index 2aebecfe52..57eb4df072 100644 --- a/clients/typescript/src/schema.d.ts +++ b/clients/typescript/src/schema.d.ts @@ -306,6 +306,22 @@ export interface paths { patch?: never; trace?: never; }; + "/api/control/locations/{location_id}/targets/{target}/column-lineage": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["readPersonColumnLineage"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/control/locations/{location_id}/targets/{target}/connection-tests": { parameters: { query?: never; @@ -1106,6 +1122,21 @@ export interface components { CliLoginStartRequest: { client_hash: string; }; + /** @description One published column lineage edge; `transform` is the compiler's transform kind, such as `direct` or `cast`. */ + ColumnLineageEdge: { + source_column: string; + source_key: string; + target_column: string; + target_key: string; + transform: string; + }; + /** @description The column lineage reaching or leaving one node, from the current snapshot; empty before one publishes lineage. */ + ColumnLineageRead: { + edges: components["schemas"]["ColumnLineageEdge"][]; + nodes: components["schemas"]["LineageNodeColumns"][]; + /** Format: uuid */ + snapshot_id?: string | null; + }; /** @description A row whose key was already stored with different content; the first row is kept. */ ConflictEvidence: { existing_sha256: string; @@ -1236,6 +1267,16 @@ export interface components { target: string; window?: null | components["schemas"]["RunWindow"]; }; + LineageColumn: { + data_type?: string | null; + name: string; + }; + /** @description A node's columns as its compiled definition infers them; `data_type` is absent when inference could not tell. */ + LineageNodeColumns: { + columns: components["schemas"]["LineageColumn"][]; + kind: string; + node_key: string; + }; /** @description One location target the person can view, with the actions allowed on at least one of its nodes. */ LocationAccess: { actions: components["schemas"]["Action"][]; @@ -2854,6 +2895,46 @@ export interface operations { }; }; }; + readPersonColumnLineage: { + parameters: { + query: { + /** @description The node whose upstream and downstream column lineage to return */ + node_key: string; + }; + header?: never; + path: { + location_id: string; + target: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ColumnLineageRead"]; + }; + }; + /** @description Session rejected */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + }; + }; requestConnectionTest: { parameters: { query?: never; diff --git a/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs b/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs index 5bce2d5b43..efdcea296b 100644 --- a/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs +++ b/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs @@ -121,6 +121,10 @@ pub(crate) fn person_routes() -> Router { "/api/control/locations/{location_id}/targets/{target}/snapshots", get(crate::api::_helpers::resources::person_reads::snapshots), ) + .route( + "/api/control/locations/{location_id}/targets/{target}/column-lineage", + get(crate::api::_helpers::resources::person_operations::column_lineage), + ) .route( "/api/control/locations/{location_id}/targets/{target}/catalog", get(crate::api::_helpers::resources::person_operations::catalog), diff --git a/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_operations.rs b/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_operations.rs index 02fcaff030..77328d67bc 100644 --- a/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_operations.rs +++ b/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_operations.rs @@ -17,12 +17,13 @@ use crate::runs::main::request_rerun::request_rerun; use crate::runs::models::{CancelRunRequest, JobRunRequest, RerunRequest, RunRecord}; use crate::store::models::ControlStore; use crate::warehouse_metadata::main::read_person_catalog::read_person_catalog; +use crate::warehouse_metadata::main::read_person_column_lineage::read_person_column_lineage; use crate::warehouse_metadata::main::read_person_fingerprint::read_person_fingerprint; use crate::warehouse_metadata::main::read_person_node_status::read_person_node_status; use crate::warehouse_metadata::main::read_person_statistics::read_person_statistics; use crate::warehouse_metadata::models::{ - CatalogPage, CatalogQuery, FingerprintObservation, FingerprintQuery, NodeStatus, - NodeStatusQuery, StatisticsPage, StatisticsQuery, StatisticsRetention, + CatalogPage, CatalogQuery, ColumnLineageRead, FingerprintObservation, FingerprintQuery, + NodeStatus, NodeStatusQuery, StatisticsPage, StatisticsQuery, StatisticsRetention, }; #[utoipa::path(get, path = "/api/control/locations/{location_id}/targets/{target}/statistics", operation_id = "readPersonStatistics", @@ -184,6 +185,28 @@ pub(crate) async fn fingerprint( )) } +#[utoipa::path(get, path = "/api/control/locations/{location_id}/targets/{target}/column-lineage", operation_id = "readPersonColumnLineage", + params(("location_id" = Uuid, Path), ("target" = String, Path), + ("node_key" = String, Query, description = "The node whose upstream and downstream column lineage to return")), + responses((status = 200, body = ColumnLineageRead), (status = 401, description = "Session rejected"), + (status = 403, body = crate::api::models::ApiErrorBody)), security(("session" = []), ("cliSession" = [])))] +pub(crate) async fn column_lineage( + (State(store), Extension(context)): (State, Extension), + Extension(default_role): Extension, + (Path((location_id, target)), Query(options)): (Path<(Uuid, String)>, Query), + Extension(AuthenticatedPerson(principal, _)): Extension, +) -> Result, ApiError> { + Ok(Json( + read_person_column_lineage( + &store, + (&context, principal), + (location_id, &target, &options.node_key), + default_role, + ) + .await?, + )) +} + #[utoipa::path(get, path = "/api/control/locations/{location_id}/targets/{target}/node-status", operation_id = "readPersonNodeStatus", params(("location_id" = Uuid, Path), ("target" = String, Path), ("node_key" = String, Query, description = "One exact published node key")), diff --git a/crates/sqlbuild-orchestration/src/api/models.rs b/crates/sqlbuild-orchestration/src/api/models.rs index 9571345a46..7d638ea6c9 100644 --- a/crates/sqlbuild-orchestration/src/api/models.rs +++ b/crates/sqlbuild-orchestration/src/api/models.rs @@ -30,6 +30,7 @@ use crate::api::_helpers::resources::{ person_operations::statistics, person_operations::fingerprint, person_operations::node_status, + person_operations::column_lineage, person_checks::list, person_checks::history, person_checks::sql, diff --git a/crates/sqlbuild-orchestration/src/warehouse_metadata/_helpers/catalog_nodes.rs b/crates/sqlbuild-orchestration/src/warehouse_metadata/_helpers/catalog_nodes.rs index a4b23af9ff..81afc05c96 100644 --- a/crates/sqlbuild-orchestration/src/warehouse_metadata/_helpers/catalog_nodes.rs +++ b/crates/sqlbuild-orchestration/src/warehouse_metadata/_helpers/catalog_nodes.rs @@ -1,11 +1,16 @@ -//! Catalog listing rows from snapshot node definitions and each node's latest build. +//! Catalog listing rows and column lineage from snapshot node definitions and published edges. + +use std::collections::{BTreeMap, BTreeSet, HashMap}; use serde_json::Value; use uuid::Uuid; use crate::sensors::constants::SOURCE_NODE_KIND; use crate::snapshots::models::SnapshotNode; -use crate::warehouse_metadata::models::{CatalogNodeBuild, CatalogNodeRecord, CatalogRelation}; +use crate::warehouse_metadata::models::{ + CatalogNodeBuild, CatalogNodeRecord, CatalogRelation, ColumnLineageEdge, LineageColumn, + LineageNodeColumns, +}; /// A declared value from the definition itself, its schema entry or its config, in that order. fn declared<'a>(definition: &'a Value, key: &str) -> Option<&'a Value> { @@ -78,3 +83,120 @@ pub(crate) fn catalog_node( snapshot_id, } } + +/// Published edge rows whose both ends are current nodes, so the graph never points at a hidden node. +pub(crate) fn published_edges( + document: Option<&Value>, + nodes: &BTreeMap, +) -> Vec { + let rows = document + .and_then(|document| document.get("edges")) + .and_then(Value::as_array); + rows.into_iter() + .flatten() + .filter_map(edge_row) + .filter(|edge| nodes.contains_key(&edge.source_key) && nodes.contains_key(&edge.target_key)) + .collect() +} + +fn edge_row(row: &Value) -> Option { + let [ + source_key, + source_column, + target_key, + target_column, + transform, + ] = row.as_array()?.as_slice() + else { + return None; + }; + let text = |value: &Value| value.as_str().map(str::to_owned); + Some(ColumnLineageEdge { + source_key: text(source_key)?, + source_column: text(source_column)?, + target_key: text(target_key)?, + target_column: text(target_column)?, + transform: text(transform)?, + }) +} + +/// Edges reached upstream and downstream from any column of `node_key`, each once, in published order. +pub(crate) fn neighbourhood(edges: &[ColumnLineageEdge], node_key: &str) -> Vec { + let mut reached: BTreeSet = walk(edges, node_key, true); + reached.extend(walk(edges, node_key, false)); + reached + .into_iter() + .map(|index| edges[index].clone()) + .collect() +} + +/// The edges followed from the node's columns towards sources (`upstream`) or consumers. +fn walk(edges: &[ColumnLineageEdge], node_key: &str, upstream: bool) -> BTreeSet { + let mut reached: BTreeSet = BTreeSet::new(); + let mut by_near: HashMap<(&str, &str), Vec> = HashMap::new(); + for (index, edge) in edges.iter().enumerate() { + by_near.entry(end(edge, !upstream)).or_default().push(index); + } + let mut pending: Vec<(&str, &str)> = by_near + .keys() + .filter(|(key, _)| *key == node_key) + .copied() + .collect(); + let mut seen: BTreeSet<(&str, &str)> = pending.iter().copied().collect(); + while let Some(column) = pending.pop() { + for &index in by_near.get(&column).into_iter().flatten() { + reached.insert(index); + let far = end(&edges[index], upstream); + if seen.insert(far) { + pending.push(far); + } + } + } + reached +} + +/// An edge's source column, or its target column when `source` is false. +fn end(edge: &ColumnLineageEdge, source: bool) -> (&str, &str) { + if source { + (&edge.source_key, &edge.source_column) + } else { + (&edge.target_key, &edge.target_column) + } +} + +/// The inferred columns of the focal node and of every node an edge touches, in node key order. +pub(crate) fn node_columns( + nodes: &BTreeMap, + node_key: &str, + edges: &[ColumnLineageEdge], +) -> Vec { + let mut keys: BTreeSet<&str> = BTreeSet::from([node_key]); + for edge in edges { + keys.extend([edge.source_key.as_str(), edge.target_key.as_str()]); + } + keys.into_iter() + .filter_map(|key| nodes.get_key_value(key)) + .map(|(key, node)| LineageNodeColumns { + node_key: key.clone(), + kind: node.kind.clone(), + columns: definition_columns(&node.definition), + }) + .collect() +} + +fn definition_columns(definition: &Value) -> Vec { + let columns = definition.get("columns").and_then(Value::as_array); + columns + .into_iter() + .flatten() + .filter_map(|column| { + Some(LineageColumn { + name: column.get("name")?.as_str()?.to_owned(), + data_type: column + .get("type") + .and_then(Value::as_str) + .map(str::to_owned), + }) + }) + .collect() +} diff --git a/crates/sqlbuild-orchestration/src/warehouse_metadata/main/mod.rs b/crates/sqlbuild-orchestration/src/warehouse_metadata/main/mod.rs index f93dd8430f..f3f9f53dc5 100644 --- a/crates/sqlbuild-orchestration/src/warehouse_metadata/main/mod.rs +++ b/crates/sqlbuild-orchestration/src/warehouse_metadata/main/mod.rs @@ -10,6 +10,7 @@ pub mod ingest_statistics; pub mod list_person_catalog_nodes; pub(crate) mod prune_statistics; pub mod read_person_catalog; +pub mod read_person_column_lineage; pub mod read_person_fingerprint; pub mod read_person_node_status; pub mod read_person_statistics; diff --git a/crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_column_lineage.rs b/crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_column_lineage.rs new file mode 100644 index 0000000000..c03656a134 --- /dev/null +++ b/crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_column_lineage.rs @@ -0,0 +1,51 @@ +//! A person reads one node's column lineage with view on every current node, as the catalog listing requires. + +use uuid::Uuid; + +use crate::authorization::main::authorize_listing::authorize_listing; +use crate::credentials::models::{AuditContext, DefaultRole}; +use crate::snapshots::main::read_current_record::read_current_record; +use crate::snapshots::main::read_snapshot_documents::read_snapshot_documents; +use crate::snapshots::main::read_snapshot_nodes::read_snapshot_nodes; +use crate::store::errors::ControlError; +use crate::store::main::{begin::begin, commit::commit}; +use crate::store::models::{ControlStore, TransactionMode}; +use crate::warehouse_metadata::_helpers::catalog_nodes::{ + neighbourhood, node_columns, published_edges, +}; +use crate::warehouse_metadata::models::ColumnLineageRead; + +/// Columns and edges only, never SQL or expressions, which need view.sql. +pub async fn read_person_column_lineage( + store: &ControlStore, + credentials: (&AuditContext, Uuid), + (location_id, target, node_key): (Uuid, &str, &str), + default_role: DefaultRole, +) -> Result { + let (transaction, _) = authorize_listing( + begin(store, TransactionMode::Write).await?, + credentials, + (location_id, target), + default_role, + ) + .await?; + let (transaction, snapshot) = read_current_record(transaction, (location_id, target)).await?; + let Some(snapshot) = snapshot else { + commit(transaction).await?; + return Ok(ColumnLineageRead { + snapshot_id: None, + nodes: Vec::new(), + edges: Vec::new(), + }); + }; + let (transaction, nodes) = read_snapshot_nodes(transaction, snapshot.snapshot_id, None).await?; + let (transaction, documents) = + read_snapshot_documents(transaction, snapshot.snapshot_id, Some(&["lineage"])).await?; + commit(transaction).await?; + let edges = neighbourhood(&published_edges(documents.get("lineage"), &nodes), node_key); + Ok(ColumnLineageRead { + snapshot_id: Some(snapshot.snapshot_id), + nodes: node_columns(&nodes, node_key, &edges), + edges, + }) +} diff --git a/crates/sqlbuild-orchestration/src/warehouse_metadata/models.rs b/crates/sqlbuild-orchestration/src/warehouse_metadata/models.rs index cf6e2589c2..e71dcc67ad 100644 --- a/crates/sqlbuild-orchestration/src/warehouse_metadata/models.rs +++ b/crates/sqlbuild-orchestration/src/warehouse_metadata/models.rs @@ -645,3 +645,35 @@ pub struct CatalogNodeBuild { pub run_id: String, pub version_hash: String, } + +/// A node's columns as its compiled definition infers them; `data_type` is absent when inference could not tell. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] +pub struct LineageNodeColumns { + pub node_key: String, + pub kind: String, + pub columns: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] +pub struct LineageColumn { + pub name: String, + pub data_type: Option, +} + +/// One published column lineage edge; `transform` is the compiler's transform kind, such as `direct` or `cast`. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] +pub struct ColumnLineageEdge { + pub source_key: String, + pub source_column: String, + pub target_key: String, + pub target_column: String, + pub transform: String, +} + +/// The column lineage reaching or leaving one node, from the current snapshot; empty before one publishes lineage. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] +pub struct ColumnLineageRead { + pub snapshot_id: Option, + pub nodes: Vec, + pub edges: Vec, +} diff --git a/crates/sqlbuild-orchestration/tests/api/test_column_lineage.rs b/crates/sqlbuild-orchestration/tests/api/test_column_lineage.rs new file mode 100644 index 0000000000..2af31100bf --- /dev/null +++ b/crates/sqlbuild-orchestration/tests/api/test_column_lineage.rs @@ -0,0 +1,156 @@ +//! Published column lineage reaches people as one node's upstream and downstream columns, on both control stores. + +use serde_json::json; +use uuid::Uuid; + +use crate::api::tests::test_types::ColumnLineageTestCase; +use crate::authorization::main::bootstrap_user::bootstrap_user; +use crate::credentials::main::identities::create_principal::create_principal; +use crate::credentials::models::{AuditContext, DefaultRole}; +use crate::credentials::tests::location_helpers::create_location; +use crate::runs::tests::helpers::snapshot; +use crate::snapshots::main::publish_snapshot::publish_snapshot; +use crate::snapshots::models::SnapshotNode; +use crate::store::errors::ControlError; +use crate::store::main::migrate_store::migrate_store; +use crate::store::tests::store_helpers::{close_test_store, open_test_store, run}; +use crate::store::tests::store_test_types::Backend; +use crate::warehouse_metadata::main::read_person_column_lineage::read_person_column_lineage; + +#[test] +fn given_published_column_lineage_when_people_read_a_node_then_only_its_reachable_columns_return() { + let test_cases = [ + ColumnLineageTestCase { + description: "SQLite column lineage", + backend: Backend::Sqlite, + expected_orders_edges: vec![ + ("model:raw_orders", "model:orders"), + ("model:orders", "model:order_totals"), + ], + expected_leaf_edges: 2, + }, + ColumnLineageTestCase { + description: "PostgreSQL column lineage", + backend: Backend::Postgres, + expected_orders_edges: vec![ + ("model:raw_orders", "model:orders"), + ("model:orders", "model:order_totals"), + ], + expected_leaf_edges: 2, + }, + ]; + for test_case in test_cases { + run(async { + let Some(fixture) = open_test_store(test_case.backend).await else { + return; + }; + let store = &fixture.store; + migrate_store(store).await.expect("migrate"); + let audit = AuditContext { + actor: "test-admin".into(), + granting_role: "admin".into(), + request_id: Uuid::now_v7(), + ip_address: None, + }; + let location_id = create_location(store, "orders", "test-admin") + .await + .expect("location"); + let model = |columns: serde_json::Value| SnapshotNode { + kind: "model".into(), + definition: json!({"materialization": "table", "columns": columns}), + raw_sql: Some("select 1 as id".into()), + compiled_sql: Some("select 1 as id".into()), + python_source: None, + }; + let mut input = snapshot(location_id); + input.nodes = [ + ( + "model:raw_orders", + model(json!([{"name": "amount", "type": "VARCHAR"}])), + ), + ( + "model:orders", + model(json!([{"name": "amount", "type": "DECIMAL(10,2)"}])), + ), + ( + "model:order_totals", + model(json!([{"name": "total", "type": null}])), + ), + ( + "model:inventory", + model(json!([{"name": "quantity", "type": "INTEGER"}])), + ), + ( + "model:inventory_report", + model(json!([{"name": "quantity", "type": "INTEGER"}])), + ), + ] + .into_iter() + .map(|(key, node)| (key.to_owned(), node)) + .collect(); + input.documents.insert( + "lineage".into(), + json!({"edges": [ + ["model:raw_orders", "amount", "model:orders", "amount", "cast"], + ["model:orders", "amount", "model:order_totals", "total", "aggregate"], + ["model:inventory", "quantity", "model:inventory_report", "quantity", "direct"], + ["model:retired_orders", "amount", "model:orders", "amount", "direct"], + ]}), + ); + publish_snapshot(store, &input).await.expect("publish"); + let admin = bootstrap_user(store, &audit, "orders-admin") + .await + .expect("administrator"); + let read = |node_key: &'static str, person: Uuid| { + read_person_column_lineage( + store, + (&audit, person), + (location_id, "development", node_key), + DefaultRole::None, + ) + }; + let orders = read("model:orders", admin).await.expect("orders lineage"); + assert_eq!( + orders + .edges + .iter() + .map(|e| (e.source_key.as_str(), e.target_key.as_str())) + .collect::>(), + test_case.expected_orders_edges, + "{}: both directions, never another node's chain or a node outside the snapshot", + test_case.description + ); + assert_eq!( + json!(orders.nodes), + json!([ + {"node_key": "model:order_totals", "kind": "model", "columns": [{"name": "total", "data_type": null}]}, + {"node_key": "model:orders", "kind": "model", "columns": [{"name": "amount", "data_type": "DECIMAL(10,2)"}]}, + {"node_key": "model:raw_orders", "kind": "model", "columns": [{"name": "amount", "data_type": "VARCHAR"}]}, + ]), + "{}", + test_case.description + ); + let leaf = read("model:order_totals", admin) + .await + .expect("leaf lineage"); + assert_eq!( + leaf.edges.len(), + test_case.expected_leaf_edges, + "{}: upstream is followed transitively", + test_case.description + ); + let stranger = create_principal(store, &audit, "orders-stranger") + .await + .expect("person"); + assert!( + matches!( + read("model:orders", stranger).await, + Err(ControlError::Denied { .. }) + ), + "{}: lineage needs view on every node", + test_case.description + ); + close_test_store(fixture).await; + }); + } +} diff --git a/crates/sqlbuild-orchestration/tests/api/test_contracts.rs b/crates/sqlbuild-orchestration/tests/api/test_contracts.rs index b59038211b..584885c2a0 100644 --- a/crates/sqlbuild-orchestration/tests/api/test_contracts.rs +++ b/crates/sqlbuild-orchestration/tests/api/test_contracts.rs @@ -38,6 +38,8 @@ mod test_changes; mod test_checks; #[path = "test_cli_login.rs"] mod test_cli_login; +#[path = "test_column_lineage.rs"] +mod test_column_lineage; #[path = "test_contract.rs"] mod test_contract; #[path = "test_custom_roles.rs"] diff --git a/crates/sqlbuild-orchestration/tests/api/test_types.rs b/crates/sqlbuild-orchestration/tests/api/test_types.rs index 5d3d2b50f3..2ea6a1020a 100644 --- a/crates/sqlbuild-orchestration/tests/api/test_types.rs +++ b/crates/sqlbuild-orchestration/tests/api/test_types.rs @@ -76,6 +76,13 @@ pub(crate) struct LocationWorkTestCase { pub(crate) expected_generation: i64, } +pub(crate) struct ColumnLineageTestCase { + pub(crate) description: &'static str, + pub(crate) backend: Backend, + pub(crate) expected_orders_edges: Vec<(&'static str, &'static str)>, + pub(crate) expected_leaf_edges: usize, +} + pub(crate) struct WarehouseMetadataTestCase { pub(crate) description: &'static str, pub(crate) backend: Backend, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/location_command/mod.rs b/crates/sqlbuild-python/src/bindings/_helpers/location_command/mod.rs index 79131a3bb0..5c4301a8d5 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/location_command/mod.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/location_command/mod.rs @@ -2,4 +2,5 @@ pub(crate) mod command; mod constants; mod runtime; mod snapshot; +mod snapshot_lineage; mod worker; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/location_command/snapshot.rs b/crates/sqlbuild-python/src/bindings/_helpers/location_command/snapshot.rs index e5e7028962..6e2353b734 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/location_command/snapshot.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/location_command/snapshot.rs @@ -19,6 +19,7 @@ use crate::bindings::_helpers::compiled_project::snapshot_models::model_nodes; use crate::bindings::_helpers::compiled_project::snapshot_resources::resource_nodes; use crate::bindings::_helpers::compiled_project::snapshot_roles::compile_project_roles; use crate::bindings::_helpers::compiled_views::project_view::NativeProjectView; +use crate::bindings::_helpers::location_command::snapshot_lineage::column_lineage_document; use crate::bindings::_helpers::native_host::orchestration_declarations::collect_declarations; use crate::bindings::_helpers::native_host::orchestration_environment::environment_hash; use crate::bindings::_helpers::planner_identity::identities::direct_model_version_identities; @@ -79,7 +80,11 @@ pub(crate) fn build_orchestration_snapshot( )?; } input = compile_project_roles(py, input, (&project_dir, graph.bind(py).get().graph()))?; - input.nodes = identity_nodes(project.into_pyobject(py)?.as_any(), input.nodes)?; + let project = project.into_pyobject(py)?; + if let Some(lineage) = column_lineage_document(project.as_any())? { + input.documents.insert("lineage".into(), lineage); + } + input.nodes = identity_nodes(project.as_any(), input.nodes)?; Ok(NativeOrchestrationSnapshot { input }) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/location_command/snapshot_lineage.rs b/crates/sqlbuild-python/src/bindings/_helpers/location_command/snapshot_lineage.rs new file mode 100644 index 0000000000..d6b65e7ef2 --- /dev/null +++ b/crates/sqlbuild-python/src/bindings/_helpers/location_command/snapshot_lineage.rs @@ -0,0 +1,39 @@ +//! The project's column lineage edges, published so people can trace columns without the project. + +use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; +use serde_json::{Value, json}; + +use crate::bindings::_helpers::lineage::project_lineage::build_project_column_lineage; +use crate::bindings::_helpers::records::construct::enum_member; +use crate::bindings::_helpers::records::fields::field; +use crate::bindings::_helpers::records::ids::RecordId; + +/// FAST column lineage of every model as `[source key, column, target key, column, transform]` rows; `None` without SQL analysis. +pub(crate) fn column_lineage_document(project: &Bound<'_, PyAny>) -> PyResult> { + let py = project.py(); + let mode = enum_member(py, RecordId::ColumnLineageMode, "FAST")?; + let Some(lineage) = build_project_column_lineage(project, None, &mode, None)? else { + return Ok(None); + }; + let edges = field(lineage.bind(py), "edges")?; + let mut rows: Vec = Vec::new(); + for edge in edges.try_iter()? { + let edge = edge?; + let (source, target) = (field(&edge, "source")?, field(&edge, "target")?); + rows.push(json!([ + node_key(&source)?, + field(&source, "column_name")?.extract::()?, + node_key(&target)?, + field(&target, "column_name")?.extract::()?, + field(&edge, "transform_kind")?.str()?.extract::()?, + ])); + } + Ok(Some(json!({ "edges": rows }))) +} + +/// The snapshot node key of a lineage column's resource, such as `model:orders`. +fn node_key(column: &Bound<'_, PyAny>) -> PyResult { + let kind: String = field(column, "resource_type")?.str()?.extract()?; + let name: String = field(column, "resource_name")?.extract()?; + Ok(format!("{kind}:{name}")) +} diff --git a/tests/integration/src/sqlbuild/compiler/compiled_project/_test_types.py b/tests/integration/src/sqlbuild/compiler/compiled_project/_test_types.py index 9fd65cab7a..8291e2a98c 100644 --- a/tests/integration/src/sqlbuild/compiler/compiled_project/_test_types.py +++ b/tests/integration/src/sqlbuild/compiler/compiled_project/_test_types.py @@ -233,6 +233,13 @@ class SnapshotEnvironmentTestCase: expected_environment_count: int +@dataclass(frozen=True) +class SnapshotLineageTestCase: + description: str + files: dict[str, str] + expected_edges: list[list[str]] + + @dataclass(frozen=True) class SnapshotScheduleTestCase: description: str diff --git a/tests/integration/src/sqlbuild/compiler/compiled_project/test_orchestration_snapshot.py b/tests/integration/src/sqlbuild/compiler/compiled_project/test_orchestration_snapshot.py index 7437f8ca6a..9824426203 100644 --- a/tests/integration/src/sqlbuild/compiler/compiled_project/test_orchestration_snapshot.py +++ b/tests/integration/src/sqlbuild/compiler/compiled_project/test_orchestration_snapshot.py @@ -35,6 +35,7 @@ SnapshotDiscoveryTestCase, SnapshotEnvironmentTestCase, SnapshotIdentityTestCase, + SnapshotLineageTestCase, SnapshotRejectionTestCase, SnapshotScheduleTestCase, ) @@ -326,6 +327,59 @@ def test_given_invalid_compilation_or_forged_nodes_when_publishing_then_no_snaps ) +@pytest.mark.parametrize( + "test_case", + [ + SnapshotLineageTestCase( + description="published column lineage follows passthroughs, casts and aggregations", + files={ + "sqlbuild_project.toml": 'name = "orders"\nadapter = "duckdb"\n', + "models/stg_orders.sql": ( + "MODEL (description 'Staged orders.');\n\n" + "SELECT 1 AS order_id, 'eu' AS region, '5.00' AS amount_text\n" + ), + "models/orders.sql": ( + "MODEL (description 'Typed orders.');\n\n" + "SELECT order_id, region, CAST(amount_text AS DECIMAL(10, 2)) AS amount\n" + 'FROM __ref("stg_orders")\n' + ), + "models/region_revenue.sql": ( + "MODEL (description 'Revenue by region.');\n\n" + "SELECT region, SUM(amount) AS revenue\n" + 'FROM __ref("orders")\nGROUP BY region\n' + ), + }, + expected_edges=[ + ["model:orders", "amount", "model:region_revenue", "revenue", "aggregation"], + ["model:orders", "region", "model:region_revenue", "region", "direct"], + ["model:stg_orders", "amount_text", "model:orders", "amount", "cast"], + ["model:stg_orders", "order_id", "model:orders", "order_id", "direct"], + ["model:stg_orders", "region", "model:orders", "region", "direct"], + ], + ), + ], + ids=lambda case: case.description, +) +def test_given_a_compiled_project_when_publishing_then_its_column_lineage_is_a_snapshot_document( + test_case: SnapshotLineageTestCase, + tmp_path: Path, +) -> None: + write_files(project_dir=tmp_path, files=test_case.files) + project: CompiledProjectView = build_compiled_project( + discovered_inputs=discover_project_inputs(project_dir=tmp_path), + adapter=DuckDbAdapter(), + resolved_connection={"database": "warehouse", "schema": "main"}, + ) + publication: dict[str, Any] = json.loads( + build_orchestration_snapshot(project, json.dumps(METADATA), tmp_path).to_json() + ) + edges: list[list[str]] = publication["documents"]["lineage"]["edges"] + assert sorted(edges) == test_case.expected_edges + sources: set[str] = {edge[0] for edge in edges} + targets: set[str] = {edge[2] for edge in edges} + assert sources | targets <= set(publication["nodes"]) + + @pytest.mark.parametrize( "test_case", [ diff --git a/ui/src/lib/api/http-catalog.test.ts b/ui/src/lib/api/http-catalog.test.ts index 12323431a7..ef0185d1ea 100644 --- a/ui/src/lib/api/http-catalog.test.ts +++ b/ui/src/lib/api/http-catalog.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import type { components } from '$control/schema.d.ts'; -import { findNode, nodeDetailOf, toCatalogNodes } from './http-catalog'; +import { findNode, nodeDetailOf, toCatalogNodes, toColumnLineage } from './http-catalog'; import { toCheck } from './http-checks'; type W = components['schemas']; @@ -97,3 +97,38 @@ describe('catalog node detail', () => { expect(withSql.sql).toEqual({ raw: 'select {{ x }}', compiled: 'select {{ x }}' }); }); }); + +describe('column lineage mapping', () => { + const nodes = toCatalogNodes([node('model:raw_orders', { materialization: 'view' }), node('model:orders', { materialization: 'table' })]); + const edge = (source: string, target: string, transform: string): W['ColumnLineageEdge'] => ({ source_key: 'model:raw_orders', source_column: source, target_key: 'model:orders', target_column: target, transform }); + + it('keeps a name through direct or star as a passthrough, a new name as a rename, and anything else as a transform', () => { + const lineage = toColumnLineage( + { + snapshot_id: 's', + nodes: [ + { node_key: 'model:raw_orders', kind: 'model', columns: [{ name: 'amount', data_type: null }] }, + { node_key: 'model:orders', kind: 'model', columns: [{ name: 'amount', data_type: 'DECIMAL(10,2)' }] }, + { node_key: 'model:retired', kind: 'model', columns: [] } + ], + edges: [edge('id', 'id', 'direct'), edge('note', 'note', 'star'), edge('cust', 'customer_id', 'direct'), edge('amount', 'amount', 'cast')] + }, + nodes + ); + expect(lineage?.edges.map(([, target, lens]) => [target, lens])).toEqual([ + ['model:orders.id', 'passthrough'], + ['model:orders.note', 'passthrough'], + ['model:orders.customer_id', 'rename'], + ['model:orders.amount', 'transform'] + ]); + expect(lineage?.models.map((m) => [m.id, m.name, m.kind, m.columns])).toEqual([ + ['model:raw_orders', 'raw_orders', 'view', [{ name: 'amount', type: '' }]], + ['model:orders', 'orders', 'table', [{ name: 'amount', type: 'DECIMAL(10,2)' }]] + ]); + expect([lineage?.sql, lineage?.expressions]).toEqual([{}, {}]); + }); + + it('has nothing to show when no lineage reaches the node', () => { + expect(toColumnLineage({ snapshot_id: 's', nodes: [], edges: [] }, nodes)).toBeNull(); + }); +}); diff --git a/ui/src/lib/api/http-catalog.ts b/ui/src/lib/api/http-catalog.ts index 5c9c492fbd..63ba2d759a 100644 --- a/ui/src/lib/api/http-catalog.ts +++ b/ui/src/lib/api/http-catalog.ts @@ -1,6 +1,7 @@ import type { components } from '$control/schema.d.ts'; import { shortName } from '$lib/build-area/labels'; -import type { CatalogNode, CatalogNodeDetail, Check, CheckCounts, StaleReason } from './types/build'; +import { cardStatus } from '$lib/lineage/catalog-graph'; +import type { CatalogNode, CatalogNodeDetail, Check, CheckCounts, ColumnLens, ColumnLineage, StaleReason } from './types/build'; import type { NodeKind } from './types/operate'; type Wire = components['schemas']; @@ -104,3 +105,28 @@ export function nodeDetailOf(nodes: CatalogNode[], node: CatalogNode, status: Wi history: null }; } + +/** A kept name through `direct` or `star` is a passthrough, a new name a rename; every other transform changes the value. */ +function lensOf(edge: Wire['ColumnLineageEdge']): ColumnLens { + if (edge.transform !== 'direct' && edge.transform !== 'star') return 'transform'; + return edge.source_column === edge.target_column ? 'passthrough' : 'rename'; +} + +/** + * Column lineage around one node, named and coloured from the catalog listing. The read carries no + * SQL or expressions, so those stay empty; null before a snapshot publishes lineage for the node. + */ +export function toColumnLineage(read: Wire['ColumnLineageRead'], nodes: CatalogNode[]): ColumnLineage | null { + if (!read.edges.length) return null; + const listed = new Map(nodes.map((n) => [n.key, n])); + return { + models: read.nodes.flatMap((n) => { + const node = listed.get(n.node_key); + const columns = n.columns.map((c) => ({ name: c.name, type: c.data_type ?? '' })); + return node ? [{ id: n.node_key, name: shortName(n.node_key), kind: node.kind, status: cardStatus(node), columns }] : []; + }), + edges: read.edges.map((e) => [`${e.source_key}.${e.source_column}`, `${e.target_key}.${e.target_column}`, lensOf(e)]), + sql: {}, + expressions: {} + }; +} diff --git a/ui/src/lib/api/http-client.ts b/ui/src/lib/api/http-client.ts index 952caa2133..6600d53438 100644 --- a/ui/src/lib/api/http-client.ts +++ b/ui/src/lib/api/http-client.ts @@ -2,13 +2,13 @@ import createFetch from 'openapi-fetch'; import type { components, paths } from '$control/schema.d.ts'; import { ApiError, type ApiClient } from './client'; import { checkDetailOf, toCheck, toCheckEvidence, toCheckSql } from './http-checks'; -import { findNode, nodeDetailOf, toCatalogNodes } from './http-catalog'; +import { findNode, nodeDetailOf, toCatalogNodes, toColumnLineage } from './http-catalog'; import { toSnapshotChanges } from './http-changes'; import { jobDetailOf, toJob, toTick, toTrigger, triggerDetailOf } from './http-jobs'; import { RUN_WINDOW, runDetailOf, runRowPage, runsMatching, runTabCountsOf, toRunNode, toRuns } from './http-runs'; import type { ChangeSubscription, ChangedChannel, ChangedSignal, ConnectionState } from './signals'; import type { AuthMode, Bootstrap, Capability, Location, Scope } from './types'; -import type { CatalogNode, CatalogNodeDetail, Check, CheckDetail, CheckEvidence, CheckSql, SnapshotChange } from './types/build'; +import type { CatalogNode, CatalogNodeDetail, Check, ColumnLineage, CheckDetail, CheckEvidence, CheckSql, SnapshotChange } from './types/build'; import type { Job, JobDetail, RerunMode, Run, RunDetail, RunListQuery, RunRowPage, RunRowQuery, RunTabCounts, Trigger, TriggerDetail, TriggerState } from './types/operate'; type Wire = components['schemas']; @@ -39,6 +39,7 @@ const SUPPORTED: ReadonlySet = new Set([ 'getCheckEvidence', 'listCatalogNodes', 'getCatalogNode', + 'getColumnLineage', 'listSnapshotChanges' ]); @@ -263,6 +264,18 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt if (!snapshots.data) throw new ApiError(errorText(snapshots.error, snapshots.response), snapshots.response.status); return toSnapshotChanges(snapshots.data, runs); }, + async getColumnLineage(scope: Scope, ref: string): Promise { + const nodes = await implemented.listCatalogNodes(scope); + const node = findNode(nodes, ref); + if (!node) return null; + const { locationId, target } = parseLocationKey(scope.locationId); + const { data, error, response } = await http.GET('/api/control/locations/{location_id}/targets/{target}/column-lineage', { + params: { path: { location_id: locationId, target }, query: { node_key: node.key } } + }); + if (response.status === 401) onUnauthorized(); + if (!data) throw new ApiError(errorText(error, response), response.status); + return toColumnLineage(data, nodes); + }, async getCatalogNode(scope: Scope, ref: string): Promise { const nodes = await implemented.listCatalogNodes(scope); const node = findNode(nodes, ref); diff --git a/ui/src/lib/api/types/build.ts b/ui/src/lib/api/types/build.ts index 906f9e77df..e9b0e51bb9 100644 --- a/ui/src/lib/api/types/build.ts +++ b/ui/src/lib/api/types/build.ts @@ -193,7 +193,7 @@ export type CatalogNodeDetail = Omit & { export type ColumnLens = 'raw' | 'passthrough' | 'rename' | 'transform'; export type ColumnLineage = { - models: { id: string; name: string; kind: string; status: 'ok' | 'fail' | 'warn' | 'stale' | 'src'; columns: { name: string; type: string }[] }[]; + models: { id: string; name: string; kind: string; status: 'ok' | 'fail' | 'warn' | 'stale' | 'src' | 'none'; columns: { name: string; type: string }[] }[]; /** `model.column` pairs from upstream to downstream. */ edges: [string, string, ColumnLens][]; sql: Record; diff --git a/ui/src/lib/components/lineage/col-cell-node.svelte b/ui/src/lib/components/lineage/col-cell-node.svelte index c1f24b1380..7b0e7752b4 100644 --- a/ui/src/lib/components/lineage/col-cell-node.svelte +++ b/ui/src/lib/components/lineage/col-cell-node.svelte @@ -7,7 +7,7 @@ model: string; column: string; kind: string; - status: 'ok' | 'fail' | 'warn' | 'stale' | 'src'; + status: 'ok' | 'fail' | 'warn' | 'stale' | 'src' | 'none'; lens?: Lens; lensLabel?: Record; focal?: boolean; @@ -22,7 +22,8 @@ fail: 'var(--sb-error)', warn: 'var(--sb-warning)', stale: 'var(--sb-stale)', - src: 'var(--sb-text-faint)' + src: 'var(--sb-text-faint)', + none: 'var(--border)' }; const KindIcon = $derived(d.kind === 'view' ? Eye : d.kind.startsWith('source') ? Database : d.kind.startsWith('incremental') ? Layers : Table2); const kindColor = $derived(d.kind === 'view' ? '#5aa9e6' : d.kind === 'table' ? '#2e90ff' : d.kind.startsWith('incremental') ? '#3fb6c9' : '#7c8aa5'); diff --git a/ui/src/lib/lineage/column-lineage.svelte b/ui/src/lib/lineage/column-lineage.svelte index b418a1ab07..6ec3e4780d 100644 --- a/ui/src/lib/lineage/column-lineage.svelte +++ b/ui/src/lib/lineage/column-lineage.svelte @@ -12,7 +12,7 @@ import { highlightSqlbuild } from '$lib/sql/highlight'; import { tick } from 'svelte'; - type ColModel = { id: string; name: string; kind: string; status: 'ok' | 'fail' | 'warn' | 'stale' | 'src'; columns: { name: string; type: string }[] }; + type ColModel = { id: string; name: string; kind: string; status: 'ok' | 'fail' | 'warn' | 'stale' | 'src' | 'none'; columns: { name: string; type: string }[] }; type Lens = 'raw' | 'passthrough' | 'rename' | 'transform'; let { @@ -394,7 +394,7 @@ {#if sqlParts && pinnedNode}
SQL · {sqlParts.model}.sql
{@html sqlParts.before}{#if sqlParts.match}{@html sqlParts.match}{/if}{@html sqlParts.after}
- {:else} + {:else if modelSql && Object.keys(modelSql).length}

Select a step to see it in the model's SQL.

{/if} diff --git a/ui/src/routes/catalog/[name]/node-view.svelte b/ui/src/routes/catalog/[name]/node-view.svelte index 1191e7eb7a..2af4eca3c3 100644 --- a/ui/src/routes/catalog/[name]/node-view.svelte +++ b/ui/src/routes/catalog/[name]/node-view.svelte @@ -97,7 +97,7 @@
-

{name}

+

{shortName(n?.key ?? name)}

{#if n} {#if n.kind === 'source'} From 2c59909a75708887190872efcc833e2a93e3414b Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 17:10:36 +0000 Subject: [PATCH 062/106] fix(ui): show full names and transform kinds in column lineage Traced identifiers and node names wrap instead of truncating, column types stay on one line, and transformed edges name the compiler's transform kind, such as Cast or Aggregation. --- ui/src/lib/api/http-catalog.test.ts | 12 ++++---- ui/src/lib/api/http-catalog.ts | 8 ++++- ui/src/lib/api/types/build.ts | 4 +-- .../components/lineage/col-cell-node.svelte | 16 +++++----- ui/src/lib/lineage/column-lineage.svelte | 30 +++++++++++-------- 5 files changed, 42 insertions(+), 28 deletions(-) diff --git a/ui/src/lib/api/http-catalog.test.ts b/ui/src/lib/api/http-catalog.test.ts index ef0185d1ea..398299bc76 100644 --- a/ui/src/lib/api/http-catalog.test.ts +++ b/ui/src/lib/api/http-catalog.test.ts @@ -102,7 +102,7 @@ describe('column lineage mapping', () => { const nodes = toCatalogNodes([node('model:raw_orders', { materialization: 'view' }), node('model:orders', { materialization: 'table' })]); const edge = (source: string, target: string, transform: string): W['ColumnLineageEdge'] => ({ source_key: 'model:raw_orders', source_column: source, target_key: 'model:orders', target_column: target, transform }); - it('keeps a name through direct or star as a passthrough, a new name as a rename, and anything else as a transform', () => { + it('keeps a name through direct or star as a passthrough, a new name as a rename, and names any other transform', () => { const lineage = toColumnLineage( { snapshot_id: 's', @@ -115,11 +115,11 @@ describe('column lineage mapping', () => { }, nodes ); - expect(lineage?.edges.map(([, target, lens]) => [target, lens])).toEqual([ - ['model:orders.id', 'passthrough'], - ['model:orders.note', 'passthrough'], - ['model:orders.customer_id', 'rename'], - ['model:orders.amount', 'transform'] + expect(lineage?.edges.map(([, target, lens, label]) => [target, lens, label])).toEqual([ + ['model:orders.id', 'passthrough', undefined], + ['model:orders.note', 'passthrough', undefined], + ['model:orders.customer_id', 'rename', undefined], + ['model:orders.amount', 'transform', 'Cast'] ]); expect(lineage?.models.map((m) => [m.id, m.name, m.kind, m.columns])).toEqual([ ['model:raw_orders', 'raw_orders', 'view', [{ name: 'amount', type: '' }]], diff --git a/ui/src/lib/api/http-catalog.ts b/ui/src/lib/api/http-catalog.ts index 63ba2d759a..76b50cb991 100644 --- a/ui/src/lib/api/http-catalog.ts +++ b/ui/src/lib/api/http-catalog.ts @@ -112,6 +112,12 @@ function lensOf(edge: Wire['ColumnLineageEdge']): ColumnLens { return edge.source_column === edge.target_column ? 'passthrough' : 'rename'; } +/** The compiler's transform kind as a label, such as `Cast`; passthroughs and renames keep the lens's own name. */ +function transformLabel(edge: Wire['ColumnLineageEdge']): string | undefined { + if (lensOf(edge) !== 'transform') return undefined; + return edge.transform === 'unknown' ? 'Transformed' : edge.transform.charAt(0).toUpperCase() + edge.transform.slice(1); +} + /** * Column lineage around one node, named and coloured from the catalog listing. The read carries no * SQL or expressions, so those stay empty; null before a snapshot publishes lineage for the node. @@ -125,7 +131,7 @@ export function toColumnLineage(read: Wire['ColumnLineageRead'], nodes: CatalogN const columns = n.columns.map((c) => ({ name: c.name, type: c.data_type ?? '' })); return node ? [{ id: n.node_key, name: shortName(n.node_key), kind: node.kind, status: cardStatus(node), columns }] : []; }), - edges: read.edges.map((e) => [`${e.source_key}.${e.source_column}`, `${e.target_key}.${e.target_column}`, lensOf(e)]), + edges: read.edges.map((e) => [`${e.source_key}.${e.source_column}`, `${e.target_key}.${e.target_column}`, lensOf(e), transformLabel(e)]), sql: {}, expressions: {} }; diff --git a/ui/src/lib/api/types/build.ts b/ui/src/lib/api/types/build.ts index e9b0e51bb9..81e534b63f 100644 --- a/ui/src/lib/api/types/build.ts +++ b/ui/src/lib/api/types/build.ts @@ -194,8 +194,8 @@ export type ColumnLens = 'raw' | 'passthrough' | 'rename' | 'transform'; export type ColumnLineage = { models: { id: string; name: string; kind: string; status: 'ok' | 'fail' | 'warn' | 'stale' | 'src' | 'none'; columns: { name: string; type: string }[] }[]; - /** `model.column` pairs from upstream to downstream. */ - edges: [string, string, ColumnLens][]; + /** `model.column` pairs from upstream to downstream, with an optional label naming the transform, such as `Cast`. */ + edges: [string, string, ColumnLens, string?][]; sql: Record; /** The defining expression of each column, per model. */ expressions: Record>; diff --git a/ui/src/lib/components/lineage/col-cell-node.svelte b/ui/src/lib/components/lineage/col-cell-node.svelte index 7b0e7752b4..0ed67190b9 100644 --- a/ui/src/lib/components/lineage/col-cell-node.svelte +++ b/ui/src/lib/components/lineage/col-cell-node.svelte @@ -9,7 +9,8 @@ kind: string; status: 'ok' | 'fail' | 'warn' | 'stale' | 'src' | 'none'; lens?: Lens; - lensLabel?: Record; + /** What the incoming edge does, such as `Cast`. */ + label?: string; focal?: boolean; active?: boolean; }; @@ -32,21 +33,22 @@
- {d.column} + {d.column}
- - {d.model} - {#if d.lens && d.lensLabel} - {d.lensLabel[d.lens]} + + {d.model} + {#if d.lens && d.label} + {d.label} {:else} {d.kind} {/if} diff --git a/ui/src/lib/lineage/column-lineage.svelte b/ui/src/lib/lineage/column-lineage.svelte index 6ec3e4780d..00096734b7 100644 --- a/ui/src/lib/lineage/column-lineage.svelte +++ b/ui/src/lib/lineage/column-lineage.svelte @@ -24,7 +24,7 @@ }: { focal: string; models: ColModel[]; - colEdges: [string, string, Lens][]; + colEdges: [string, string, Lens, string?][]; /** per-model SQL: modelId -> SQL text */ modelSql?: Record; /** per-model column expressions: modelId -> { colName -> defining substring } */ @@ -70,6 +70,9 @@ const kindOf = (id: string) => models.find((m) => m.id === id)?.kind ?? 'table'; const statusOf = (id: string) => models.find((m) => m.id === id)?.status ?? 'ok'; const incomingLens = $derived(new Map(colEdges.map(([, t, lens]) => [t, lens]))); + // What a column's incoming edge does: the published transform, such as Cast, or the lens's name. + const incomingLabel = $derived(new Map(colEdges.map(([, t, lens, label]) => [t, label ?? lensLabel[lens]]))); + const labelOf = (cid: string) => incomingLabel.get(cid) ?? lensLabel.raw; const focalModel = $derived(models.find((m) => m.id === focal)); let colQuery = $state(''); @@ -223,7 +226,7 @@ kind: kindOf(modelOf(cid)), status: statusOf(modelOf(cid)), lens: incomingLens.get(cid), - lensLabel, + label: incomingLens.has(cid) ? labelOf(cid) : undefined, focal: cid === selectedCol, active: cid === activeStep } @@ -284,9 +287,9 @@ onclick={() => (selectedCol = cid)} > {#if selectedCol === cid}{/if} - {c.name} - {lens ? lensLabel[lens] : ''} - {c.type.toLowerCase()} + {c.name} + {lens ? labelOf(cid) : ''} + {#if c.type}{c.type.toLowerCase()}{/if} {:else}
No columns match.
@@ -369,8 +372,8 @@
  • @@ -383,8 +386,8 @@
  • @@ -498,11 +501,14 @@ border-color: var(--primary); background: var(--primary); } + /* The identifier being traced wraps rather than truncates. */ .dv-name { - overflow: hidden; font: 12.5px var(--font-mono); - text-overflow: ellipsis; - white-space: nowrap; + overflow-wrap: anywhere; + } + .col-name { + min-width: 0; + overflow-wrap: anywhere; } .dv-name .m { color: var(--muted-foreground); From 2ff5a9ea79640ca535845b8f9846ef8b7d145bd1 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 17:15:57 +0000 Subject: [PATCH 063/106] refactor: move column lineage to its own change Column lineage publication would push large projects past the publish body limit, so it lands after the publish request is compressed. This change keeps node detail, labels and the SQL preview fixes. --- clients/typescript/src/schema.d.ts | 81 --------- .../authentication/password_sessions.rs | 4 - .../_helpers/resources/person_operations.rs | 27 +-- .../sqlbuild-orchestration/src/api/models.rs | 1 - .../_helpers/catalog_nodes.rs | 126 +------------- .../src/warehouse_metadata/main/mod.rs | 1 - .../main/read_person_column_lineage.rs | 51 ------ .../src/warehouse_metadata/models.rs | 32 ---- .../tests/api/test_column_lineage.rs | 156 ------------------ .../tests/api/test_contracts.rs | 2 - .../tests/api/test_types.rs | 7 - .../bindings/_helpers/location_command/mod.rs | 1 - .../_helpers/location_command/snapshot.rs | 7 +- .../location_command/snapshot_lineage.rs | 39 ----- .../compiler/compiled_project/_test_types.py | 7 - .../test_orchestration_snapshot.py | 54 ------ ui/src/lib/api/http-catalog.test.ts | 37 +---- ui/src/lib/api/http-catalog.ts | 34 +--- ui/src/lib/api/http-client.ts | 17 +- ui/src/lib/api/types/build.ts | 6 +- .../components/lineage/col-cell-node.svelte | 21 +-- ui/src/lib/lineage/column-lineage.svelte | 34 ++-- 22 files changed, 35 insertions(+), 710 deletions(-) delete mode 100644 crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_column_lineage.rs delete mode 100644 crates/sqlbuild-orchestration/tests/api/test_column_lineage.rs delete mode 100644 crates/sqlbuild-python/src/bindings/_helpers/location_command/snapshot_lineage.rs diff --git a/clients/typescript/src/schema.d.ts b/clients/typescript/src/schema.d.ts index 57eb4df072..2aebecfe52 100644 --- a/clients/typescript/src/schema.d.ts +++ b/clients/typescript/src/schema.d.ts @@ -306,22 +306,6 @@ export interface paths { patch?: never; trace?: never; }; - "/api/control/locations/{location_id}/targets/{target}/column-lineage": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations["readPersonColumnLineage"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; "/api/control/locations/{location_id}/targets/{target}/connection-tests": { parameters: { query?: never; @@ -1122,21 +1106,6 @@ export interface components { CliLoginStartRequest: { client_hash: string; }; - /** @description One published column lineage edge; `transform` is the compiler's transform kind, such as `direct` or `cast`. */ - ColumnLineageEdge: { - source_column: string; - source_key: string; - target_column: string; - target_key: string; - transform: string; - }; - /** @description The column lineage reaching or leaving one node, from the current snapshot; empty before one publishes lineage. */ - ColumnLineageRead: { - edges: components["schemas"]["ColumnLineageEdge"][]; - nodes: components["schemas"]["LineageNodeColumns"][]; - /** Format: uuid */ - snapshot_id?: string | null; - }; /** @description A row whose key was already stored with different content; the first row is kept. */ ConflictEvidence: { existing_sha256: string; @@ -1267,16 +1236,6 @@ export interface components { target: string; window?: null | components["schemas"]["RunWindow"]; }; - LineageColumn: { - data_type?: string | null; - name: string; - }; - /** @description A node's columns as its compiled definition infers them; `data_type` is absent when inference could not tell. */ - LineageNodeColumns: { - columns: components["schemas"]["LineageColumn"][]; - kind: string; - node_key: string; - }; /** @description One location target the person can view, with the actions allowed on at least one of its nodes. */ LocationAccess: { actions: components["schemas"]["Action"][]; @@ -2895,46 +2854,6 @@ export interface operations { }; }; }; - readPersonColumnLineage: { - parameters: { - query: { - /** @description The node whose upstream and downstream column lineage to return */ - node_key: string; - }; - header?: never; - path: { - location_id: string; - target: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ColumnLineageRead"]; - }; - }; - /** @description Session rejected */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ApiErrorBody"]; - }; - }; - }; - }; requestConnectionTest: { parameters: { query?: never; diff --git a/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs b/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs index efdcea296b..5bce2d5b43 100644 --- a/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs +++ b/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs @@ -121,10 +121,6 @@ pub(crate) fn person_routes() -> Router { "/api/control/locations/{location_id}/targets/{target}/snapshots", get(crate::api::_helpers::resources::person_reads::snapshots), ) - .route( - "/api/control/locations/{location_id}/targets/{target}/column-lineage", - get(crate::api::_helpers::resources::person_operations::column_lineage), - ) .route( "/api/control/locations/{location_id}/targets/{target}/catalog", get(crate::api::_helpers::resources::person_operations::catalog), diff --git a/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_operations.rs b/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_operations.rs index 77328d67bc..02fcaff030 100644 --- a/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_operations.rs +++ b/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_operations.rs @@ -17,13 +17,12 @@ use crate::runs::main::request_rerun::request_rerun; use crate::runs::models::{CancelRunRequest, JobRunRequest, RerunRequest, RunRecord}; use crate::store::models::ControlStore; use crate::warehouse_metadata::main::read_person_catalog::read_person_catalog; -use crate::warehouse_metadata::main::read_person_column_lineage::read_person_column_lineage; use crate::warehouse_metadata::main::read_person_fingerprint::read_person_fingerprint; use crate::warehouse_metadata::main::read_person_node_status::read_person_node_status; use crate::warehouse_metadata::main::read_person_statistics::read_person_statistics; use crate::warehouse_metadata::models::{ - CatalogPage, CatalogQuery, ColumnLineageRead, FingerprintObservation, FingerprintQuery, - NodeStatus, NodeStatusQuery, StatisticsPage, StatisticsQuery, StatisticsRetention, + CatalogPage, CatalogQuery, FingerprintObservation, FingerprintQuery, NodeStatus, + NodeStatusQuery, StatisticsPage, StatisticsQuery, StatisticsRetention, }; #[utoipa::path(get, path = "/api/control/locations/{location_id}/targets/{target}/statistics", operation_id = "readPersonStatistics", @@ -185,28 +184,6 @@ pub(crate) async fn fingerprint( )) } -#[utoipa::path(get, path = "/api/control/locations/{location_id}/targets/{target}/column-lineage", operation_id = "readPersonColumnLineage", - params(("location_id" = Uuid, Path), ("target" = String, Path), - ("node_key" = String, Query, description = "The node whose upstream and downstream column lineage to return")), - responses((status = 200, body = ColumnLineageRead), (status = 401, description = "Session rejected"), - (status = 403, body = crate::api::models::ApiErrorBody)), security(("session" = []), ("cliSession" = [])))] -pub(crate) async fn column_lineage( - (State(store), Extension(context)): (State, Extension), - Extension(default_role): Extension, - (Path((location_id, target)), Query(options)): (Path<(Uuid, String)>, Query), - Extension(AuthenticatedPerson(principal, _)): Extension, -) -> Result, ApiError> { - Ok(Json( - read_person_column_lineage( - &store, - (&context, principal), - (location_id, &target, &options.node_key), - default_role, - ) - .await?, - )) -} - #[utoipa::path(get, path = "/api/control/locations/{location_id}/targets/{target}/node-status", operation_id = "readPersonNodeStatus", params(("location_id" = Uuid, Path), ("target" = String, Path), ("node_key" = String, Query, description = "One exact published node key")), diff --git a/crates/sqlbuild-orchestration/src/api/models.rs b/crates/sqlbuild-orchestration/src/api/models.rs index 7d638ea6c9..9571345a46 100644 --- a/crates/sqlbuild-orchestration/src/api/models.rs +++ b/crates/sqlbuild-orchestration/src/api/models.rs @@ -30,7 +30,6 @@ use crate::api::_helpers::resources::{ person_operations::statistics, person_operations::fingerprint, person_operations::node_status, - person_operations::column_lineage, person_checks::list, person_checks::history, person_checks::sql, diff --git a/crates/sqlbuild-orchestration/src/warehouse_metadata/_helpers/catalog_nodes.rs b/crates/sqlbuild-orchestration/src/warehouse_metadata/_helpers/catalog_nodes.rs index 81afc05c96..a4b23af9ff 100644 --- a/crates/sqlbuild-orchestration/src/warehouse_metadata/_helpers/catalog_nodes.rs +++ b/crates/sqlbuild-orchestration/src/warehouse_metadata/_helpers/catalog_nodes.rs @@ -1,16 +1,11 @@ -//! Catalog listing rows and column lineage from snapshot node definitions and published edges. - -use std::collections::{BTreeMap, BTreeSet, HashMap}; +//! Catalog listing rows from snapshot node definitions and each node's latest build. use serde_json::Value; use uuid::Uuid; use crate::sensors::constants::SOURCE_NODE_KIND; use crate::snapshots::models::SnapshotNode; -use crate::warehouse_metadata::models::{ - CatalogNodeBuild, CatalogNodeRecord, CatalogRelation, ColumnLineageEdge, LineageColumn, - LineageNodeColumns, -}; +use crate::warehouse_metadata::models::{CatalogNodeBuild, CatalogNodeRecord, CatalogRelation}; /// A declared value from the definition itself, its schema entry or its config, in that order. fn declared<'a>(definition: &'a Value, key: &str) -> Option<&'a Value> { @@ -83,120 +78,3 @@ pub(crate) fn catalog_node( snapshot_id, } } - -/// Published edge rows whose both ends are current nodes, so the graph never points at a hidden node. -pub(crate) fn published_edges( - document: Option<&Value>, - nodes: &BTreeMap, -) -> Vec { - let rows = document - .and_then(|document| document.get("edges")) - .and_then(Value::as_array); - rows.into_iter() - .flatten() - .filter_map(edge_row) - .filter(|edge| nodes.contains_key(&edge.source_key) && nodes.contains_key(&edge.target_key)) - .collect() -} - -fn edge_row(row: &Value) -> Option { - let [ - source_key, - source_column, - target_key, - target_column, - transform, - ] = row.as_array()?.as_slice() - else { - return None; - }; - let text = |value: &Value| value.as_str().map(str::to_owned); - Some(ColumnLineageEdge { - source_key: text(source_key)?, - source_column: text(source_column)?, - target_key: text(target_key)?, - target_column: text(target_column)?, - transform: text(transform)?, - }) -} - -/// Edges reached upstream and downstream from any column of `node_key`, each once, in published order. -pub(crate) fn neighbourhood(edges: &[ColumnLineageEdge], node_key: &str) -> Vec { - let mut reached: BTreeSet = walk(edges, node_key, true); - reached.extend(walk(edges, node_key, false)); - reached - .into_iter() - .map(|index| edges[index].clone()) - .collect() -} - -/// The edges followed from the node's columns towards sources (`upstream`) or consumers. -fn walk(edges: &[ColumnLineageEdge], node_key: &str, upstream: bool) -> BTreeSet { - let mut reached: BTreeSet = BTreeSet::new(); - let mut by_near: HashMap<(&str, &str), Vec> = HashMap::new(); - for (index, edge) in edges.iter().enumerate() { - by_near.entry(end(edge, !upstream)).or_default().push(index); - } - let mut pending: Vec<(&str, &str)> = by_near - .keys() - .filter(|(key, _)| *key == node_key) - .copied() - .collect(); - let mut seen: BTreeSet<(&str, &str)> = pending.iter().copied().collect(); - while let Some(column) = pending.pop() { - for &index in by_near.get(&column).into_iter().flatten() { - reached.insert(index); - let far = end(&edges[index], upstream); - if seen.insert(far) { - pending.push(far); - } - } - } - reached -} - -/// An edge's source column, or its target column when `source` is false. -fn end(edge: &ColumnLineageEdge, source: bool) -> (&str, &str) { - if source { - (&edge.source_key, &edge.source_column) - } else { - (&edge.target_key, &edge.target_column) - } -} - -/// The inferred columns of the focal node and of every node an edge touches, in node key order. -pub(crate) fn node_columns( - nodes: &BTreeMap, - node_key: &str, - edges: &[ColumnLineageEdge], -) -> Vec { - let mut keys: BTreeSet<&str> = BTreeSet::from([node_key]); - for edge in edges { - keys.extend([edge.source_key.as_str(), edge.target_key.as_str()]); - } - keys.into_iter() - .filter_map(|key| nodes.get_key_value(key)) - .map(|(key, node)| LineageNodeColumns { - node_key: key.clone(), - kind: node.kind.clone(), - columns: definition_columns(&node.definition), - }) - .collect() -} - -fn definition_columns(definition: &Value) -> Vec { - let columns = definition.get("columns").and_then(Value::as_array); - columns - .into_iter() - .flatten() - .filter_map(|column| { - Some(LineageColumn { - name: column.get("name")?.as_str()?.to_owned(), - data_type: column - .get("type") - .and_then(Value::as_str) - .map(str::to_owned), - }) - }) - .collect() -} diff --git a/crates/sqlbuild-orchestration/src/warehouse_metadata/main/mod.rs b/crates/sqlbuild-orchestration/src/warehouse_metadata/main/mod.rs index f3f9f53dc5..f93dd8430f 100644 --- a/crates/sqlbuild-orchestration/src/warehouse_metadata/main/mod.rs +++ b/crates/sqlbuild-orchestration/src/warehouse_metadata/main/mod.rs @@ -10,7 +10,6 @@ pub mod ingest_statistics; pub mod list_person_catalog_nodes; pub(crate) mod prune_statistics; pub mod read_person_catalog; -pub mod read_person_column_lineage; pub mod read_person_fingerprint; pub mod read_person_node_status; pub mod read_person_statistics; diff --git a/crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_column_lineage.rs b/crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_column_lineage.rs deleted file mode 100644 index c03656a134..0000000000 --- a/crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_column_lineage.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! A person reads one node's column lineage with view on every current node, as the catalog listing requires. - -use uuid::Uuid; - -use crate::authorization::main::authorize_listing::authorize_listing; -use crate::credentials::models::{AuditContext, DefaultRole}; -use crate::snapshots::main::read_current_record::read_current_record; -use crate::snapshots::main::read_snapshot_documents::read_snapshot_documents; -use crate::snapshots::main::read_snapshot_nodes::read_snapshot_nodes; -use crate::store::errors::ControlError; -use crate::store::main::{begin::begin, commit::commit}; -use crate::store::models::{ControlStore, TransactionMode}; -use crate::warehouse_metadata::_helpers::catalog_nodes::{ - neighbourhood, node_columns, published_edges, -}; -use crate::warehouse_metadata::models::ColumnLineageRead; - -/// Columns and edges only, never SQL or expressions, which need view.sql. -pub async fn read_person_column_lineage( - store: &ControlStore, - credentials: (&AuditContext, Uuid), - (location_id, target, node_key): (Uuid, &str, &str), - default_role: DefaultRole, -) -> Result { - let (transaction, _) = authorize_listing( - begin(store, TransactionMode::Write).await?, - credentials, - (location_id, target), - default_role, - ) - .await?; - let (transaction, snapshot) = read_current_record(transaction, (location_id, target)).await?; - let Some(snapshot) = snapshot else { - commit(transaction).await?; - return Ok(ColumnLineageRead { - snapshot_id: None, - nodes: Vec::new(), - edges: Vec::new(), - }); - }; - let (transaction, nodes) = read_snapshot_nodes(transaction, snapshot.snapshot_id, None).await?; - let (transaction, documents) = - read_snapshot_documents(transaction, snapshot.snapshot_id, Some(&["lineage"])).await?; - commit(transaction).await?; - let edges = neighbourhood(&published_edges(documents.get("lineage"), &nodes), node_key); - Ok(ColumnLineageRead { - snapshot_id: Some(snapshot.snapshot_id), - nodes: node_columns(&nodes, node_key, &edges), - edges, - }) -} diff --git a/crates/sqlbuild-orchestration/src/warehouse_metadata/models.rs b/crates/sqlbuild-orchestration/src/warehouse_metadata/models.rs index e71dcc67ad..cf6e2589c2 100644 --- a/crates/sqlbuild-orchestration/src/warehouse_metadata/models.rs +++ b/crates/sqlbuild-orchestration/src/warehouse_metadata/models.rs @@ -645,35 +645,3 @@ pub struct CatalogNodeBuild { pub run_id: String, pub version_hash: String, } - -/// A node's columns as its compiled definition infers them; `data_type` is absent when inference could not tell. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] -pub struct LineageNodeColumns { - pub node_key: String, - pub kind: String, - pub columns: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] -pub struct LineageColumn { - pub name: String, - pub data_type: Option, -} - -/// One published column lineage edge; `transform` is the compiler's transform kind, such as `direct` or `cast`. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] -pub struct ColumnLineageEdge { - pub source_key: String, - pub source_column: String, - pub target_key: String, - pub target_column: String, - pub transform: String, -} - -/// The column lineage reaching or leaving one node, from the current snapshot; empty before one publishes lineage. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] -pub struct ColumnLineageRead { - pub snapshot_id: Option, - pub nodes: Vec, - pub edges: Vec, -} diff --git a/crates/sqlbuild-orchestration/tests/api/test_column_lineage.rs b/crates/sqlbuild-orchestration/tests/api/test_column_lineage.rs deleted file mode 100644 index 2af31100bf..0000000000 --- a/crates/sqlbuild-orchestration/tests/api/test_column_lineage.rs +++ /dev/null @@ -1,156 +0,0 @@ -//! Published column lineage reaches people as one node's upstream and downstream columns, on both control stores. - -use serde_json::json; -use uuid::Uuid; - -use crate::api::tests::test_types::ColumnLineageTestCase; -use crate::authorization::main::bootstrap_user::bootstrap_user; -use crate::credentials::main::identities::create_principal::create_principal; -use crate::credentials::models::{AuditContext, DefaultRole}; -use crate::credentials::tests::location_helpers::create_location; -use crate::runs::tests::helpers::snapshot; -use crate::snapshots::main::publish_snapshot::publish_snapshot; -use crate::snapshots::models::SnapshotNode; -use crate::store::errors::ControlError; -use crate::store::main::migrate_store::migrate_store; -use crate::store::tests::store_helpers::{close_test_store, open_test_store, run}; -use crate::store::tests::store_test_types::Backend; -use crate::warehouse_metadata::main::read_person_column_lineage::read_person_column_lineage; - -#[test] -fn given_published_column_lineage_when_people_read_a_node_then_only_its_reachable_columns_return() { - let test_cases = [ - ColumnLineageTestCase { - description: "SQLite column lineage", - backend: Backend::Sqlite, - expected_orders_edges: vec![ - ("model:raw_orders", "model:orders"), - ("model:orders", "model:order_totals"), - ], - expected_leaf_edges: 2, - }, - ColumnLineageTestCase { - description: "PostgreSQL column lineage", - backend: Backend::Postgres, - expected_orders_edges: vec![ - ("model:raw_orders", "model:orders"), - ("model:orders", "model:order_totals"), - ], - expected_leaf_edges: 2, - }, - ]; - for test_case in test_cases { - run(async { - let Some(fixture) = open_test_store(test_case.backend).await else { - return; - }; - let store = &fixture.store; - migrate_store(store).await.expect("migrate"); - let audit = AuditContext { - actor: "test-admin".into(), - granting_role: "admin".into(), - request_id: Uuid::now_v7(), - ip_address: None, - }; - let location_id = create_location(store, "orders", "test-admin") - .await - .expect("location"); - let model = |columns: serde_json::Value| SnapshotNode { - kind: "model".into(), - definition: json!({"materialization": "table", "columns": columns}), - raw_sql: Some("select 1 as id".into()), - compiled_sql: Some("select 1 as id".into()), - python_source: None, - }; - let mut input = snapshot(location_id); - input.nodes = [ - ( - "model:raw_orders", - model(json!([{"name": "amount", "type": "VARCHAR"}])), - ), - ( - "model:orders", - model(json!([{"name": "amount", "type": "DECIMAL(10,2)"}])), - ), - ( - "model:order_totals", - model(json!([{"name": "total", "type": null}])), - ), - ( - "model:inventory", - model(json!([{"name": "quantity", "type": "INTEGER"}])), - ), - ( - "model:inventory_report", - model(json!([{"name": "quantity", "type": "INTEGER"}])), - ), - ] - .into_iter() - .map(|(key, node)| (key.to_owned(), node)) - .collect(); - input.documents.insert( - "lineage".into(), - json!({"edges": [ - ["model:raw_orders", "amount", "model:orders", "amount", "cast"], - ["model:orders", "amount", "model:order_totals", "total", "aggregate"], - ["model:inventory", "quantity", "model:inventory_report", "quantity", "direct"], - ["model:retired_orders", "amount", "model:orders", "amount", "direct"], - ]}), - ); - publish_snapshot(store, &input).await.expect("publish"); - let admin = bootstrap_user(store, &audit, "orders-admin") - .await - .expect("administrator"); - let read = |node_key: &'static str, person: Uuid| { - read_person_column_lineage( - store, - (&audit, person), - (location_id, "development", node_key), - DefaultRole::None, - ) - }; - let orders = read("model:orders", admin).await.expect("orders lineage"); - assert_eq!( - orders - .edges - .iter() - .map(|e| (e.source_key.as_str(), e.target_key.as_str())) - .collect::>(), - test_case.expected_orders_edges, - "{}: both directions, never another node's chain or a node outside the snapshot", - test_case.description - ); - assert_eq!( - json!(orders.nodes), - json!([ - {"node_key": "model:order_totals", "kind": "model", "columns": [{"name": "total", "data_type": null}]}, - {"node_key": "model:orders", "kind": "model", "columns": [{"name": "amount", "data_type": "DECIMAL(10,2)"}]}, - {"node_key": "model:raw_orders", "kind": "model", "columns": [{"name": "amount", "data_type": "VARCHAR"}]}, - ]), - "{}", - test_case.description - ); - let leaf = read("model:order_totals", admin) - .await - .expect("leaf lineage"); - assert_eq!( - leaf.edges.len(), - test_case.expected_leaf_edges, - "{}: upstream is followed transitively", - test_case.description - ); - let stranger = create_principal(store, &audit, "orders-stranger") - .await - .expect("person"); - assert!( - matches!( - read("model:orders", stranger).await, - Err(ControlError::Denied { .. }) - ), - "{}: lineage needs view on every node", - test_case.description - ); - close_test_store(fixture).await; - }); - } -} diff --git a/crates/sqlbuild-orchestration/tests/api/test_contracts.rs b/crates/sqlbuild-orchestration/tests/api/test_contracts.rs index 584885c2a0..b59038211b 100644 --- a/crates/sqlbuild-orchestration/tests/api/test_contracts.rs +++ b/crates/sqlbuild-orchestration/tests/api/test_contracts.rs @@ -38,8 +38,6 @@ mod test_changes; mod test_checks; #[path = "test_cli_login.rs"] mod test_cli_login; -#[path = "test_column_lineage.rs"] -mod test_column_lineage; #[path = "test_contract.rs"] mod test_contract; #[path = "test_custom_roles.rs"] diff --git a/crates/sqlbuild-orchestration/tests/api/test_types.rs b/crates/sqlbuild-orchestration/tests/api/test_types.rs index 2ea6a1020a..5d3d2b50f3 100644 --- a/crates/sqlbuild-orchestration/tests/api/test_types.rs +++ b/crates/sqlbuild-orchestration/tests/api/test_types.rs @@ -76,13 +76,6 @@ pub(crate) struct LocationWorkTestCase { pub(crate) expected_generation: i64, } -pub(crate) struct ColumnLineageTestCase { - pub(crate) description: &'static str, - pub(crate) backend: Backend, - pub(crate) expected_orders_edges: Vec<(&'static str, &'static str)>, - pub(crate) expected_leaf_edges: usize, -} - pub(crate) struct WarehouseMetadataTestCase { pub(crate) description: &'static str, pub(crate) backend: Backend, diff --git a/crates/sqlbuild-python/src/bindings/_helpers/location_command/mod.rs b/crates/sqlbuild-python/src/bindings/_helpers/location_command/mod.rs index 5c4301a8d5..79131a3bb0 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/location_command/mod.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/location_command/mod.rs @@ -2,5 +2,4 @@ pub(crate) mod command; mod constants; mod runtime; mod snapshot; -mod snapshot_lineage; mod worker; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/location_command/snapshot.rs b/crates/sqlbuild-python/src/bindings/_helpers/location_command/snapshot.rs index 6e2353b734..e5e7028962 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/location_command/snapshot.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/location_command/snapshot.rs @@ -19,7 +19,6 @@ use crate::bindings::_helpers::compiled_project::snapshot_models::model_nodes; use crate::bindings::_helpers::compiled_project::snapshot_resources::resource_nodes; use crate::bindings::_helpers::compiled_project::snapshot_roles::compile_project_roles; use crate::bindings::_helpers::compiled_views::project_view::NativeProjectView; -use crate::bindings::_helpers::location_command::snapshot_lineage::column_lineage_document; use crate::bindings::_helpers::native_host::orchestration_declarations::collect_declarations; use crate::bindings::_helpers::native_host::orchestration_environment::environment_hash; use crate::bindings::_helpers::planner_identity::identities::direct_model_version_identities; @@ -80,11 +79,7 @@ pub(crate) fn build_orchestration_snapshot( )?; } input = compile_project_roles(py, input, (&project_dir, graph.bind(py).get().graph()))?; - let project = project.into_pyobject(py)?; - if let Some(lineage) = column_lineage_document(project.as_any())? { - input.documents.insert("lineage".into(), lineage); - } - input.nodes = identity_nodes(project.as_any(), input.nodes)?; + input.nodes = identity_nodes(project.into_pyobject(py)?.as_any(), input.nodes)?; Ok(NativeOrchestrationSnapshot { input }) } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/location_command/snapshot_lineage.rs b/crates/sqlbuild-python/src/bindings/_helpers/location_command/snapshot_lineage.rs deleted file mode 100644 index d6b65e7ef2..0000000000 --- a/crates/sqlbuild-python/src/bindings/_helpers/location_command/snapshot_lineage.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! The project's column lineage edges, published so people can trace columns without the project. - -use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult}; -use serde_json::{Value, json}; - -use crate::bindings::_helpers::lineage::project_lineage::build_project_column_lineage; -use crate::bindings::_helpers::records::construct::enum_member; -use crate::bindings::_helpers::records::fields::field; -use crate::bindings::_helpers::records::ids::RecordId; - -/// FAST column lineage of every model as `[source key, column, target key, column, transform]` rows; `None` without SQL analysis. -pub(crate) fn column_lineage_document(project: &Bound<'_, PyAny>) -> PyResult> { - let py = project.py(); - let mode = enum_member(py, RecordId::ColumnLineageMode, "FAST")?; - let Some(lineage) = build_project_column_lineage(project, None, &mode, None)? else { - return Ok(None); - }; - let edges = field(lineage.bind(py), "edges")?; - let mut rows: Vec = Vec::new(); - for edge in edges.try_iter()? { - let edge = edge?; - let (source, target) = (field(&edge, "source")?, field(&edge, "target")?); - rows.push(json!([ - node_key(&source)?, - field(&source, "column_name")?.extract::()?, - node_key(&target)?, - field(&target, "column_name")?.extract::()?, - field(&edge, "transform_kind")?.str()?.extract::()?, - ])); - } - Ok(Some(json!({ "edges": rows }))) -} - -/// The snapshot node key of a lineage column's resource, such as `model:orders`. -fn node_key(column: &Bound<'_, PyAny>) -> PyResult { - let kind: String = field(column, "resource_type")?.str()?.extract()?; - let name: String = field(column, "resource_name")?.extract()?; - Ok(format!("{kind}:{name}")) -} diff --git a/tests/integration/src/sqlbuild/compiler/compiled_project/_test_types.py b/tests/integration/src/sqlbuild/compiler/compiled_project/_test_types.py index 8291e2a98c..9fd65cab7a 100644 --- a/tests/integration/src/sqlbuild/compiler/compiled_project/_test_types.py +++ b/tests/integration/src/sqlbuild/compiler/compiled_project/_test_types.py @@ -233,13 +233,6 @@ class SnapshotEnvironmentTestCase: expected_environment_count: int -@dataclass(frozen=True) -class SnapshotLineageTestCase: - description: str - files: dict[str, str] - expected_edges: list[list[str]] - - @dataclass(frozen=True) class SnapshotScheduleTestCase: description: str diff --git a/tests/integration/src/sqlbuild/compiler/compiled_project/test_orchestration_snapshot.py b/tests/integration/src/sqlbuild/compiler/compiled_project/test_orchestration_snapshot.py index 9824426203..7437f8ca6a 100644 --- a/tests/integration/src/sqlbuild/compiler/compiled_project/test_orchestration_snapshot.py +++ b/tests/integration/src/sqlbuild/compiler/compiled_project/test_orchestration_snapshot.py @@ -35,7 +35,6 @@ SnapshotDiscoveryTestCase, SnapshotEnvironmentTestCase, SnapshotIdentityTestCase, - SnapshotLineageTestCase, SnapshotRejectionTestCase, SnapshotScheduleTestCase, ) @@ -327,59 +326,6 @@ def test_given_invalid_compilation_or_forged_nodes_when_publishing_then_no_snaps ) -@pytest.mark.parametrize( - "test_case", - [ - SnapshotLineageTestCase( - description="published column lineage follows passthroughs, casts and aggregations", - files={ - "sqlbuild_project.toml": 'name = "orders"\nadapter = "duckdb"\n', - "models/stg_orders.sql": ( - "MODEL (description 'Staged orders.');\n\n" - "SELECT 1 AS order_id, 'eu' AS region, '5.00' AS amount_text\n" - ), - "models/orders.sql": ( - "MODEL (description 'Typed orders.');\n\n" - "SELECT order_id, region, CAST(amount_text AS DECIMAL(10, 2)) AS amount\n" - 'FROM __ref("stg_orders")\n' - ), - "models/region_revenue.sql": ( - "MODEL (description 'Revenue by region.');\n\n" - "SELECT region, SUM(amount) AS revenue\n" - 'FROM __ref("orders")\nGROUP BY region\n' - ), - }, - expected_edges=[ - ["model:orders", "amount", "model:region_revenue", "revenue", "aggregation"], - ["model:orders", "region", "model:region_revenue", "region", "direct"], - ["model:stg_orders", "amount_text", "model:orders", "amount", "cast"], - ["model:stg_orders", "order_id", "model:orders", "order_id", "direct"], - ["model:stg_orders", "region", "model:orders", "region", "direct"], - ], - ), - ], - ids=lambda case: case.description, -) -def test_given_a_compiled_project_when_publishing_then_its_column_lineage_is_a_snapshot_document( - test_case: SnapshotLineageTestCase, - tmp_path: Path, -) -> None: - write_files(project_dir=tmp_path, files=test_case.files) - project: CompiledProjectView = build_compiled_project( - discovered_inputs=discover_project_inputs(project_dir=tmp_path), - adapter=DuckDbAdapter(), - resolved_connection={"database": "warehouse", "schema": "main"}, - ) - publication: dict[str, Any] = json.loads( - build_orchestration_snapshot(project, json.dumps(METADATA), tmp_path).to_json() - ) - edges: list[list[str]] = publication["documents"]["lineage"]["edges"] - assert sorted(edges) == test_case.expected_edges - sources: set[str] = {edge[0] for edge in edges} - targets: set[str] = {edge[2] for edge in edges} - assert sources | targets <= set(publication["nodes"]) - - @pytest.mark.parametrize( "test_case", [ diff --git a/ui/src/lib/api/http-catalog.test.ts b/ui/src/lib/api/http-catalog.test.ts index 398299bc76..12323431a7 100644 --- a/ui/src/lib/api/http-catalog.test.ts +++ b/ui/src/lib/api/http-catalog.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import type { components } from '$control/schema.d.ts'; -import { findNode, nodeDetailOf, toCatalogNodes, toColumnLineage } from './http-catalog'; +import { findNode, nodeDetailOf, toCatalogNodes } from './http-catalog'; import { toCheck } from './http-checks'; type W = components['schemas']; @@ -97,38 +97,3 @@ describe('catalog node detail', () => { expect(withSql.sql).toEqual({ raw: 'select {{ x }}', compiled: 'select {{ x }}' }); }); }); - -describe('column lineage mapping', () => { - const nodes = toCatalogNodes([node('model:raw_orders', { materialization: 'view' }), node('model:orders', { materialization: 'table' })]); - const edge = (source: string, target: string, transform: string): W['ColumnLineageEdge'] => ({ source_key: 'model:raw_orders', source_column: source, target_key: 'model:orders', target_column: target, transform }); - - it('keeps a name through direct or star as a passthrough, a new name as a rename, and names any other transform', () => { - const lineage = toColumnLineage( - { - snapshot_id: 's', - nodes: [ - { node_key: 'model:raw_orders', kind: 'model', columns: [{ name: 'amount', data_type: null }] }, - { node_key: 'model:orders', kind: 'model', columns: [{ name: 'amount', data_type: 'DECIMAL(10,2)' }] }, - { node_key: 'model:retired', kind: 'model', columns: [] } - ], - edges: [edge('id', 'id', 'direct'), edge('note', 'note', 'star'), edge('cust', 'customer_id', 'direct'), edge('amount', 'amount', 'cast')] - }, - nodes - ); - expect(lineage?.edges.map(([, target, lens, label]) => [target, lens, label])).toEqual([ - ['model:orders.id', 'passthrough', undefined], - ['model:orders.note', 'passthrough', undefined], - ['model:orders.customer_id', 'rename', undefined], - ['model:orders.amount', 'transform', 'Cast'] - ]); - expect(lineage?.models.map((m) => [m.id, m.name, m.kind, m.columns])).toEqual([ - ['model:raw_orders', 'raw_orders', 'view', [{ name: 'amount', type: '' }]], - ['model:orders', 'orders', 'table', [{ name: 'amount', type: 'DECIMAL(10,2)' }]] - ]); - expect([lineage?.sql, lineage?.expressions]).toEqual([{}, {}]); - }); - - it('has nothing to show when no lineage reaches the node', () => { - expect(toColumnLineage({ snapshot_id: 's', nodes: [], edges: [] }, nodes)).toBeNull(); - }); -}); diff --git a/ui/src/lib/api/http-catalog.ts b/ui/src/lib/api/http-catalog.ts index 76b50cb991..5c9c492fbd 100644 --- a/ui/src/lib/api/http-catalog.ts +++ b/ui/src/lib/api/http-catalog.ts @@ -1,7 +1,6 @@ import type { components } from '$control/schema.d.ts'; import { shortName } from '$lib/build-area/labels'; -import { cardStatus } from '$lib/lineage/catalog-graph'; -import type { CatalogNode, CatalogNodeDetail, Check, CheckCounts, ColumnLens, ColumnLineage, StaleReason } from './types/build'; +import type { CatalogNode, CatalogNodeDetail, Check, CheckCounts, StaleReason } from './types/build'; import type { NodeKind } from './types/operate'; type Wire = components['schemas']; @@ -105,34 +104,3 @@ export function nodeDetailOf(nodes: CatalogNode[], node: CatalogNode, status: Wi history: null }; } - -/** A kept name through `direct` or `star` is a passthrough, a new name a rename; every other transform changes the value. */ -function lensOf(edge: Wire['ColumnLineageEdge']): ColumnLens { - if (edge.transform !== 'direct' && edge.transform !== 'star') return 'transform'; - return edge.source_column === edge.target_column ? 'passthrough' : 'rename'; -} - -/** The compiler's transform kind as a label, such as `Cast`; passthroughs and renames keep the lens's own name. */ -function transformLabel(edge: Wire['ColumnLineageEdge']): string | undefined { - if (lensOf(edge) !== 'transform') return undefined; - return edge.transform === 'unknown' ? 'Transformed' : edge.transform.charAt(0).toUpperCase() + edge.transform.slice(1); -} - -/** - * Column lineage around one node, named and coloured from the catalog listing. The read carries no - * SQL or expressions, so those stay empty; null before a snapshot publishes lineage for the node. - */ -export function toColumnLineage(read: Wire['ColumnLineageRead'], nodes: CatalogNode[]): ColumnLineage | null { - if (!read.edges.length) return null; - const listed = new Map(nodes.map((n) => [n.key, n])); - return { - models: read.nodes.flatMap((n) => { - const node = listed.get(n.node_key); - const columns = n.columns.map((c) => ({ name: c.name, type: c.data_type ?? '' })); - return node ? [{ id: n.node_key, name: shortName(n.node_key), kind: node.kind, status: cardStatus(node), columns }] : []; - }), - edges: read.edges.map((e) => [`${e.source_key}.${e.source_column}`, `${e.target_key}.${e.target_column}`, lensOf(e), transformLabel(e)]), - sql: {}, - expressions: {} - }; -} diff --git a/ui/src/lib/api/http-client.ts b/ui/src/lib/api/http-client.ts index 6600d53438..952caa2133 100644 --- a/ui/src/lib/api/http-client.ts +++ b/ui/src/lib/api/http-client.ts @@ -2,13 +2,13 @@ import createFetch from 'openapi-fetch'; import type { components, paths } from '$control/schema.d.ts'; import { ApiError, type ApiClient } from './client'; import { checkDetailOf, toCheck, toCheckEvidence, toCheckSql } from './http-checks'; -import { findNode, nodeDetailOf, toCatalogNodes, toColumnLineage } from './http-catalog'; +import { findNode, nodeDetailOf, toCatalogNodes } from './http-catalog'; import { toSnapshotChanges } from './http-changes'; import { jobDetailOf, toJob, toTick, toTrigger, triggerDetailOf } from './http-jobs'; import { RUN_WINDOW, runDetailOf, runRowPage, runsMatching, runTabCountsOf, toRunNode, toRuns } from './http-runs'; import type { ChangeSubscription, ChangedChannel, ChangedSignal, ConnectionState } from './signals'; import type { AuthMode, Bootstrap, Capability, Location, Scope } from './types'; -import type { CatalogNode, CatalogNodeDetail, Check, ColumnLineage, CheckDetail, CheckEvidence, CheckSql, SnapshotChange } from './types/build'; +import type { CatalogNode, CatalogNodeDetail, Check, CheckDetail, CheckEvidence, CheckSql, SnapshotChange } from './types/build'; import type { Job, JobDetail, RerunMode, Run, RunDetail, RunListQuery, RunRowPage, RunRowQuery, RunTabCounts, Trigger, TriggerDetail, TriggerState } from './types/operate'; type Wire = components['schemas']; @@ -39,7 +39,6 @@ const SUPPORTED: ReadonlySet = new Set([ 'getCheckEvidence', 'listCatalogNodes', 'getCatalogNode', - 'getColumnLineage', 'listSnapshotChanges' ]); @@ -264,18 +263,6 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt if (!snapshots.data) throw new ApiError(errorText(snapshots.error, snapshots.response), snapshots.response.status); return toSnapshotChanges(snapshots.data, runs); }, - async getColumnLineage(scope: Scope, ref: string): Promise { - const nodes = await implemented.listCatalogNodes(scope); - const node = findNode(nodes, ref); - if (!node) return null; - const { locationId, target } = parseLocationKey(scope.locationId); - const { data, error, response } = await http.GET('/api/control/locations/{location_id}/targets/{target}/column-lineage', { - params: { path: { location_id: locationId, target }, query: { node_key: node.key } } - }); - if (response.status === 401) onUnauthorized(); - if (!data) throw new ApiError(errorText(error, response), response.status); - return toColumnLineage(data, nodes); - }, async getCatalogNode(scope: Scope, ref: string): Promise { const nodes = await implemented.listCatalogNodes(scope); const node = findNode(nodes, ref); diff --git a/ui/src/lib/api/types/build.ts b/ui/src/lib/api/types/build.ts index 81e534b63f..906f9e77df 100644 --- a/ui/src/lib/api/types/build.ts +++ b/ui/src/lib/api/types/build.ts @@ -193,9 +193,9 @@ export type CatalogNodeDetail = Omit & { export type ColumnLens = 'raw' | 'passthrough' | 'rename' | 'transform'; export type ColumnLineage = { - models: { id: string; name: string; kind: string; status: 'ok' | 'fail' | 'warn' | 'stale' | 'src' | 'none'; columns: { name: string; type: string }[] }[]; - /** `model.column` pairs from upstream to downstream, with an optional label naming the transform, such as `Cast`. */ - edges: [string, string, ColumnLens, string?][]; + models: { id: string; name: string; kind: string; status: 'ok' | 'fail' | 'warn' | 'stale' | 'src'; columns: { name: string; type: string }[] }[]; + /** `model.column` pairs from upstream to downstream. */ + edges: [string, string, ColumnLens][]; sql: Record; /** The defining expression of each column, per model. */ expressions: Record>; diff --git a/ui/src/lib/components/lineage/col-cell-node.svelte b/ui/src/lib/components/lineage/col-cell-node.svelte index 0ed67190b9..c1f24b1380 100644 --- a/ui/src/lib/components/lineage/col-cell-node.svelte +++ b/ui/src/lib/components/lineage/col-cell-node.svelte @@ -7,10 +7,9 @@ model: string; column: string; kind: string; - status: 'ok' | 'fail' | 'warn' | 'stale' | 'src' | 'none'; + status: 'ok' | 'fail' | 'warn' | 'stale' | 'src'; lens?: Lens; - /** What the incoming edge does, such as `Cast`. */ - label?: string; + lensLabel?: Record; focal?: boolean; active?: boolean; }; @@ -23,8 +22,7 @@ fail: 'var(--sb-error)', warn: 'var(--sb-warning)', stale: 'var(--sb-stale)', - src: 'var(--sb-text-faint)', - none: 'var(--border)' + src: 'var(--sb-text-faint)' }; const KindIcon = $derived(d.kind === 'view' ? Eye : d.kind.startsWith('source') ? Database : d.kind.startsWith('incremental') ? Layers : Table2); const kindColor = $derived(d.kind === 'view' ? '#5aa9e6' : d.kind === 'table' ? '#2e90ff' : d.kind.startsWith('incremental') ? '#3fb6c9' : '#7c8aa5'); @@ -33,22 +31,21 @@
    - {d.column} + {d.column}
    - - {d.model} - {#if d.lens && d.label} - {d.label} + + {d.model} + {#if d.lens && d.lensLabel} + {d.lensLabel[d.lens]} {:else} {d.kind} {/if} diff --git a/ui/src/lib/lineage/column-lineage.svelte b/ui/src/lib/lineage/column-lineage.svelte index 00096734b7..b418a1ab07 100644 --- a/ui/src/lib/lineage/column-lineage.svelte +++ b/ui/src/lib/lineage/column-lineage.svelte @@ -12,7 +12,7 @@ import { highlightSqlbuild } from '$lib/sql/highlight'; import { tick } from 'svelte'; - type ColModel = { id: string; name: string; kind: string; status: 'ok' | 'fail' | 'warn' | 'stale' | 'src' | 'none'; columns: { name: string; type: string }[] }; + type ColModel = { id: string; name: string; kind: string; status: 'ok' | 'fail' | 'warn' | 'stale' | 'src'; columns: { name: string; type: string }[] }; type Lens = 'raw' | 'passthrough' | 'rename' | 'transform'; let { @@ -24,7 +24,7 @@ }: { focal: string; models: ColModel[]; - colEdges: [string, string, Lens, string?][]; + colEdges: [string, string, Lens][]; /** per-model SQL: modelId -> SQL text */ modelSql?: Record; /** per-model column expressions: modelId -> { colName -> defining substring } */ @@ -70,9 +70,6 @@ const kindOf = (id: string) => models.find((m) => m.id === id)?.kind ?? 'table'; const statusOf = (id: string) => models.find((m) => m.id === id)?.status ?? 'ok'; const incomingLens = $derived(new Map(colEdges.map(([, t, lens]) => [t, lens]))); - // What a column's incoming edge does: the published transform, such as Cast, or the lens's name. - const incomingLabel = $derived(new Map(colEdges.map(([, t, lens, label]) => [t, label ?? lensLabel[lens]]))); - const labelOf = (cid: string) => incomingLabel.get(cid) ?? lensLabel.raw; const focalModel = $derived(models.find((m) => m.id === focal)); let colQuery = $state(''); @@ -226,7 +223,7 @@ kind: kindOf(modelOf(cid)), status: statusOf(modelOf(cid)), lens: incomingLens.get(cid), - label: incomingLens.has(cid) ? labelOf(cid) : undefined, + lensLabel, focal: cid === selectedCol, active: cid === activeStep } @@ -287,9 +284,9 @@ onclick={() => (selectedCol = cid)} > {#if selectedCol === cid}{/if} - {c.name} - {lens ? labelOf(cid) : ''} - {#if c.type}{c.type.toLowerCase()}{/if} + {c.name} + {lens ? lensLabel[lens] : ''} + {c.type.toLowerCase()} {:else}
    No columns match.
    @@ -372,8 +369,8 @@
  • @@ -386,8 +383,8 @@
  • @@ -397,7 +394,7 @@ {#if sqlParts && pinnedNode}
    SQL · {sqlParts.model}.sql
    {@html sqlParts.before}{#if sqlParts.match}{@html sqlParts.match}{/if}{@html sqlParts.after}
    - {:else if modelSql && Object.keys(modelSql).length} + {:else}

    Select a step to see it in the model's SQL.

    {/if} @@ -501,14 +498,11 @@ border-color: var(--primary); background: var(--primary); } - /* The identifier being traced wraps rather than truncates. */ .dv-name { + overflow: hidden; font: 12.5px var(--font-mono); - overflow-wrap: anywhere; - } - .col-name { - min-width: 0; - overflow-wrap: anywhere; + text-overflow: ellipsis; + white-space: nowrap; } .dv-name .m { color: var(--muted-foreground); From 2cd87572d7f737ecdc2b8f081f7586faf09c3080 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 17:28:00 +0000 Subject: [PATCH 064/106] feat: show per-node run history and selecting jobs on catalog node pages A person read lists the scheduler runs that built one node in a target, newest first, behind the same view-on-every-node gate as each run's node list, and carries the same metadata. The catalog node page fills its Runs tab, recent build strip and last duration from it, and lists the jobs and groups whose selection includes the node from the existing jobs and triggers reads. The catalog list drops the kind prefix from node names, and the runs list gives its Started column room for queued times. --- clients/typescript/src/schema.d.ts | 75 +++++++++++++++++++ .../authentication/password_sessions.rs | 4 + .../api/_helpers/resources/person_reads.rs | 31 +++++++- .../sqlbuild-orchestration/src/api/models.rs | 1 + .../sqlbuild-orchestration/src/runs/models.rs | 14 ++++ .../src/warehouse_history/_helpers/mod.rs | 1 + .../warehouse_history/_helpers/node_runs.rs | 22 ++++++ .../src/warehouse_history/main/mod.rs | 1 + .../main/read_person_node_runs.rs | 31 ++++++++ .../tests/api/test_denials.rs | 3 +- .../tests/api/test_person_sessions.rs | 46 ++++++++++++ fensu.toml | 2 +- ui/src/lib/api/http-catalog.test.ts | 37 ++++++++- ui/src/lib/api/http-catalog.ts | 55 +++++++++++--- ui/src/lib/api/http-client.ts | 14 ++-- ui/src/routes/catalog/+page.svelte | 4 +- ui/src/routes/runs/run-list.svelte | 4 +- 17 files changed, 322 insertions(+), 23 deletions(-) create mode 100644 crates/sqlbuild-orchestration/src/warehouse_history/_helpers/node_runs.rs create mode 100644 crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_node_runs.rs diff --git a/clients/typescript/src/schema.d.ts b/clients/typescript/src/schema.d.ts index 2aebecfe52..788cf6753e 100644 --- a/clients/typescript/src/schema.d.ts +++ b/clients/typescript/src/schema.d.ts @@ -434,6 +434,22 @@ export interface paths { patch?: never; trace?: never; }; + "/api/control/locations/{location_id}/targets/{target}/nodes/{node_key}/runs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["listPersonNodeRuns"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/control/locations/{location_id}/targets/{target}/nodes/{node_key}/sql": { parameters: { query?: never; @@ -1409,6 +1425,23 @@ export interface components { NodeEventKind: "success" | "failed" | "recovered"; /** @enum {string} */ NodeOutcome: "success" | "warn" | "failed" | "skipped" | "cancelled"; + /** @description One build attempt of a node by a run in this target; the same metadata as the run's node list. */ + NodeRunRecord: { + /** Format: date-time */ + ended_at?: string | null; + error_code?: string | null; + job_key?: string | null; + /** Format: date-time */ + queued_at: string; + /** Format: int64 */ + rows_affected?: number | null; + /** Format: uuid */ + run_id: string; + skipped_because_of?: string | null; + /** Format: date-time */ + started_at?: string | null; + status: string; + }; /** @description SQL inspection never returns Python source, execution artifacts or unrelated node metadata. */ NodeSql: { compiled_sql?: string | null; @@ -3202,6 +3235,48 @@ export interface operations { }; }; }; + listPersonNodeRuns: { + parameters: { + query?: { + /** @description Maximum recent runs; defaults to 100, capped at 1000 */ + limit?: number; + }; + header?: never; + path: { + location_id: string; + target: string; + node_key: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Runs that built the node, newest first */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NodeRunRecord"][]; + }; + }; + /** @description Session rejected */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + }; + }; readPersonNodeSql: { parameters: { query?: never; diff --git a/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs b/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs index 5bce2d5b43..c59f0a2119 100644 --- a/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs +++ b/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs @@ -137,6 +137,10 @@ pub(crate) fn person_routes() -> Router { "/api/control/locations/{location_id}/targets/{target}/node-status", get(crate::api::_helpers::resources::person_operations::node_status), ) + .route( + "/api/control/locations/{location_id}/targets/{target}/nodes/{node_key}/runs", + get(crate::api::_helpers::resources::person_reads::node_runs), + ) .route( "/api/control/locations/{location_id}/targets/{target}/nodes/{node_key}/sql", get(crate::api::_helpers::resources::person_node_sql::read), diff --git a/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_reads.rs b/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_reads.rs index aa10e0c867..2440ab37a1 100644 --- a/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_reads.rs +++ b/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_reads.rs @@ -1,4 +1,4 @@ -//! Person reads of run nodes, catalog nodes, jobs, triggers, ticks and publications; refusal precedes every lookup, so unknown objects look forbidden. +//! Person reads of run nodes, node run history, catalog nodes, jobs, triggers, ticks and publications; refusal precedes every lookup, so unknown objects look forbidden. use axum::extract::{Path, Query, State}; use axum::http::StatusCode; @@ -12,12 +12,13 @@ use crate::declarations::main::list_person_jobs::list_person_jobs; use crate::declarations::main::list_person_triggers::list_person_triggers; use crate::declarations::models::{JobRecord, TriggerRecord}; use crate::runs::main::list_person_run_nodes::list_person_run_nodes; -use crate::runs::models::RunNodeRecord; +use crate::runs::models::{NodeRunRecord, RunNodeRecord}; use crate::scheduler::main::list_person_ticks::list_person_ticks; use crate::scheduler::models::TickRecord; use crate::snapshots::main::list_person_snapshots::list_person_snapshots; use crate::snapshots::models::PublishedSnapshot; use crate::store::models::ControlStore; +use crate::warehouse_history::main::read_person_node_runs::read_person_node_runs; use crate::warehouse_metadata::main::list_person_catalog_nodes::list_person_catalog_nodes; use crate::warehouse_metadata::models::CatalogNodeRecord; @@ -66,6 +67,32 @@ pub(crate) async fn nodes( )) } +#[utoipa::path(get, path = "/api/control/locations/{location_id}/targets/{target}/nodes/{node_key}/runs", operation_id = "listPersonNodeRuns", + params(("location_id" = Uuid, Path), ("target" = String, Path), ("node_key" = String, Path), + ("limit" = Option, Query, description = "Maximum recent runs; defaults to 100, capped at 1000")), + responses((status = 200, body = Vec, description = "Runs that built the node, newest first"), + (status = 401, description = "Session rejected"), + (status = 403, body = crate::api::models::ApiErrorBody)), security(("session" = []), ("cliSession" = [])))] +pub(crate) async fn node_runs( + (State(store), Extension(context)): (State, Extension), + Extension(default_role): Extension, + (Path((location_id, target, node_key)), Query(options)): ( + Path<(Uuid, String, String)>, + Query, + ), + Extension(AuthenticatedPerson(principal, _)): Extension, +) -> Result>, ApiError> { + Ok(Json( + read_person_node_runs( + &store, + (&context, principal), + (location_id, &target, &node_key), + (options.limit.unwrap_or(100), default_role), + ) + .await?, + )) +} + #[utoipa::path(get, path = "/api/control/locations/{location_id}/targets/{target}/jobs", operation_id = "listPersonJobs", params(("location_id" = Uuid, Path), ("target" = String, Path)), responses((status = 200, body = Vec), (status = 401, description = "Session rejected"), diff --git a/crates/sqlbuild-orchestration/src/api/models.rs b/crates/sqlbuild-orchestration/src/api/models.rs index 9571345a46..5a0e02d980 100644 --- a/crates/sqlbuild-orchestration/src/api/models.rs +++ b/crates/sqlbuild-orchestration/src/api/models.rs @@ -22,6 +22,7 @@ use crate::api::_helpers::resources::{ person_operations::recent_runs, person_reads::run_nodes, person_reads::nodes, + person_reads::node_runs, person_reads::jobs, person_reads::triggers, person_reads::ticks, diff --git a/crates/sqlbuild-orchestration/src/runs/models.rs b/crates/sqlbuild-orchestration/src/runs/models.rs index 545660e12d..bd8a64843d 100644 --- a/crates/sqlbuild-orchestration/src/runs/models.rs +++ b/crates/sqlbuild-orchestration/src/runs/models.rs @@ -213,6 +213,20 @@ pub struct RunNodeRecord { pub skipped_because_of: Option, } +/// One build attempt of a node by a run in this target; the same metadata as the run's node list. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] +pub struct NodeRunRecord { + pub run_id: Uuid, + pub job_key: Option, + pub queued_at: DateTime, + pub status: String, + pub started_at: Option>, + pub ended_at: Option>, + pub rows_affected: Option, + pub error_code: Option, + pub skipped_because_of: Option, +} + /// The database owns the claim before a launcher sees the pinned request. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, utoipa::ToSchema)] pub struct RunClaim { diff --git a/crates/sqlbuild-orchestration/src/warehouse_history/_helpers/mod.rs b/crates/sqlbuild-orchestration/src/warehouse_history/_helpers/mod.rs index 51c3c3a651..8ca7426d23 100644 --- a/crates/sqlbuild-orchestration/src/warehouse_history/_helpers/mod.rs +++ b/crates/sqlbuild-orchestration/src/warehouse_history/_helpers/mod.rs @@ -1,3 +1,4 @@ pub(crate) mod checks; pub(crate) mod coverage; pub(crate) mod maintenance; +pub(crate) mod node_runs; diff --git a/crates/sqlbuild-orchestration/src/warehouse_history/_helpers/node_runs.rs b/crates/sqlbuild-orchestration/src/warehouse_history/_helpers/node_runs.rs new file mode 100644 index 0000000000..633a2477fb --- /dev/null +++ b/crates/sqlbuild-orchestration/src/warehouse_history/_helpers/node_runs.rs @@ -0,0 +1,22 @@ +//! Bounded run metadata for one node; it never includes SQL, sampled rows or captured output. + +use uuid::Uuid; + +use crate::runs::models::NodeRunRecord; +use crate::store::errors::ControlError; +use crate::store::main::query_records::query_records; +use crate::store::models::{ControlTransaction, Parameter}; + +pub(crate) async fn node_runs_in( + transaction: ControlTransaction, + (location_id, target, node_key): (Uuid, &str, &str), + limit: u32, +) -> Result<(ControlTransaction, Vec), ControlError> { + query_records(transaction, + "SELECT r.run_id, r.job_key, r.queued_at, n.status, n.started_at, n.ended_at, n.rows_affected, n.error_code, \ + n.skipped_because_of FROM sqb_run_nodes n JOIN sqb_runs r ON r.run_id=n.run_id \ + WHERE r.location_id=$1 AND r.target=$2 AND n.node_key=$3 AND n.kind='node' \ + ORDER BY r.queued_at DESC, r.run_id DESC LIMIT $4", + vec![Parameter::Id(location_id), Parameter::Text(target.into()), Parameter::Text(node_key.into()), + Parameter::Integer(i64::from(limit.min(1000)))]).await +} diff --git a/crates/sqlbuild-orchestration/src/warehouse_history/main/mod.rs b/crates/sqlbuild-orchestration/src/warehouse_history/main/mod.rs index 55736dc22c..d29b0b83b3 100644 --- a/crates/sqlbuild-orchestration/src/warehouse_history/main/mod.rs +++ b/crates/sqlbuild-orchestration/src/warehouse_history/main/mod.rs @@ -4,3 +4,4 @@ pub mod read_person_check_sql; pub mod read_person_checks; pub mod read_person_maintenance; pub mod read_person_microbatch_coverage; +pub mod read_person_node_runs; diff --git a/crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_node_runs.rs b/crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_node_runs.rs new file mode 100644 index 0000000000..d2481e9ad5 --- /dev/null +++ b/crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_node_runs.rs @@ -0,0 +1,31 @@ +//! A node's run history needs view on every current node, as each run's node list does. + +use uuid::Uuid; + +use crate::authorization::main::authorize_listing::authorize_listing; +use crate::credentials::models::{AuditContext, DefaultRole}; +use crate::runs::models::NodeRunRecord; +use crate::store::errors::ControlError; +use crate::store::main::{begin::begin, commit::commit}; +use crate::store::models::{ControlStore, TransactionMode}; +use crate::warehouse_history::_helpers::node_runs::node_runs_in; + +/// The runs that built one node in this target, newest first; a key no run built returns an empty list. +pub async fn read_person_node_runs( + store: &ControlStore, + (context, credentials): (&AuditContext, Uuid), + (location_id, target, node_key): (Uuid, &str, &str), + (limit, default_role): (u32, DefaultRole), +) -> Result, ControlError> { + let (transaction, _) = authorize_listing( + begin(store, TransactionMode::Write).await?, + (context, credentials), + (location_id, target), + default_role, + ) + .await?; + let (transaction, runs) = + node_runs_in(transaction, (location_id, target, node_key), limit).await?; + commit(transaction).await?; + Ok(runs) +} diff --git a/crates/sqlbuild-orchestration/tests/api/test_denials.rs b/crates/sqlbuild-orchestration/tests/api/test_denials.rs index d2cb06ea01..95fc367fe9 100644 --- a/crates/sqlbuild-orchestration/tests/api/test_denials.rs +++ b/crates/sqlbuild-orchestration/tests/api/test_denials.rs @@ -20,9 +20,10 @@ use crate::store::tests::store_helpers::{close_test_store, open_test_store, run} use crate::store::tests::store_test_types::Backend; /// Location-scoped reads and the action each needs. -const READS: [(&str, &str); 12] = [ +const READS: [(&str, &str); 13] = [ ("runs", "view"), ("nodes", "view"), + ("nodes/orders/runs", "view"), ("jobs", "view"), ("triggers", "view"), ("changes", "view"), diff --git a/crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs b/crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs index cc253368c7..b208bc31a5 100644 --- a/crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs +++ b/crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs @@ -286,6 +286,9 @@ fn given_browser_session_roles_when_work_is_requested_then_scope_revocation_and_ let known_reads = [ format!("{metadata}/{}/nodes", seed_run.run_id), format!("/api/control/locations/{location}/targets/development/nodes"), + format!( + "/api/control/locations/{location}/targets/development/nodes/orders/runs" + ), format!("/api/control/locations/{location}/targets/development/jobs"), format!("/api/control/locations/{location}/targets/development/triggers"), format!("/api/control/locations/{location}/targets/development/snapshots"), @@ -347,6 +350,49 @@ fn given_browser_session_roles_when_work_is_requested_then_scope_revocation_and_ expected_jobs[..expected_jobs.len() * usize::from(jobs.is_array())], "jobs list declared metadata and never settings" ); + let node_runs: Value = request( + &provider.client, + session, + ( + Method::GET, + &format!("/api/control/locations/{location}/targets/development/nodes/orders/runs"), + ), + (&Value::Null, &session.csrf), + ) + .await + .json() + .await + .expect("node runs response"); + let seeded_node_runs: Vec<&Value> = node_runs + .as_array() + .into_iter() + .flatten() + .filter(|row| row["run_id"] == json!(seed_run.run_id)) + .collect(); + assert_eq!( + seeded_node_runs.len(), + usize::from(node_runs.is_array()), + "the seed run built orders once" + ); + for seeded in seeded_node_runs { + for (key, expected) in [ + ("job_key", Value::Null), + ("status", json!("pending")), + ("error_code", Value::Null), + ] { + assert_eq!(seeded[key], expected, "node run {key}"); + } + } + assert_eq!( + node_runs + .as_array() + .into_iter() + .flatten() + .filter(|row| row["job_key"] == json!("orders")) + .count(), + launched_person_runs * usize::from(node_runs.is_array()), + "every person launch of the orders job built orders" + ); for read_path in &unknown_reads { let missing = request( &provider.client, diff --git a/fensu.toml b/fensu.toml index 7c1f8e7b53..bc1b9c46ca 100644 --- a/fensu.toml +++ b/fensu.toml @@ -1381,7 +1381,7 @@ paths = ["crates/sqlbuild-orchestration/src/api/_helpers/resources/administrator reason = "Each function is one utoipa-annotated axum route: OpenAPI needs one annotated handler per operation, and each body only checks the caller's scope and calls one store operation. Scope checks and store work live in their shared owners." [[dupes.allowlist]] -paths = ["crates/sqlbuild-orchestration/src/authorization/main/read_security_audit.rs", "crates/sqlbuild-orchestration/src/credentials/_helpers/active_session.rs", "crates/sqlbuild-orchestration/src/declarations/_helpers/reads.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_person_jobs.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_person_triggers.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_triggers.rs", "crates/sqlbuild-orchestration/src/platform/main/read_location_work.rs", "crates/sqlbuild-orchestration/src/runs/main/list_person_runs.rs", "crates/sqlbuild-orchestration/src/runs/main/list_person_run_nodes.rs", "crates/sqlbuild-orchestration/src/runs/main/list_run_nodes.rs", "crates/sqlbuild-orchestration/src/runs/main/list_runs.rs", "crates/sqlbuild-orchestration/src/runs/_helpers/lifecycle/reads.rs", "crates/sqlbuild-orchestration/src/warehouse_history/_helpers/checks.rs", "crates/sqlbuild-orchestration/src/scheduler/main/list_person_ticks.rs", "crates/sqlbuild-orchestration/src/store/main/read_records.rs", "crates/sqlbuild-orchestration/src/platform/main/location_work.rs", "crates/sqlbuild-orchestration/src/scheduler/main/list_ticks.rs", "crates/sqlbuild-orchestration/src/sensors/_helpers/attempts.rs", "crates/sqlbuild-orchestration/src/sensors/_helpers/facts.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_current_record.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_snapshot_documents.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_snapshot_nodes.rs", "crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_catalog.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_check.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_checks.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_maintenance.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_microbatch_coverage.rs", "crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_statistics.rs"] +paths = ["crates/sqlbuild-orchestration/src/authorization/main/read_security_audit.rs", "crates/sqlbuild-orchestration/src/credentials/_helpers/active_session.rs", "crates/sqlbuild-orchestration/src/declarations/_helpers/reads.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_person_jobs.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_person_triggers.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_triggers.rs", "crates/sqlbuild-orchestration/src/platform/main/read_location_work.rs", "crates/sqlbuild-orchestration/src/runs/main/list_person_runs.rs", "crates/sqlbuild-orchestration/src/runs/main/list_person_run_nodes.rs", "crates/sqlbuild-orchestration/src/runs/main/list_run_nodes.rs", "crates/sqlbuild-orchestration/src/runs/main/list_runs.rs", "crates/sqlbuild-orchestration/src/runs/_helpers/lifecycle/reads.rs", "crates/sqlbuild-orchestration/src/warehouse_history/_helpers/checks.rs", "crates/sqlbuild-orchestration/src/scheduler/main/list_person_ticks.rs", "crates/sqlbuild-orchestration/src/store/main/read_records.rs", "crates/sqlbuild-orchestration/src/platform/main/location_work.rs", "crates/sqlbuild-orchestration/src/scheduler/main/list_ticks.rs", "crates/sqlbuild-orchestration/src/sensors/_helpers/attempts.rs", "crates/sqlbuild-orchestration/src/sensors/_helpers/facts.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_current_record.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_snapshot_documents.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_snapshot_nodes.rs", "crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_catalog.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_check.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_checks.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_maintenance.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_microbatch_coverage.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_node_runs.rs", "crates/sqlbuild-orchestration/src/warehouse_history/_helpers/node_runs.rs", "crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_statistics.rs"] reason = "Each is one control-store read with its own SQL and record type. Transactions, person authorization and decoding go through the shared begin_authorized, query_records, read_records and query_bodies helpers; what remains alike is the call shape." [[dupes.allowlist]] diff --git a/ui/src/lib/api/http-catalog.test.ts b/ui/src/lib/api/http-catalog.test.ts index 12323431a7..d8c3d60150 100644 --- a/ui/src/lib/api/http-catalog.test.ts +++ b/ui/src/lib/api/http-catalog.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import type { components } from '$control/schema.d.ts'; -import { findNode, nodeDetailOf, toCatalogNodes } from './http-catalog'; +import { findNode, nodeDetailOf, nodeJobsOf, toCatalogNodes, toNodeRuns } from './http-catalog'; import { toCheck } from './http-checks'; type W = components['schemas']; @@ -97,3 +97,38 @@ describe('catalog node detail', () => { expect(withSql.sql).toEqual({ raw: 'select {{ x }}', compiled: 'select {{ x }}' }); }); }); + +describe('catalog node runs and jobs', () => { + const snapshot = '01a1b000-0000-7000-8000-000000000001'; + const job = (key: string, kind: 'job' | 'group', selection: string[]): W['JobRecord'] => ({ job_key: key, kind, description: key, selection, concurrency_groups: [], snapshot_id: snapshot }); + const trigger = (id: string, jobKey: string) => ({ trigger_id: id, job_key: jobKey }) as W['TriggerRecord']; + const run = (id: string, status: string, extra: Partial = {}): W['NodeRunRecord'] => ({ run_id: id, job_key: 'orders', queued_at: '2026-11-08T05:59:00Z', status, ...extra }); + + it('lists the jobs and groups that select the node with their own triggers only', () => { + const jobs = [job('orders', 'job', ['model:orders']), job('nightly', 'group', ['model:orders', 'model:customers']), job('customers', 'job', ['model:customers'])]; + const jobsOfOrders = nodeJobsOf('model:orders', jobs, [trigger('t1', 'orders'), trigger('t2', 'customers'), trigger('t3', 'orders')]); + expect(jobsOfOrders).toEqual([ + { key: 'orders', kind: 'job', triggerIds: ['t1', 't3'] }, + { key: 'nightly', kind: 'group', triggerIds: [] } + ]); + }); + + it('keeps finished builds newest first with duration and the reason a node failed or was skipped', () => { + const runs = toNodeRuns([ + run('r4', 'running', { started_at: '2026-11-08T06:00:00Z' }), + run('r3', 'skipped', { skipped_because_of: 'model:stg_orders' }), + run('r2', 'failed', { started_at: '2026-11-08T05:00:00Z', ended_at: '2026-11-08T05:00:01.5Z', error_code: 'warehouse_error' }), + run('r1', 'success', { job_key: null, started_at: '2026-11-08T04:00:00Z', ended_at: '2026-11-08T04:00:30Z', rows_affected: 12 }), + run('r0', 'cancelled') + ]); + expect(runs.map((r) => [r.runId, r.outcome, r.jobKey, r.durationMs, r.rows, r.message])).toEqual([ + ['r3', 'skipped', 'orders', null, null, 'Skipped because stg_orders did not succeed'], + ['r2', 'failed', 'orders', 1500, null, 'warehouse_error'], + ['r1', 'success', null, 30000, 12, null] + ]); + expect(runs[0].startedAt).toBe('2026-11-08T05:59:00Z'); + const nodes = toCatalogNodes([node('model:orders', { materialization: 'table' })]); + const d = nodeDetailOf(nodes, nodes[0], null, null, [], { runs, jobs: [] }); + expect([d.recentOutcomes, d.lastDurationMs, d.jobs]).toEqual([['success', 'failed', 'skipped'], 1500, []]); + }); +}); diff --git a/ui/src/lib/api/http-catalog.ts b/ui/src/lib/api/http-catalog.ts index 5c9c492fbd..d7afd7683b 100644 --- a/ui/src/lib/api/http-catalog.ts +++ b/ui/src/lib/api/http-catalog.ts @@ -1,6 +1,6 @@ import type { components } from '$control/schema.d.ts'; import { shortName } from '$lib/build-area/labels'; -import type { CatalogNode, CatalogNodeDetail, Check, CheckCounts, StaleReason } from './types/build'; +import type { CatalogNode, CatalogNodeDetail, Check, CheckCounts, NodeJob, NodeOutcome, NodeRunEntry, StaleReason } from './types/build'; import type { NodeKind } from './types/operate'; type Wire = components['schemas']; @@ -73,12 +73,49 @@ export function findNode(nodes: CatalogNode[], ref: string): CatalogNode | null return nodes.find((n) => n.key === ref) ?? nodes.find((n) => n.key === `model:${ref}`) ?? nodes.find((n) => shortName(n.key) === ref) ?? null; } +/** Jobs and groups whose resolved selection includes the node, with the triggers that start each. */ +export function nodeJobsOf(nodeKey: string, jobs: Wire['JobRecord'][], triggers: Wire['TriggerRecord'][]): NodeJob[] { + return jobs + .filter((job) => job.selection.includes(nodeKey)) + .map((job) => ({ key: job.job_key, kind: job.kind === 'group' ? 'group' : 'job', triggerIds: triggers.filter((t) => t.job_key === job.job_key).map((t) => t.trigger_id) })); +} + +const FINISHED: readonly NodeOutcome[] = ['success', 'warn', 'failed', 'skipped']; + +/** Finished builds of the node by scheduler runs, newest first; queued, running and cancelled attempts are left out. */ +export function toNodeRuns(wire: Wire['NodeRunRecord'][]): NodeRunEntry[] { + return wire.flatMap((r) => { + const outcome = FINISHED.find((o) => o === r.status); + if (!outcome) return []; + const started = r.started_at ?? r.queued_at; + return [ + { + runId: r.run_id, + fingerprintRunId: null, + jobKey: r.job_key ?? null, + outcome, + startedAt: started, + durationMs: r.started_at && r.ended_at ? Date.parse(r.ended_at) - Date.parse(r.started_at) : null, + rows: r.rows_affected ?? null, + message: r.skipped_because_of ? `Skipped because ${shortName(r.skipped_because_of)} did not succeed` : (r.error_code ?? null) + } + ]; + }); +} + /** - * A listed node with its status and SQL reads and its checks. The server keeps no - * per-node run history, row counts, cost, jobs or migrations, so those stay empty or null and the - * pages hide them. `sql` is null when the person may not see SQL. + * A listed node with its status, SQL, run history, jobs and checks. The server keeps no row + * counts, cost or migrations per node, so those stay empty or null and the pages hide them. `sql` + * is null when the person may not see SQL; `runs` and `jobs` are null when their reads failed. */ -export function nodeDetailOf(nodes: CatalogNode[], node: CatalogNode, status: Wire['NodeStatus'] | null, sql: Wire['NodeSql'] | null, checks: Check[]): CatalogNodeDetail { +export function nodeDetailOf( + nodes: CatalogNode[], + node: CatalogNode, + status: Wire['NodeStatus'] | null, + sql: Wire['NodeSql'] | null, + checks: Check[], + { runs, jobs }: { runs: NodeRunEntry[] | null; jobs: NodeJob[] | null } = { runs: null, jobs: null } +): CatalogNodeDetail { // Checks name their node without its kind, so the attachment tells a source from a model of the same name. const own = checks.filter((c) => c.node === node.key || `${c.attachment}:${c.node}` === node.key); const physical = status?.physical.status === 'observed' ? status.physical.observation.record : null; @@ -87,17 +124,17 @@ export function nodeDetailOf(nodes: CatalogNode[], node: CatalogNode, status: Wi stale: staleOf(status), checks: checkCountsOf(own), downstream: nodes.filter((n) => n.upstream.includes(node.key)).map((n) => n.key), - recentOutcomes: [], + recentOutcomes: (runs ?? []).slice(0, 40).map((r) => r.outcome).reverse(), lastRows: null, rowsDelta: null, - lastDurationMs: null, + lastDurationMs: runs?.find((r) => r.durationMs !== null)?.durationMs ?? null, cost: null, columns: (physical?.columns ?? []).map((c) => ({ name: c.name, type: c.data_type, description: '', audits: [] })), sql: sql?.raw_sql ? { raw: sql.raw_sql, compiled: sql.compiled_sql ?? sql.raw_sql } : null, code: null, builds: [], - jobs: null, - runs: null, + jobs, + runs, checkResults: own, warehouse: null, microbatch: null, diff --git a/ui/src/lib/api/http-client.ts b/ui/src/lib/api/http-client.ts index 952caa2133..7c2b778335 100644 --- a/ui/src/lib/api/http-client.ts +++ b/ui/src/lib/api/http-client.ts @@ -2,7 +2,7 @@ import createFetch from 'openapi-fetch'; import type { components, paths } from '$control/schema.d.ts'; import { ApiError, type ApiClient } from './client'; import { checkDetailOf, toCheck, toCheckEvidence, toCheckSql } from './http-checks'; -import { findNode, nodeDetailOf, toCatalogNodes } from './http-catalog'; +import { findNode, nodeDetailOf, nodeJobsOf, toCatalogNodes, toNodeRuns } from './http-catalog'; import { toSnapshotChanges } from './http-changes'; import { jobDetailOf, toJob, toTick, toTrigger, triggerDetailOf } from './http-jobs'; import { RUN_WINDOW, runDetailOf, runRowPage, runsMatching, runTabCountsOf, toRunNode, toRuns } from './http-runs'; @@ -269,17 +269,21 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt if (!node) return null; const { locationId, target } = parseLocationKey(scope.locationId); const path = { location_id: locationId, target }; - const [status, sql, checks] = await Promise.all([ + const [status, sql, checks, runs, jobs] = await Promise.all([ http.GET('/api/control/locations/{location_id}/targets/{target}/node-status', { params: { path, query: { node_key: node.key } } }), sqlLocations.has(scope.locationId) && !sqlRefused.has(`${scope.locationId}|${node.key}`) ? http.GET('/api/control/locations/{location_id}/targets/{target}/nodes/{node_key}/sql', { params: { path: { ...path, node_key: node.key } } }) : null, - checkResults(scope).catch(() => []) + checkResults(scope).catch(() => []), + node.kind === 'source' + ? null + : http.GET('/api/control/locations/{location_id}/targets/{target}/nodes/{node_key}/runs', { params: { path: { ...path, node_key: node.key }, query: { limit: 100 } } }), + Promise.all([listedJobs(scope), listedTriggers(scope)]).then(([j, t]) => nodeJobsOf(node.key, j, t)).catch(() => null) ]); - if (status.response.status === 401 || sql?.response.status === 401) onUnauthorized(); + if (status.response.status === 401 || sql?.response.status === 401 || runs?.response.status === 401) onUnauthorized(); if (sql?.response.status === 403) sqlRefused.add(`${scope.locationId}|${node.key}`); // Status and SQL have their own permissions; a refusal leaves that part out rather than failing the page. - return nodeDetailOf(nodes, node, status.data ?? null, sql?.data ?? null, checks.map(toCheck)); + return nodeDetailOf(nodes, node, status.data ?? null, sql?.data ?? null, checks.map(toCheck), { runs: runs?.data ? toNodeRuns(runs.data) : null, jobs }); }, listChecks: async (scope: Scope): Promise => (await checkResults(scope)).map(toCheck), getCheck: async (scope: Scope, id: string): Promise => checkDetailOf(await checkResults(scope, id)), diff --git a/ui/src/routes/catalog/+page.svelte b/ui/src/routes/catalog/+page.svelte index afe9457a83..aa9010bdad 100644 --- a/ui/src/routes/catalog/+page.svelte +++ b/ui/src/routes/catalog/+page.svelte @@ -17,7 +17,7 @@ import { createResource } from '$lib/state/resource.svelte'; import { formatAgo, formatStamp } from '$lib/format/time'; import { nodeOutcomeLabel, nodeOutcomeTone } from '$lib/status'; - import { NODE_KINDS, kindLine, nodeIcon, nodeIconKey, nodeKindPlural, shortRunId } from '$lib/build-area/labels'; + import { NODE_KINDS, kindLine, nodeIcon, nodeIconKey, nodeKindPlural, shortName, shortRunId } from '$lib/build-area/labels'; type StatusFilter = 'stale' | 'changed' | 'behind' | 'failed' | 'warn'; @@ -130,7 +130,7 @@
    - {#if linksNodes} e.stopPropagation()}>{n.key}{:else}{n.key}{/if} + {#if linksNodes} e.stopPropagation()}>{shortName(n.key)}{:else}{shortName(n.key)}{/if}
    {n.description}
    diff --git a/ui/src/routes/runs/run-list.svelte b/ui/src/routes/runs/run-list.svelte index d5e1d9e39f..65eb4640a9 100644 --- a/ui/src/routes/runs/run-list.svelte +++ b/ui/src/routes/runs/run-list.svelte @@ -65,8 +65,8 @@ - - + + From b5f41d4d2713b885e161e3d21138e95efd9bd58f Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 17:41:33 +0000 Subject: [PATCH 065/106] feat(orchestration): gzip location snapshot publishes A location now sends its snapshot with Content-Encoding: gzip. The server keeps the 64 MiB wire bound and, after authenticating the location token, streams the body through a 512 MiB decompressed-size bound into the JSON parser. Encodings other than gzip get 415, and a body over either bound gets 413. A server that refuses the encoded body with 400 or 415 gets the snapshot again as plain JSON, so new locations still publish to older servers. --- clients/typescript/src/schema.d.ts | 14 ++ .../src/api/_helpers/resources/mod.rs | 1 + .../api/_helpers/resources/request_json.rs | 125 +++++++++++ .../src/api/_helpers/resources/snapshots.rs | 21 +- .../src/api/constants.rs | 5 + .../sqlbuild-orchestration/src/api/models.rs | 8 + .../snapshots/main/publish_remote_snapshot.rs | 27 ++- .../tests/api/helpers.rs | 50 +++++ .../tests/api/test_contracts.rs | 2 + .../tests/api/test_publish_encoding.rs | 212 ++++++++++++++++++ .../tests/api/test_types.rs | 14 ++ 11 files changed, 474 insertions(+), 5 deletions(-) create mode 100644 crates/sqlbuild-orchestration/src/api/_helpers/resources/request_json.rs create mode 100644 crates/sqlbuild-orchestration/tests/api/test_publish_encoding.rs diff --git a/clients/typescript/src/schema.d.ts b/clients/typescript/src/schema.d.ts index 2aebecfe52..394b3a0100 100644 --- a/clients/typescript/src/schema.d.ts +++ b/clients/typescript/src/schema.d.ts @@ -4229,6 +4229,20 @@ export interface operations { }; content?: never; }; + /** @description Body over its wire or decompressed bound */ + 413: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Content encoding other than gzip */ + 415: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; }; metadata: { diff --git a/crates/sqlbuild-orchestration/src/api/_helpers/resources/mod.rs b/crates/sqlbuild-orchestration/src/api/_helpers/resources/mod.rs index a4a82ccea6..5b8f666c47 100644 --- a/crates/sqlbuild-orchestration/src/api/_helpers/resources/mod.rs +++ b/crates/sqlbuild-orchestration/src/api/_helpers/resources/mod.rs @@ -12,6 +12,7 @@ pub(crate) mod person_maintenance; pub(crate) mod person_node_sql; pub(crate) mod person_operations; pub(crate) mod person_reads; +pub(crate) mod request_json; pub(crate) mod router; pub(crate) mod run_reads; pub(crate) mod runs; diff --git a/crates/sqlbuild-orchestration/src/api/_helpers/resources/request_json.rs b/crates/sqlbuild-orchestration/src/api/_helpers/resources/request_json.rs new file mode 100644 index 0000000000..3956a45d37 --- /dev/null +++ b/crates/sqlbuild-orchestration/src/api/_helpers/resources/request_json.rs @@ -0,0 +1,125 @@ +//! JSON request bodies that may arrive gzip-encoded, decoded within a decompressed-size bound. + +use std::io::{BufReader, Read}; + +use axum::http::{HeaderMap, StatusCode, header}; +use flate2::read::MultiGzDecoder; +use serde::de::DeserializeOwned; + +use crate::api::constants::{GZIP_ENCODING, IDENTITY_ENCODING, JSON_MEDIA_TYPE}; +use crate::api::errors::ApiError; +use crate::api::models::BoundedReader; + +/// `body` as JSON after its declared encoding; gzip streams into the parser and stops past `limit` bytes. +pub(crate) fn request_json( + headers: &HeaderMap, + body: &[u8], + limit: usize, +) -> Result { + if !json_content(headers) { + return Err(rejection( + StatusCode::UNSUPPORTED_MEDIA_TYPE, + "expected an application/json body".into(), + )); + } + match encodings(headers)?.as_slice() { + [] => serde_json::from_slice(body).map_err(|error| parse_rejection(&error)), + [encoding] if encoding == GZIP_ENCODING => { + let mut reader = BoundedReader { + inner: MultiGzDecoder::new(body), + remaining: limit, + exceeded: false, + }; + let parsed: Result = + serde_json::from_reader(BufReader::new(&mut reader)); + match parsed { + Ok(value) => Ok(value), + Err(_) if reader.exceeded => Err(rejection( + StatusCode::PAYLOAD_TOO_LARGE, + format!("decompressed body exceeds {limit} bytes"), + )), + Err(error) if error.is_io() => Err(rejection( + StatusCode::BAD_REQUEST, + "request body is not valid gzip".into(), + )), + Err(error) => Err(parse_rejection(&error)), + } + } + _ => Err(rejection( + StatusCode::UNSUPPORTED_MEDIA_TYPE, + "only gzip content encoding is accepted".into(), + )), + } +} + +impl Read for BoundedReader { + fn read(&mut self, buffer: &mut [u8]) -> std::io::Result { + let window = buffer.len().min(self.remaining.saturating_add(1)); + let read = self.inner.read(&mut buffer[..window])?; + if read > self.remaining { + self.exceeded = true; + return Err(std::io::Error::other("decompressed body exceeds its bound")); + } + self.remaining -= read; + Ok(read) + } +} + +fn json_content(headers: &HeaderMap) -> bool { + let Some(value) = headers.get(header::CONTENT_TYPE) else { + return false; + }; + let Ok(text) = value.to_str() else { + return false; + }; + let essence: String = text + .split(';') + .next() + .unwrap_or_default() + .trim() + .to_ascii_lowercase(); + essence == JSON_MEDIA_TYPE + || (essence.starts_with("application/") && essence.ends_with("+json")) +} + +/// Declared encodings other than identity, in order; an unreadable header is refused. +fn encodings(headers: &HeaderMap) -> Result, ApiError> { + let mut encodings: Vec = Vec::new(); + for value in headers.get_all(header::CONTENT_ENCODING) { + let text = value.to_str().map_err(|_| { + rejection( + StatusCode::UNSUPPORTED_MEDIA_TYPE, + "only gzip content encoding is accepted".into(), + ) + })?; + encodings.extend( + text.split(',') + .map(|encoding| encoding.trim().to_ascii_lowercase()) + .filter(|encoding| !encoding.is_empty() && encoding != IDENTITY_ENCODING), + ); + } + Ok(encodings) +} + +/// The statuses axum's JSON extractor uses: malformed JSON is 400, a wrong shape is 422. +fn parse_rejection(error: &serde_json::Error) -> ApiError { + if error.is_data() { + rejection( + StatusCode::UNPROCESSABLE_ENTITY, + format!("Failed to deserialize the JSON body into the target type: {error}"), + ) + } else { + rejection( + StatusCode::BAD_REQUEST, + format!("Failed to parse the request body as JSON: {error}"), + ) + } +} + +fn rejection(status: StatusCode, message: String) -> ApiError { + ApiError { + status, + message, + decision_id: None, + } +} diff --git a/crates/sqlbuild-orchestration/src/api/_helpers/resources/snapshots.rs b/crates/sqlbuild-orchestration/src/api/_helpers/resources/snapshots.rs index 99afc12f8c..c9cb63ced0 100644 --- a/crates/sqlbuild-orchestration/src/api/_helpers/resources/snapshots.rs +++ b/crates/sqlbuild-orchestration/src/api/_helpers/resources/snapshots.rs @@ -1,12 +1,15 @@ //! A location credential publishes and reads only its own location's snapshots. +use axum::body::Bytes; use axum::extract::{Path, State}; -use axum::http::StatusCode; +use axum::http::{HeaderMap, StatusCode}; use axum::{Extension, Json}; use uuid::Uuid; use crate::api::_helpers::authentication::bearer::require_location; use crate::api::_helpers::resources::docker_image; +use crate::api::_helpers::resources::request_json::request_json; +use crate::api::constants::DECOMPRESSED_BODY_BYTES; use crate::api::errors::ApiError; use crate::api::models::{DevelopmentScope, DockerImageSource}; use crate::credentials::models::TokenIdentity; @@ -22,20 +25,30 @@ use crate::store::models::ControlStore; request_body = SnapshotInput, responses((status = 200, body = SnapshotRecord), (status = 400, body = crate::api::models::ApiErrorBody), (status = 401, description = "Invalid credential"), (status = 403, description = "Location not authorized"), - (status = 409, description = "Stale generation")), security(("bearer" = [])))] + (status = 409, description = "Stale generation"), (status = 413, description = "Body over its wire or decompressed bound"), + (status = 415, description = "Content encoding other than gzip")), security(("bearer" = [])))] pub(crate) async fn publish( State(store): State, (Extension(identity), Extension(images)): ( Extension, Extension>>, ), - (Path((location_id, target)), development): ( + (Path((location_id, target)), development, headers): ( Path<(Uuid, String)>, Option>, + HeaderMap, ), - Json(input): Json, + body: Bytes, ) -> Result, ApiError> { require_location(&identity, location_id)?; + let input: SnapshotInput = + tokio::task::spawn_blocking(move || request_json(&headers, &body, DECOMPRESSED_BODY_BYTES)) + .await + .map_err(|_| ApiError { + status: StatusCode::INTERNAL_SERVER_ERROR, + message: "snapshot decoding stopped".into(), + decision_id: None, + })??; if input.location_id != location_id || input.target != target { return Err(ApiError { status: StatusCode::BAD_REQUEST, diff --git a/crates/sqlbuild-orchestration/src/api/constants.rs b/crates/sqlbuild-orchestration/src/api/constants.rs index 9bd3b7c6f9..4d1103b818 100644 --- a/crates/sqlbuild-orchestration/src/api/constants.rs +++ b/crates/sqlbuild-orchestration/src/api/constants.rs @@ -1,6 +1,11 @@ //! Shared HTTP transport bounds for the server and native clients. pub const BODY_BYTES: usize = 64 * 1024 * 1024; +/// A gzip request body may decompress to at most this many bytes before it is refused. +pub(crate) const DECOMPRESSED_BODY_BYTES: usize = 512 * 1024 * 1024; +pub(crate) const GZIP_ENCODING: &str = "gzip"; +pub(crate) const IDENTITY_ENCODING: &str = "identity"; +pub(crate) const JSON_MEDIA_TYPE: &str = "application/json"; /// Two 1024-byte passwords can each expand sixfold through JSON escaping. pub(crate) const PASSWORD_BODY_BYTES: usize = 16 * 1024; pub(crate) const PASSWORD_COOKIE: &str = "__Host-sqb-session"; diff --git a/crates/sqlbuild-orchestration/src/api/models.rs b/crates/sqlbuild-orchestration/src/api/models.rs index 9571345a46..f16da08e53 100644 --- a/crates/sqlbuild-orchestration/src/api/models.rs +++ b/crates/sqlbuild-orchestration/src/api/models.rs @@ -431,3 +431,11 @@ pub(crate) struct UiAsset { pub(crate) path: &'static str, pub(crate) body: &'static [u8], } + +/// A decompressing reader that fails once it would yield more than `remaining` bytes. +#[derive(Debug)] +pub(crate) struct BoundedReader { + pub(crate) inner: R, + pub(crate) remaining: usize, + pub(crate) exceeded: bool, +} diff --git a/crates/sqlbuild-orchestration/src/snapshots/main/publish_remote_snapshot.rs b/crates/sqlbuild-orchestration/src/snapshots/main/publish_remote_snapshot.rs index 681cc0f61e..0944a898fd 100644 --- a/crates/sqlbuild-orchestration/src/snapshots/main/publish_remote_snapshot.rs +++ b/crates/sqlbuild-orchestration/src/snapshots/main/publish_remote_snapshot.rs @@ -1,10 +1,16 @@ //! A code location sends retained compiled data over HTTP and never opens the control store. +use reqwest::StatusCode; +use reqwest::header::{CONTENT_ENCODING, CONTENT_TYPE}; + +use crate::api::constants::{GZIP_ENCODING, JSON_MEDIA_TYPE}; use crate::api::errors::ClientError; use crate::api::main::client_response::client_response; use crate::api::main::connect_client::connect_client; use crate::snapshots::models::{SnapshotInput, SnapshotRecord}; +use crate::store::main::compress::compress; +/// Sends the snapshot gzip-encoded, and once more as plain JSON when a server refuses the encoding. pub async fn publish_remote_snapshot( server_url: &str, bearer: &str, @@ -19,5 +25,24 @@ pub async fn publish_remote_snapshot( &input.target, "snapshots", ])?; - client_response(connection.client.post(url).json(input)).await + let body = serde_json::to_vec(input) + .map_err(|_| ClientError::Configuration("snapshot is not serializable as JSON"))?; + let gzipped = + compress(&body).map_err(|_| ClientError::Configuration("snapshot could not be gzipped"))?; + let request = |body: Vec| { + connection + .client + .post(url.clone()) + .header(CONTENT_TYPE, JSON_MEDIA_TYPE) + .body(body) + }; + match client_response(request(gzipped).header(CONTENT_ENCODING, GZIP_ENCODING)).await { + Err(ClientError::Rejected(status)) + if status == StatusCode::BAD_REQUEST + || status == StatusCode::UNSUPPORTED_MEDIA_TYPE => + { + client_response(request(body)).await + } + result => result, + } } diff --git a/crates/sqlbuild-orchestration/tests/api/helpers.rs b/crates/sqlbuild-orchestration/tests/api/helpers.rs index daa60882a2..5b29e670a6 100644 --- a/crates/sqlbuild-orchestration/tests/api/helpers.rs +++ b/crates/sqlbuild-orchestration/tests/api/helpers.rs @@ -101,3 +101,53 @@ pub(crate) fn raw_get(address: &str, path: &str) -> (u16, String) { .expect("numeric status"); (status, body.to_owned()) } + +/// Gzip members that decompress to a JSON string field longer than the decompressed bound. +pub(crate) fn gzip_bomb() -> Vec { + let filler = crate::store::main::compress::compress(&vec![b'a'; 1024 * 1024]).expect("gzip"); + let mut body = crate::store::main::compress::compress(br#"{"padding":""#).expect("gzip"); + for _ in 0..=crate::api::constants::DECOMPRESSED_BODY_BYTES / (1024 * 1024) { + body.extend_from_slice(&filler); + } + body +} + +/// Plain JSON one byte longer than the wire bound. +pub(crate) fn oversized_json() -> Vec { + let mut body = br#"{"padding":""#.to_vec(); + body.resize(crate::api::constants::BODY_BYTES - 1, b'a'); + body.extend_from_slice(br#""}"#); + body +} + +pub(crate) fn gzipped_empty_object() -> Vec { + crate::store::main::compress::compress(b"{}").expect("gzip") +} + +pub(crate) fn not_gzip() -> Vec { + b"{}".to_vec() +} + +/// A DuckShop-10k-sized snapshot whose plain JSON is larger than the wire bound. +pub(crate) fn large_snapshot(location_id: uuid::Uuid) -> crate::snapshots::models::SnapshotInput { + let mut input = crate::runs::tests::helpers::snapshot(location_id); + input.nodes = (0..10_282) + .map(|index| { + let sql = format!( + "select {index} as order_id, '{}' as note from orders_{index}", + "x".repeat(3_400) + ); + ( + format!("model:orders_{index}"), + crate::snapshots::models::SnapshotNode { + kind: "model".into(), + definition: serde_json::json!({"materialization": "table"}), + raw_sql: Some(sql.clone()), + compiled_sql: Some(sql), + python_source: None, + }, + ) + }) + .collect(); + input +} diff --git a/crates/sqlbuild-orchestration/tests/api/test_contracts.rs b/crates/sqlbuild-orchestration/tests/api/test_contracts.rs index b59038211b..9265f0c7f9 100644 --- a/crates/sqlbuild-orchestration/tests/api/test_contracts.rs +++ b/crates/sqlbuild-orchestration/tests/api/test_contracts.rs @@ -70,6 +70,8 @@ mod test_person_location_work; mod test_person_sessions; #[path = "test_proxy.rs"] mod test_proxy; +#[path = "test_publish_encoding.rs"] +mod test_publish_encoding; #[path = "test_reads.rs"] mod test_reads; #[path = "test_statistics_recovery.rs"] diff --git a/crates/sqlbuild-orchestration/tests/api/test_publish_encoding.rs b/crates/sqlbuild-orchestration/tests/api/test_publish_encoding.rs new file mode 100644 index 0000000000..4009d6a420 --- /dev/null +++ b/crates/sqlbuild-orchestration/tests/api/test_publish_encoding.rs @@ -0,0 +1,212 @@ +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::Duration; + +use axum::http::HeaderMap; +use axum::routing::post; +use axum::{Json, Router}; +use chrono::Utc; +use reqwest::StatusCode; +use uuid::Uuid; + +use crate::api::constants::BODY_BYTES; +use crate::api::tests::helpers::{ + gzip_bomb, gzipped_empty_object, large_snapshot, not_gzip, oversized_json, start_api, stop_api, +}; +use crate::api::tests::test_types::{OlderServerTestCase, PublishEncodingTestCase}; +use crate::credentials::main::tokens::issue_token::issue_token; +use crate::credentials::models::{AuditContext, CredentialScope}; +use crate::credentials::tests::location_helpers::create_location; +use crate::runs::tests::helpers::snapshot; +use crate::snapshots::main::publish_remote_snapshot::publish_remote_snapshot; +use crate::snapshots::models::{SnapshotInput, SnapshotRecord}; +use crate::store::main::migrate_store::migrate_store; +use crate::store::tests::store_helpers::{open_test_store, run}; +use crate::store::tests::store_test_types::Backend; + +/// Decoding happens before the store is touched, so one backend covers every encoding case. +#[test] +fn given_encoded_snapshot_bodies_when_publishing_then_gzip_is_bounded_after_authentication() { + let test_cases = [ + PublishEncodingTestCase { + description: "an unauthenticated gzip bomb is refused before it is decompressed", + forged_bearer: Some("unknown.credential"), + encoding: "gzip", + body: gzip_bomb, + expected_status: StatusCode::UNAUTHORIZED, + }, + PublishEncodingTestCase { + description: "a gzip body that decompresses past the bound is refused", + forged_bearer: None, + encoding: "gzip", + body: gzip_bomb, + expected_status: StatusCode::PAYLOAD_TOO_LARGE, + }, + PublishEncodingTestCase { + description: "a plain body over the wire bound is refused", + forged_bearer: None, + encoding: "identity", + body: oversized_json, + expected_status: StatusCode::PAYLOAD_TOO_LARGE, + }, + PublishEncodingTestCase { + description: "an encoding other than gzip is refused", + forged_bearer: None, + encoding: "br", + body: gzipped_empty_object, + expected_status: StatusCode::UNSUPPORTED_MEDIA_TYPE, + }, + PublishEncodingTestCase { + description: "a body declared gzip that is not gzip is refused", + forged_bearer: None, + encoding: "gzip", + body: not_gzip, + expected_status: StatusCode::BAD_REQUEST, + }, + ]; + run(async { + let Some(fixture) = open_test_store(Backend::Sqlite).await else { + return; + }; + let store = &fixture.store; + migrate_store(store).await.expect("migrate"); + let location_id = create_location(store, "orders", "test-admin") + .await + .expect("location"); + let audit = AuditContext { + actor: "test-admin".into(), + granting_role: "admin".into(), + request_id: Uuid::now_v7(), + ip_address: None, + }; + let token = issue_token( + store, + &audit, + CredentialScope::Location { location_id }, + Utc::now() + chrono::Duration::hours(1), + ) + .await + .expect("location credential"); + let api = start_api(store).await; + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(120)) + .build() + .expect("HTTP client"); + let path = format!( + "{}/api/locations/{location_id}/targets/development/snapshots", + api.url + ); + for test_case in &test_cases { + let bearer: &str = test_case.forged_bearer.unwrap_or(&token.bearer); + let status = client + .post(&path) + .header("Content-Type", "application/json") + .header("Content-Encoding", test_case.encoding) + .bearer_auth(bearer) + .body((test_case.body)()) + .send() + .await + .expect("a response, not a reset connection") + .status(); + assert_eq!( + status, test_case.expected_status, + "{}", + test_case.description + ); + } + let input = large_snapshot(location_id); + assert!( + serde_json::to_vec(&input).expect("JSON").len() > BODY_BYTES, + "the snapshot only fits the wire bound gzip-encoded" + ); + let published = publish_remote_snapshot(&api.url, &token.bearer, &input) + .await + .expect("publish a 10k-node snapshot"); + assert_eq!(published.generation, 1); + let current: serde_json::Value = client + .get(&path) + .bearer_auth(&token.bearer) + .send() + .await + .expect("current snapshot") + .json() + .await + .expect("snapshot JSON"); + assert_eq!( + current["nodes"].as_object().map(serde_json::Map::len), + Some(10_282) + ); + stop_api(api).await; + }); +} + +#[test] +fn given_a_server_that_refuses_gzip_when_publishing_then_the_snapshot_is_resent_as_plain_json() { + let test_cases = [ + OlderServerTestCase { + description: "a server that predates gzip rejects the encoded body as malformed JSON", + refusal: StatusCode::BAD_REQUEST, + expected_requests: 2, + }, + OlderServerTestCase { + description: "a server that refuses the encoding", + refusal: StatusCode::UNSUPPORTED_MEDIA_TYPE, + expected_requests: 2, + }, + ]; + for test_case in test_cases { + run(async { + let requests = Arc::new(AtomicUsize::new(0)); + let seen = Arc::clone(&requests); + let refusal = test_case.refusal; + let router = Router::new().route( + "/api/locations/{location_id}/targets/{target}/snapshots", + post(move |headers: HeaderMap, body: axum::body::Bytes| { + let seen = Arc::clone(&seen); + async move { + seen.fetch_add(1, Ordering::SeqCst); + headers + .get("content-encoding") + .map_or(Ok(()), |_| Err(refusal))?; + let input: SnapshotInput = + serde_json::from_slice(&body).map_err(|_| StatusCode::BAD_REQUEST)?; + Ok::<_, StatusCode>(Json(SnapshotRecord { + snapshot_id: Uuid::nil(), + location_id: input.location_id, + target: input.target, + generation: input.generation, + manifest_digest: "older-server".into(), + published_at: Utc::now(), + })) + } + }), + ); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("older server socket"); + let url = format!("http://{}", listener.local_addr().expect("address")); + let server = tokio::spawn(async move { axum::serve(listener, router).await }); + let location_id = Uuid::now_v7(); + let published = publish_remote_snapshot(&url, "token.secret", &snapshot(location_id)) + .await + .expect("plain JSON publish to an older server"); + assert_eq!( + published.manifest_digest, "older-server", + "{}", + test_case.description + ); + assert_eq!( + published.location_id, location_id, + "{}", + test_case.description + ); + assert_eq!( + requests.load(Ordering::SeqCst), + test_case.expected_requests, + "{}", + test_case.description + ); + server.abort(); + }); + } +} diff --git a/crates/sqlbuild-orchestration/tests/api/test_types.rs b/crates/sqlbuild-orchestration/tests/api/test_types.rs index 5d3d2b50f3..7f73e0b6c4 100644 --- a/crates/sqlbuild-orchestration/tests/api/test_types.rs +++ b/crates/sqlbuild-orchestration/tests/api/test_types.rs @@ -8,6 +8,20 @@ pub(crate) struct ApiContractTestCase { pub(crate) expected_status: RunStatus, } +pub(crate) struct PublishEncodingTestCase { + pub(crate) description: &'static str, + pub(crate) forged_bearer: Option<&'static str>, + pub(crate) encoding: &'static str, + pub(crate) body: fn() -> Vec, + pub(crate) expected_status: reqwest::StatusCode, +} + +pub(crate) struct OlderServerTestCase { + pub(crate) description: &'static str, + pub(crate) refusal: reqwest::StatusCode, + pub(crate) expected_requests: usize, +} + pub(crate) struct DevelopmentTestCase { pub(crate) description: &'static str, pub(crate) backend: Backend, From e9a8dde26cbcee312518a90cdb5cb16d3f5c6acf Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 18:05:35 +0000 Subject: [PATCH 066/106] feat: show run events, statements, SQL and output to people Person reads serve a run's Recall rows, scoped to its scheduler and claimed worker invocations. Event and statement metadata need view on every node of the target, as the run's node list does, and leave out event payloads, which can carry SQL and error text. Statement SQL also needs view.sql, and captured output with invocation and diagnostic error text also needs view.evidence, on every node the run selected; both are audited like node SQL and check evidence. The run page's Statements, Events and Output tabs read them. The UI asks for SQL and output only where the bootstrap grants them and stops asking for a run after a refusal until reload, so viewers do not repeat audited denials. SQL blocks wrap instead of clipping. --- clients/typescript/src/schema.d.ts | 335 ++++++++++++++++++ .../authentication/password_sessions.rs | 16 + .../api/_helpers/resources/person_reads.rs | 121 ++++++- .../sqlbuild-orchestration/src/api/models.rs | 17 + .../sqlbuild-orchestration/src/runs/models.rs | 80 +++++ .../src/warehouse_history/_helpers/mod.rs | 1 + .../warehouse_history/_helpers/run_recall.rs | 296 ++++++++++++++++ .../src/warehouse_history/main/mod.rs | 4 + .../main/read_person_run_events.rs | 27 ++ .../main/read_person_run_output.rs | 30 ++ .../main/read_person_run_statement_sql.rs | 31 ++ .../main/read_person_run_statements.rs | 27 ++ .../tests/api/test_denials.rs | 22 ++ .../tests/api/test_reads.rs | 171 +++++++++ fensu.toml | 2 +- ui/src/lib/api/http-client.ts | 53 ++- ui/src/lib/api/http-runs.test.ts | 56 ++- ui/src/lib/api/http-runs.ts | 86 ++++- ui/src/lib/components/code/sql-block.svelte | 3 + ui/src/routes/runs/[id]/run-tabs.svelte | 2 +- ui/src/routes/runs/[id]/run-view.svelte | 2 +- ui/src/routes/runs/[id]/state.svelte.ts | 2 +- ui/src/routes/runs/[id]/utils/tabs.ts | 11 +- 23 files changed, 1380 insertions(+), 15 deletions(-) create mode 100644 crates/sqlbuild-orchestration/src/warehouse_history/_helpers/run_recall.rs create mode 100644 crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_run_events.rs create mode 100644 crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_run_output.rs create mode 100644 crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_run_statement_sql.rs create mode 100644 crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_run_statements.rs diff --git a/clients/typescript/src/schema.d.ts b/clients/typescript/src/schema.d.ts index 788cf6753e..ac96cd4421 100644 --- a/clients/typescript/src/schema.d.ts +++ b/clients/typescript/src/schema.d.ts @@ -482,6 +482,22 @@ export interface paths { patch?: never; trace?: never; }; + "/api/control/locations/{location_id}/targets/{target}/runs/{run_id}/events": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["listPersonRunEvents"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/control/locations/{location_id}/targets/{target}/runs/{run_id}/nodes": { parameters: { query?: never; @@ -498,6 +514,54 @@ export interface paths { patch?: never; trace?: never; }; + "/api/control/locations/{location_id}/targets/{target}/runs/{run_id}/output": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["readPersonRunOutput"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/control/locations/{location_id}/targets/{target}/runs/{run_id}/statements": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["listPersonRunStatements"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/control/locations/{location_id}/targets/{target}/runs/{run_id}/statements/sql": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["readPersonRunStatementSql"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/control/locations/{location_id}/targets/{target}/snapshots": { parameters: { query?: never; @@ -1683,8 +1747,39 @@ export interface components { claim_id: string; request: components["schemas"]["RunRequest"]; }; + RunErrorText: { + error_type?: string | null; + invocation_id: string; + message: string; + occurred_at?: string | null; + }; /** @enum {string} */ RunEventKind: "completed" | "success" | "warn" | "partial" | "failed" | "cancelled"; + /** @description Recall event metadata of a run. Payloads are left out: they can carry SQL, request settings and error text. */ + RunEventRecord: { + /** Format: int64 */ + affected_rows?: number | null; + /** Format: double */ + duration_ms?: number | null; + error_code?: string | null; + error_type?: string | null; + event_id: string; + event_type: string; + invocation_id: string; + occurred_at: string; + outcome?: string | null; + producer?: string | null; + resource_attempt_id?: string | null; + resource_id?: string | null; + resource_kind?: string | null; + resource_name?: string | null; + /** Format: int64 */ + row_count?: number | null; + severity?: string | null; + statement_id?: string | null; + /** Format: int64 */ + storage_id: number; + }; /** @description Workers compile locally and compare against this immutable snapshot, not the current deploy. */ RunExecution: { claim: components["schemas"]["RunClaim"]; @@ -1719,6 +1814,21 @@ export interface components { * @enum {string} */ RunOutcome: "success" | "warn" | "partial" | "failed" | "cancelled"; + /** @description Captured output and error text of a run's invocations; both can quote warehouse data. */ + RunOutput: { + /** @description Invocation failures and error diagnostics, oldest first. */ + errors: components["schemas"]["RunErrorText"][]; + /** @description The newest captured lines per invocation, oldest first. */ + lines: components["schemas"]["RunOutputLine"][]; + }; + RunOutputLine: { + invocation_id: string; + occurred_at: string; + /** Format: int64 */ + sequence: number; + stream: string; + text: string; + }; RunRecord: { /** Format: date-time */ cancel_requested_at?: string | null; @@ -1801,6 +1911,36 @@ export interface components { window?: null | components["schemas"]["RunWindow"]; write_set: string[]; }; + /** @description Warehouse statement metadata of a run: kind, timing, counts and outcome, never the SQL text. */ + RunStatementRecord: { + /** Format: int64 */ + affected_rows?: number | null; + /** Format: double */ + duration_ms?: number | null; + ended_at?: string | null; + error_code?: string | null; + error_type?: string | null; + intent?: string | null; + invocation_id: string; + outcome?: string | null; + purpose?: string | null; + query_id?: string | null; + resource_id?: string | null; + resource_kind?: string | null; + resource_name?: string | null; + /** Format: int64 */ + row_count?: number | null; + sql_available: boolean; + sql_unavailable_reason?: string | null; + started_at?: string | null; + statement_id: string; + statement_kind?: string | null; + }; + /** @description One statement's executed SQL; `sql` is null when capture was off or retention dropped it. */ + RunStatementSql: { + sql?: string | null; + statement_id: string; + }; /** @enum {string} */ RunStatus: "queued" | "launched" | "running" | "unresponsive" | "success" | "warn" | "partial" | "failed" | "cancelled"; /** @description Inclusive typed CLI cursor bounds; the native planner derives exclusive execution ends. */ @@ -3354,6 +3494,57 @@ export interface operations { }; }; }; + listPersonRunEvents: { + parameters: { + query?: { + /** @description Return events after this storage cursor */ + after?: number; + /** @description Maximum events; defaults to 1000, capped at 5000 */ + limit?: number; + }; + header?: never; + path: { + location_id: string; + target: string; + run_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Event metadata oldest first, without payloads */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RunEventRecord"][]; + }; + }; + /** @description Session rejected */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + /** @description Run not found for a person who may view the whole target */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; listPersonRunNodes: { parameters: { query?: never; @@ -3399,6 +3590,150 @@ export interface operations { }; }; }; + readPersonRunOutput: { + parameters: { + query?: never; + header?: never; + path: { + location_id: string; + target: string; + run_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RunOutput"]; + }; + }; + /** @description Session rejected */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Needs view.evidence on every node the run selected */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + /** @description Run not found for a person who may view the whole target */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + listPersonRunStatements: { + parameters: { + query?: { + /** @description Maximum statements; defaults to 1000, capped at 5000 */ + limit?: number; + }; + header?: never; + path: { + location_id: string; + target: string; + run_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Statement metadata in start order, without SQL */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RunStatementRecord"][]; + }; + }; + /** @description Session rejected */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + /** @description Run not found for a person who may view the whole target */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + readPersonRunStatementSql: { + parameters: { + query: { + /** @description One statement of this run */ + statement_id: string; + }; + header?: never; + path: { + location_id: string; + target: string; + run_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RunStatementSql"]; + }; + }; + /** @description Session rejected */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Needs view.sql on every node the run selected */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + /** @description Run or statement not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; listPersonSnapshots: { parameters: { query?: { diff --git a/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs b/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs index c59f0a2119..63ac396abf 100644 --- a/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs +++ b/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs @@ -137,6 +137,22 @@ pub(crate) fn person_routes() -> Router { "/api/control/locations/{location_id}/targets/{target}/node-status", get(crate::api::_helpers::resources::person_operations::node_status), ) + .route( + "/api/control/locations/{location_id}/targets/{target}/runs/{run_id}/events", + get(crate::api::_helpers::resources::person_reads::run_events), + ) + .route( + "/api/control/locations/{location_id}/targets/{target}/runs/{run_id}/statements", + get(crate::api::_helpers::resources::person_reads::run_statements), + ) + .route( + "/api/control/locations/{location_id}/targets/{target}/runs/{run_id}/statements/sql", + get(crate::api::_helpers::resources::person_reads::run_statement_sql), + ) + .route( + "/api/control/locations/{location_id}/targets/{target}/runs/{run_id}/output", + get(crate::api::_helpers::resources::person_reads::run_output), + ) .route( "/api/control/locations/{location_id}/targets/{target}/nodes/{node_key}/runs", get(crate::api::_helpers::resources::person_reads::node_runs), diff --git a/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_reads.rs b/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_reads.rs index 2440ab37a1..e7c01050e4 100644 --- a/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_reads.rs +++ b/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_reads.rs @@ -6,19 +6,25 @@ use axum::{Extension, Json}; use uuid::Uuid; use crate::api::errors::ApiError; -use crate::api::models::ReadQuery; +use crate::api::models::{ReadQuery, RunEventsQuery, StatementSqlQuery}; use crate::credentials::models::{AuditContext, AuthenticatedPerson, DefaultRole}; use crate::declarations::main::list_person_jobs::list_person_jobs; use crate::declarations::main::list_person_triggers::list_person_triggers; use crate::declarations::models::{JobRecord, TriggerRecord}; use crate::runs::main::list_person_run_nodes::list_person_run_nodes; -use crate::runs::models::{NodeRunRecord, RunNodeRecord}; +use crate::runs::models::{ + NodeRunRecord, RunEventRecord, RunNodeRecord, RunOutput, RunStatementRecord, RunStatementSql, +}; use crate::scheduler::main::list_person_ticks::list_person_ticks; use crate::scheduler::models::TickRecord; use crate::snapshots::main::list_person_snapshots::list_person_snapshots; use crate::snapshots::models::PublishedSnapshot; use crate::store::models::ControlStore; use crate::warehouse_history::main::read_person_node_runs::read_person_node_runs; +use crate::warehouse_history::main::read_person_run_events::read_person_run_events; +use crate::warehouse_history::main::read_person_run_output::read_person_run_output; +use crate::warehouse_history::main::read_person_run_statement_sql::read_person_run_statement_sql; +use crate::warehouse_history::main::read_person_run_statements::read_person_run_statements; use crate::warehouse_metadata::main::list_person_catalog_nodes::list_person_catalog_nodes; use crate::warehouse_metadata::models::CatalogNodeRecord; @@ -93,6 +99,117 @@ pub(crate) async fn node_runs( )) } +#[utoipa::path(get, path = "/api/control/locations/{location_id}/targets/{target}/runs/{run_id}/events", operation_id = "listPersonRunEvents", + params(("location_id" = Uuid, Path), ("target" = String, Path), ("run_id" = Uuid, Path), + ("after" = Option, Query, description = "Return events after this storage cursor"), + ("limit" = Option, Query, description = "Maximum events; defaults to 1000, capped at 5000")), + responses((status = 200, body = Vec, description = "Event metadata oldest first, without payloads"), + (status = 401, description = "Session rejected"), (status = 403, body = crate::api::models::ApiErrorBody), + (status = 404, description = "Run not found for a person who may view the whole target")), + security(("session" = []), ("cliSession" = [])))] +pub(crate) async fn run_events( + (State(store), Extension(context)): (State, Extension), + Extension(default_role): Extension, + (Path((location_id, target, run_id)), Query(options)): ( + Path<(Uuid, String, Uuid)>, + Query, + ), + Extension(AuthenticatedPerson(principal, _)): Extension, +) -> Result>, ApiError> { + read_person_run_events( + &store, + (&context, principal), + ( + (location_id, &target, run_id), + (options.after.unwrap_or(0), options.limit.unwrap_or(1000)), + ), + default_role, + ) + .await? + .map(Json) + .ok_or_else(|| not_found("run not found")) +} + +#[utoipa::path(get, path = "/api/control/locations/{location_id}/targets/{target}/runs/{run_id}/statements", operation_id = "listPersonRunStatements", + params(("location_id" = Uuid, Path), ("target" = String, Path), ("run_id" = Uuid, Path), + ("limit" = Option, Query, description = "Maximum statements; defaults to 1000, capped at 5000")), + responses((status = 200, body = Vec, description = "Statement metadata in start order, without SQL"), + (status = 401, description = "Session rejected"), (status = 403, body = crate::api::models::ApiErrorBody), + (status = 404, description = "Run not found for a person who may view the whole target")), + security(("session" = []), ("cliSession" = [])))] +pub(crate) async fn run_statements( + (State(store), Extension(context)): (State, Extension), + Extension(default_role): Extension, + (Path((location_id, target, run_id)), Query(options)): ( + Path<(Uuid, String, Uuid)>, + Query, + ), + Extension(AuthenticatedPerson(principal, _)): Extension, +) -> Result>, ApiError> { + read_person_run_statements( + &store, + (&context, principal), + ( + (location_id, &target, run_id), + options.limit.unwrap_or(1000), + ), + default_role, + ) + .await? + .map(Json) + .ok_or_else(|| not_found("run not found")) +} + +#[utoipa::path(get, path = "/api/control/locations/{location_id}/targets/{target}/runs/{run_id}/statements/sql", operation_id = "readPersonRunStatementSql", + params(("location_id" = Uuid, Path), ("target" = String, Path), ("run_id" = Uuid, Path), + ("statement_id" = String, Query, description = "One statement of this run")), + responses((status = 200, body = RunStatementSql), (status = 401, description = "Session rejected"), + (status = 403, body = crate::api::models::ApiErrorBody, description = "Needs view.sql on every node the run selected"), + (status = 404, description = "Run or statement not found")), + security(("session" = []), ("cliSession" = [])))] +pub(crate) async fn run_statement_sql( + (State(store), Extension(context)): (State, Extension), + Extension(default_role): Extension, + (Path((location_id, target, run_id)), Query(options)): ( + Path<(Uuid, String, Uuid)>, + Query, + ), + Extension(AuthenticatedPerson(principal, _)): Extension, +) -> Result, ApiError> { + read_person_run_statement_sql( + &store, + (&context, principal), + ((location_id, &target, run_id), &options.statement_id), + default_role, + ) + .await? + .map(Json) + .ok_or_else(|| not_found("statement not found")) +} + +#[utoipa::path(get, path = "/api/control/locations/{location_id}/targets/{target}/runs/{run_id}/output", operation_id = "readPersonRunOutput", + params(("location_id" = Uuid, Path), ("target" = String, Path), ("run_id" = Uuid, Path)), + responses((status = 200, body = RunOutput), (status = 401, description = "Session rejected"), + (status = 403, body = crate::api::models::ApiErrorBody, description = "Needs view.evidence on every node the run selected"), + (status = 404, description = "Run not found for a person who may view the whole target")), + security(("session" = []), ("cliSession" = [])))] +pub(crate) async fn run_output( + (State(store), Extension(context)): (State, Extension), + Extension(default_role): Extension, + Path((location_id, target, run_id)): Path<(Uuid, String, Uuid)>, + Extension(AuthenticatedPerson(principal, _)): Extension, +) -> Result, ApiError> { + read_person_run_output( + &store, + (&context, principal), + (location_id, &target, run_id), + default_role, + ) + .await? + .map(Json) + .ok_or_else(|| not_found("run not found")) +} + #[utoipa::path(get, path = "/api/control/locations/{location_id}/targets/{target}/jobs", operation_id = "listPersonJobs", params(("location_id" = Uuid, Path), ("target" = String, Path)), responses((status = 200, body = Vec), (status = 401, description = "Session rejected"), diff --git a/crates/sqlbuild-orchestration/src/api/models.rs b/crates/sqlbuild-orchestration/src/api/models.rs index 5a0e02d980..f3b1e61c11 100644 --- a/crates/sqlbuild-orchestration/src/api/models.rs +++ b/crates/sqlbuild-orchestration/src/api/models.rs @@ -23,6 +23,10 @@ use crate::api::_helpers::resources::{ person_reads::run_nodes, person_reads::nodes, person_reads::node_runs, + person_reads::run_events, + person_reads::run_statements, + person_reads::run_statement_sql, + person_reads::run_output, person_reads::jobs, person_reads::triggers, person_reads::ticks, @@ -84,6 +88,19 @@ pub(crate) struct ReadQuery { pub(crate) limit: Option, } +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct RunEventsQuery { + pub(crate) after: Option, + pub(crate) limit: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct StatementSqlQuery { + pub(crate) statement_id: String, +} + #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] pub(crate) struct ReadAuditQuery { diff --git a/crates/sqlbuild-orchestration/src/runs/models.rs b/crates/sqlbuild-orchestration/src/runs/models.rs index bd8a64843d..520b46e5c7 100644 --- a/crates/sqlbuild-orchestration/src/runs/models.rs +++ b/crates/sqlbuild-orchestration/src/runs/models.rs @@ -213,6 +213,86 @@ pub struct RunNodeRecord { pub skipped_because_of: Option, } +/// Recall event metadata of a run. Payloads are left out: they can carry SQL, request settings and error text. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, utoipa::ToSchema)] +pub struct RunEventRecord { + pub storage_id: i64, + pub event_id: String, + pub invocation_id: String, + pub occurred_at: String, + pub event_type: String, + pub severity: Option, + pub producer: Option, + pub resource_id: Option, + pub resource_kind: Option, + pub resource_name: Option, + pub resource_attempt_id: Option, + pub statement_id: Option, + pub error_type: Option, + pub error_code: Option, + pub duration_ms: Option, + pub outcome: Option, + pub row_count: Option, + pub affected_rows: Option, +} + +/// Warehouse statement metadata of a run: kind, timing, counts and outcome, never the SQL text. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, utoipa::ToSchema)] +pub struct RunStatementRecord { + pub invocation_id: String, + pub statement_id: String, + pub resource_id: Option, + pub resource_name: Option, + pub resource_kind: Option, + pub intent: Option, + pub statement_kind: Option, + pub purpose: Option, + pub query_id: Option, + pub started_at: Option, + pub ended_at: Option, + pub outcome: Option, + pub error_type: Option, + pub error_code: Option, + pub duration_ms: Option, + pub row_count: Option, + pub affected_rows: Option, + pub sql_available: bool, + pub sql_unavailable_reason: Option, +} + +/// One statement's executed SQL; `sql` is null when capture was off or retention dropped it. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] +pub struct RunStatementSql { + pub statement_id: String, + pub sql: Option, +} + +/// Captured output and error text of a run's invocations; both can quote warehouse data. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] +pub struct RunOutput { + /// The newest captured lines per invocation, oldest first. + pub lines: Vec, + /// Invocation failures and error diagnostics, oldest first. + pub errors: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] +pub struct RunOutputLine { + pub invocation_id: String, + pub sequence: i64, + pub stream: String, + pub occurred_at: String, + pub text: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] +pub struct RunErrorText { + pub invocation_id: String, + pub occurred_at: Option, + pub error_type: Option, + pub message: String, +} + /// One build attempt of a node by a run in this target; the same metadata as the run's node list. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] pub struct NodeRunRecord { diff --git a/crates/sqlbuild-orchestration/src/warehouse_history/_helpers/mod.rs b/crates/sqlbuild-orchestration/src/warehouse_history/_helpers/mod.rs index 8ca7426d23..e3f0ef7cbf 100644 --- a/crates/sqlbuild-orchestration/src/warehouse_history/_helpers/mod.rs +++ b/crates/sqlbuild-orchestration/src/warehouse_history/_helpers/mod.rs @@ -2,3 +2,4 @@ pub(crate) mod checks; pub(crate) mod coverage; pub(crate) mod maintenance; pub(crate) mod node_runs; +pub(crate) mod run_recall; diff --git a/crates/sqlbuild-orchestration/src/warehouse_history/_helpers/run_recall.rs b/crates/sqlbuild-orchestration/src/warehouse_history/_helpers/run_recall.rs new file mode 100644 index 0000000000..eae24f60d4 --- /dev/null +++ b/crates/sqlbuild-orchestration/src/warehouse_history/_helpers/run_recall.rs @@ -0,0 +1,296 @@ +//! A run's Recall rows: the scheduler invocation and its claimed worker's invocations, with fixed column lists. + +use std::collections::BTreeSet; + +use serde_json::{Value, json}; +use uuid::Uuid; + +use crate::authorization::main::authorize_listing::authorize_listing; +use crate::authorization::main::authorize_person::authorize_person; +use crate::credentials::main::audit::append_audit::append_audit; +use crate::credentials::models::{Action, AuditContext, DefaultRole, PermissionScope}; +use crate::runs::models::{ + RunErrorText, RunEventRecord, RunOutput, RunOutputLine, RunStatementRecord, RunStatementSql, +}; +use crate::store::errors::ControlError; +use crate::store::main::decode_row::decode_row; +use crate::store::main::query::query; +use crate::store::main::query_records::query_records; +use crate::store::main::{begin::begin, commit::commit}; +use crate::store::models::{ControlStore, ControlTransaction, Parameter, TransactionMode}; + +/// The Recall invocations of one run and the nodes it selected. +pub(crate) struct RunRecallScope { + namespaces: Vec, + selection: BTreeSet, +} + +/// The run's transaction and scope, plus the deciding context and granted scope of a sensitive read. +pub(crate) type RunRecallRead = ( + ControlTransaction, + RunRecallScope, + Option<(AuditContext, PermissionScope)>, +); + +/// Listing view first so unknown runs look refused, then the run, then any sensitive action on its selection. +pub(crate) async fn begin_run_recall( + store: &ControlStore, + (context, credentials): (&AuditContext, Uuid), + (location_id, target, run_id): (Uuid, &str, Uuid), + (default_role, sensitive): (DefaultRole, Option), +) -> Result, ControlError> { + let (transaction, context) = authorize_listing( + begin(store, TransactionMode::Write).await?, + (context, credentials), + (location_id, target), + default_role, + ) + .await?; + let (transaction, scope) = run_recall_scope(transaction, (location_id, target, run_id)).await?; + let Some(scope) = scope else { + commit(transaction).await?; + return Ok(None); + }; + let Some(action) = sensitive else { + return Ok(Some((transaction, scope, None))); + }; + let permission = PermissionScope { + action, + location_id: Some(location_id), + target: Some(target.into()), + nodes: scope.selection.clone(), + }; + let (transaction, context) = authorize_person( + transaction, + (&context, credentials), + &permission, + DefaultRole::None, + ) + .await?; + Ok(Some((transaction, scope, Some((context, permission))))) +} + +/// A sensitive run read, authorized for `action` and audited as `audit_action` in the same transaction. +pub(crate) async fn sensitive_run_read( + store: &ControlStore, + credentials: (&AuditContext, Uuid), + (run, default_role, action): ((Uuid, &str, Uuid), DefaultRole, Action), + (audit_action, read, details): ( + &str, + impl AsyncFnOnce( + ControlTransaction, + &RunRecallScope, + ) -> Result<(ControlTransaction, T), ControlError>, + impl FnOnce(&T) -> Value, + ), +) -> Result, ControlError> { + let Some((transaction, scope, Some((context, permission)))) = + begin_run_recall(store, credentials, run, (default_role, Some(action))).await? + else { + return Ok(None); + }; + let (transaction, result) = read(transaction, &scope).await?; + let transaction = append_audit( + transaction, + &context, + (Uuid::now_v7(), audit_action), + ( + json!(permission), + json!(null), + json!({"run_id": run.2, "read": details(&result)}), + ), + ) + .await?; + commit(transaction).await?; + Ok(Some(result)) +} + +/// `None` when the run is not in this location and target. +async fn run_recall_scope( + transaction: ControlTransaction, + (location_id, target, run_id): (Uuid, &str, Uuid), +) -> Result<(ControlTransaction, Option), ControlError> { + let (transaction, runs) = query( + transaction, + "SELECT selection_json FROM sqb_runs WHERE run_id=$1 AND location_id=$2 AND target=$3", + vec![ + Parameter::Id(run_id), + Parameter::Id(location_id), + Parameter::Text(target.into()), + ], + ) + .await?; + let Some(run) = runs.first() else { + return Ok((transaction, None)); + }; + let selection = match &run["selection_json"] { + Value::String(text) => serde_json::from_str(text), + other => serde_json::from_value(other.clone()), + } + .map_err(|error| ControlError::Invalid(error.to_string()))?; + let scheduler = format!("scheduler/{run_id}"); + let (transaction, claims) = query( + transaction, + "SELECT payload FROM recall_events WHERE invocation_id=$1 AND event_type='run_claimed' ORDER BY invocation_sequence DESC LIMIT 1", + vec![Parameter::Text(scheduler.clone())], + ) + .await?; + let mut namespaces = vec![scheduler]; + if let Some(claim_id) = claims + .first() + .and_then(|row| row["payload"]["claim"]["claim_id"].as_str()) + { + namespaces.push(format!("worker/{claim_id}")); + } + Ok(( + transaction, + Some(RunRecallScope { + namespaces, + selection, + }), + )) +} + +pub(crate) async fn run_events_in( + transaction: ControlTransaction, + scope: &RunRecallScope, + (after, limit): (i64, u32), +) -> Result<(ControlTransaction, Vec), ControlError> { + let (filter, mut parameters) = invocation_filter(scope, 1); + let next = parameters.len() + 1; + parameters.extend([ + Parameter::Integer(after), + Parameter::Integer(i64::from(limit.min(5000))), + ]); + query_records(transaction, + &format!("SELECT storage_id, event_id, invocation_id, occurred_at, event_type, severity, producer, resource_id, \ + resource_kind, resource_name, resource_attempt_id, statement_id, error_type, error_code, duration_ms, outcome, \ + row_count, affected_rows FROM events WHERE {filter} AND storage_id>${next} ORDER BY storage_id LIMIT ${}", next + 1), + parameters).await +} + +pub(crate) async fn run_statements_in( + transaction: ControlTransaction, + scope: &RunRecallScope, + limit: u32, +) -> Result<(ControlTransaction, Vec), ControlError> { + let (filter, mut parameters) = invocation_filter(scope, 1); + let next = parameters.len() + 1; + parameters.push(Parameter::Integer(i64::from(limit.min(5000)))); + let (transaction, rows) = query(transaction, + &format!("SELECT invocation_id, statement_id, resource_id, resource_name, resource_kind, intent, statement_kind, \ + purpose, query_id, started_at, ended_at, outcome, error_type, error_code, duration_ms, row_count, affected_rows, \ + sql_available, sql_unavailable_reason FROM statements WHERE {filter} ORDER BY started_at, statement_id LIMIT ${next}"), + parameters).await?; + let records = rows + .into_iter() + .map(|mut row| { + let available = row + .get("sql_available") + .is_some_and(|value| value.as_bool().unwrap_or_else(|| value.as_i64() == Some(1))); + row.insert("sql_available".into(), Value::Bool(available)); + decode_row(row) + }) + .collect::>()?; + Ok((transaction, records)) +} + +pub(crate) async fn statement_sql_in( + transaction: ControlTransaction, + scope: &RunRecallScope, + statement_id: &str, +) -> Result<(ControlTransaction, Option), ControlError> { + let (filter, mut parameters) = invocation_filter(scope, 1); + let next = parameters.len() + 1; + parameters.push(Parameter::Text(statement_id.into())); + let (transaction, statements) = query( + transaction, + &format!("SELECT statement_id FROM statements WHERE {filter} AND statement_id=${next}"), + parameters.clone(), + ) + .await?; + if statements.is_empty() { + return Ok((transaction, None)); + } + let (transaction, rows) = query(transaction, + &format!("SELECT text FROM artifacts WHERE {filter} AND statement_id=${next} AND kind='sql' LIMIT 1"), + parameters).await?; + let sql = rows + .first() + .map(|row| display_text(&row["text"])) + .transpose()?; + Ok(( + transaction, + Some(RunStatementSql { + statement_id: statement_id.into(), + sql: sql.flatten(), + }), + )) +} + +pub(crate) async fn run_output_in( + transaction: ControlTransaction, + scope: &RunRecallScope, +) -> Result<(ControlTransaction, RunOutput), ControlError> { + let (filter, parameters) = invocation_filter(scope, 1); + let (transaction, rows) = query(transaction, + &format!("SELECT invocation_id, sequence, stream, occurred_at, text FROM output_tail WHERE {filter} \ + ORDER BY occurred_at, invocation_id, sequence"), + parameters.clone()).await?; + let lines = rows + .into_iter() + .map(|mut row| { + let text = display_text(&row["text"])?.unwrap_or_default(); + row.insert("text".into(), Value::String(text)); + decode_row::(row) + }) + .collect::>()?; + let (transaction, failures) = query(transaction, + &format!("SELECT invocation_id, ended_at AS occurred_at, error_type, error_message AS message FROM invocations \ + WHERE {filter} AND error_message IS NOT NULL"), + parameters.clone()).await?; + let (transaction, diagnostics) = query( + transaction, + &format!( + "SELECT invocation_id, occurred_at, logger AS error_type, message FROM diagnostics \ + WHERE {filter} AND severity IN ('error','critical') AND message IS NOT NULL" + ), + parameters, + ) + .await?; + let mut errors: Vec = failures + .into_iter() + .chain(diagnostics) + .map(decode_row) + .collect::>()?; + errors.sort_by(|left, right| left.occurred_at.cmp(&right.occurred_at)); + Ok((transaction, RunOutput { lines, errors })) +} + +/// Exact scheduler and worker invocations, plus the worker's nested invocations. +fn invocation_filter(scope: &RunRecallScope, first: usize) -> (String, Vec) { + let mut clauses: Vec = Vec::new(); + let mut parameters: Vec = Vec::new(); + for namespace in &scope.namespaces { + clauses.push(format!("invocation_id=${}", first + parameters.len())); + parameters.push(Parameter::Text(namespace.clone())); + if namespace.starts_with("worker/") { + clauses.push(format!("invocation_id LIKE ${}", first + parameters.len())); + parameters.push(Parameter::Text(format!("{namespace}/%"))); + } + } + (format!("({})", clauses.join(" OR ")), parameters) +} + +/// Captured bytes decode lossily for display; text columns pass through. +fn display_text(value: &Value) -> Result, ControlError> { + match value { + Value::Null => Ok(None), + Value::String(text) => Ok(Some(text.clone())), + other => { + let bytes: Vec = serde_json::from_value(other.clone()) + .map_err(|error| ControlError::Invalid(error.to_string()))?; + Ok(Some(String::from_utf8_lossy(&bytes).into_owned())) + } + } +} diff --git a/crates/sqlbuild-orchestration/src/warehouse_history/main/mod.rs b/crates/sqlbuild-orchestration/src/warehouse_history/main/mod.rs index d29b0b83b3..b87929e1b4 100644 --- a/crates/sqlbuild-orchestration/src/warehouse_history/main/mod.rs +++ b/crates/sqlbuild-orchestration/src/warehouse_history/main/mod.rs @@ -5,3 +5,7 @@ pub mod read_person_checks; pub mod read_person_maintenance; pub mod read_person_microbatch_coverage; pub mod read_person_node_runs; +pub mod read_person_run_events; +pub mod read_person_run_output; +pub mod read_person_run_statement_sql; +pub mod read_person_run_statements; diff --git a/crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_run_events.rs b/crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_run_events.rs new file mode 100644 index 0000000000..bc759e1e14 --- /dev/null +++ b/crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_run_events.rs @@ -0,0 +1,27 @@ +//! A run's event metadata needs view on every current node, as its node list does; payloads are never read. + +use uuid::Uuid; + +use crate::credentials::models::{AuditContext, DefaultRole}; +use crate::runs::models::RunEventRecord; +use crate::store::errors::ControlError; +use crate::store::main::commit::commit; +use crate::store::models::ControlStore; +use crate::warehouse_history::_helpers::run_recall::{begin_run_recall, run_events_in}; + +/// Events after the storage cursor, oldest first; `None` when the run is not in this location and target. +pub async fn read_person_run_events( + store: &ControlStore, + credentials: (&AuditContext, Uuid), + (run, page): ((Uuid, &str, Uuid), (i64, u32)), + default_role: DefaultRole, +) -> Result>, ControlError> { + let Some((transaction, scope, _)) = + begin_run_recall(store, credentials, run, (default_role, None)).await? + else { + return Ok(None); + }; + let (transaction, events) = run_events_in(transaction, &scope, page).await?; + commit(transaction).await?; + Ok(Some(events)) +} diff --git a/crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_run_output.rs b/crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_run_output.rs new file mode 100644 index 0000000000..ec531d7939 --- /dev/null +++ b/crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_run_output.rs @@ -0,0 +1,30 @@ +//! Run output and error text can quote warehouse data, so they need view.evidence on every node the run selected. + +use serde_json::json; +use uuid::Uuid; + +use crate::credentials::models::{Action, AuditContext, DefaultRole}; +use crate::runs::models::RunOutput; +use crate::store::errors::ControlError; +use crate::store::models::ControlStore; +use crate::warehouse_history::_helpers::run_recall::{run_output_in, sensitive_run_read}; + +/// `None` when the run is not in this location and target; each read is audited like check evidence. +pub async fn read_person_run_output( + store: &ControlStore, + credentials: (&AuditContext, Uuid), + run: (Uuid, &str, Uuid), + default_role: DefaultRole, +) -> Result, ControlError> { + sensitive_run_read( + store, + credentials, + (run, default_role, Action::ViewEvidence), + ( + "output.read", + run_output_in, + |output: &RunOutput| json!({"lines": output.lines.len(), "errors": output.errors.len()}), + ), + ) + .await +} diff --git a/crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_run_statement_sql.rs b/crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_run_statement_sql.rs new file mode 100644 index 0000000000..16d35ffee3 --- /dev/null +++ b/crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_run_statement_sql.rs @@ -0,0 +1,31 @@ +//! Statement SQL needs view.sql on every node the run selected, and each read is audited like node SQL. + +use serde_json::json; +use uuid::Uuid; + +use crate::credentials::models::{Action, AuditContext, DefaultRole}; +use crate::runs::models::RunStatementSql; +use crate::store::errors::ControlError; +use crate::store::models::ControlStore; +use crate::warehouse_history::_helpers::run_recall::{sensitive_run_read, statement_sql_in}; + +/// `None` when the run is not here or the statement is not one of its own. +pub async fn read_person_run_statement_sql( + store: &ControlStore, + credentials: (&AuditContext, Uuid), + (run, statement_id): ((Uuid, &str, Uuid), &str), + default_role: DefaultRole, +) -> Result, ControlError> { + Ok(sensitive_run_read( + store, + credentials, + (run, default_role, Action::ViewSql), + ( + "sql.read", + async |transaction, scope| statement_sql_in(transaction, scope, statement_id).await, + |sql: &Option| json!({"statement_id": statement_id, "found": sql.is_some()}), + ), + ) + .await? + .flatten()) +} diff --git a/crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_run_statements.rs b/crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_run_statements.rs new file mode 100644 index 0000000000..4c6b5b6179 --- /dev/null +++ b/crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_run_statements.rs @@ -0,0 +1,27 @@ +//! A run's statement metadata needs view on every current node; the SQL text has its own read. + +use uuid::Uuid; + +use crate::credentials::models::{AuditContext, DefaultRole}; +use crate::runs::models::RunStatementRecord; +use crate::store::errors::ControlError; +use crate::store::main::commit::commit; +use crate::store::models::ControlStore; +use crate::warehouse_history::_helpers::run_recall::{begin_run_recall, run_statements_in}; + +/// `None` when the run is not in this location and target. +pub async fn read_person_run_statements( + store: &ControlStore, + credentials: (&AuditContext, Uuid), + (run, limit): ((Uuid, &str, Uuid), u32), + default_role: DefaultRole, +) -> Result>, ControlError> { + let Some((transaction, scope, _)) = + begin_run_recall(store, credentials, run, (default_role, None)).await? + else { + return Ok(None); + }; + let (transaction, statements) = run_statements_in(transaction, &scope, limit).await?; + commit(transaction).await?; + Ok(Some(statements)) +} diff --git a/crates/sqlbuild-orchestration/tests/api/test_denials.rs b/crates/sqlbuild-orchestration/tests/api/test_denials.rs index 95fc367fe9..c3fe067e9e 100644 --- a/crates/sqlbuild-orchestration/tests/api/test_denials.rs +++ b/crates/sqlbuild-orchestration/tests/api/test_denials.rs @@ -133,6 +133,28 @@ fn given_refused_requests_when_people_send_them_then_no_node_is_named_and_unknow ) }) .collect(); + for part in [ + "events", + "statements", + "statements/sql?statement_id=s", + "output", + ] { + cases.push(( + format!("run {part}"), + "view", + vec![ + ( + read(inventory, &format!("runs/{}/{part}", queued.run_id)), + None, + ), + (read(inventory, &format!("runs/{unknown_run}/{part}")), None), + ( + read(roles.location_id, &format!("runs/{unknown_run}/{part}")), + None, + ), + ], + )); + } cases.extend([ ( "run nodes".to_owned(), diff --git a/crates/sqlbuild-orchestration/tests/api/test_reads.rs b/crates/sqlbuild-orchestration/tests/api/test_reads.rs index 52ed1644f1..8753983b69 100644 --- a/crates/sqlbuild-orchestration/tests/api/test_reads.rs +++ b/crates/sqlbuild-orchestration/tests/api/test_reads.rs @@ -5,6 +5,11 @@ use crate::authorization::main::bootstrap_user::bootstrap_user; use crate::credentials::main::identities::create_principal::create_principal; use crate::scheduler::tests::contracts::ticks::helpers::calendar_text; use crate::snapshots::main::list_person_snapshots::list_person_snapshots; +use crate::store::errors::ControlError; +use crate::warehouse_history::main::read_person_run_events::read_person_run_events; +use crate::warehouse_history::main::read_person_run_output::read_person_run_output; +use crate::warehouse_history::main::read_person_run_statement_sql::read_person_run_statement_sql; +use crate::warehouse_history::main::read_person_run_statements::read_person_run_statements; use std::collections::BTreeMap; use std::time::Duration; @@ -396,6 +401,172 @@ fn given_published_locations_and_scheduled_work_when_read_through_http_then_reso {"change_kind": "added", "subject_key": "role:"}]), "a viewer sees that a role changed, not which" ); + let run = (location_id, "development", claim.request.run_id); + let execution = format!("worker/{}/execution", claim.claim_id); + let run_events = read_person_run_events( + store, + (&audit, viewer), + (run, (0, 1000)), + DefaultRole::None, + ) + .await + .expect("viewer events") + .expect("claimed run"); + assert!( + run_events + .iter() + .any(|event| event.event_type == "run_claimed"), + "scheduler invocation" + ); + assert!( + run_events + .iter() + .any(|event| event.invocation_id == execution + && event.duration_ms == Some(12.75)), + "nested worker invocation" + ); + assert!( + run_events + .iter() + .all(|event| !event.invocation_id.contains("-other")), + "a neighboring claim prefix is not this run" + ); + assert!( + run_events + .windows(2) + .all(|pair| pair[0].storage_id < pair[1].storage_id) + ); + let page = read_person_run_events( + store, + (&audit, viewer), + (run, (run_events[0].storage_id, 1)), + DefaultRole::None, + ) + .await + .expect("event page") + .expect("claimed run"); + assert_eq!( + page.iter() + .map(|event| event.storage_id) + .collect::>(), + [run_events[1].storage_id], + "the cursor resumes after the last event" + ); + let mut statements: Vec<(String, Option)> = + read_person_run_statements(store, (&audit, viewer), (run, 100), DefaultRole::None) + .await + .expect("viewer statements") + .expect("claimed run") + .into_iter() + .map(|statement| (statement.invocation_id, statement.duration_ms)) + .collect(); + statements.sort_by(|left, right| left.0.cmp(&right.0)); + assert_eq!( + statements, + [ + ( + format!("worker/{}/another-invocation", claim.claim_id), + Some(13.25) + ), + (execution.clone(), Some(12.75)), + ] + ); + let statement_id = read_person_run_statements( + store, + (&audit, administrator), + (run, 100), + DefaultRole::None, + ) + .await + .expect("statements") + .expect("claimed run") + .into_iter() + .find(|statement| statement.invocation_id == execution) + .expect("execution statement") + .statement_id; + for refused in [ + read_person_run_statement_sql( + store, + (&audit, viewer), + (run, &statement_id), + DefaultRole::None, + ) + .await + .map(|_| ()), + read_person_run_output(store, (&audit, viewer), run, DefaultRole::None) + .await + .map(|_| ()), + ] { + assert!( + matches!(refused, Err(ControlError::Denied { .. })), + "SQL and output need view.sql and view.evidence, which a viewer lacks" + ); + } + let sql = read_person_run_statement_sql( + store, + (&audit, administrator), + (run, &statement_id), + DefaultRole::None, + ) + .await + .expect("statement SQL") + .expect("the run's own statement"); + assert_eq!( + (sql.statement_id.as_str(), sql.sql), + (statement_id.as_str(), None) + ); + let output = + read_person_run_output(store, (&audit, administrator), run, DefaultRole::None) + .await + .expect("run output") + .expect("claimed run"); + let own = format!("worker/{}", claim.claim_id); + assert!( + output.lines.iter().all(|line| line.invocation_id == own + || line.invocation_id.starts_with(&format!("{own}/"))), + "only this run's worker invocations" + ); + assert_eq!( + output + .lines + .iter() + .filter(|line| (line.stream.as_str(), line.text.as_str()) + == ("stdout", "orders worker output")) + .count(), + 1, + "the neighboring claim prefix's identical line is left out" + ); + let elsewhere = (location_id, "development", foreign_run.run_id); + assert!( + read_person_run_events( + store, + (&audit, administrator), + (elsewhere, (0, 10)), + DefaultRole::None + ) + .await + .expect("foreign run") + .is_none() + && read_person_run_output( + store, + (&audit, administrator), + elsewhere, + DefaultRole::None + ) + .await + .expect("foreign run output") + .is_none() + && read_person_run_statement_sql( + store, + (&audit, administrator), + ((location_id, "development", manual_ids[0]), &statement_id), + DefaultRole::None, + ) + .await + .expect("another run's statement") + .is_none(), + "another location's run and another run's statement are not found" + ); assert!(!history[1].changes.is_empty()); assert_eq!( (history[0].git_dirty, history[1].git_dirty), diff --git a/fensu.toml b/fensu.toml index bc1b9c46ca..911a92f4a1 100644 --- a/fensu.toml +++ b/fensu.toml @@ -1381,7 +1381,7 @@ paths = ["crates/sqlbuild-orchestration/src/api/_helpers/resources/administrator reason = "Each function is one utoipa-annotated axum route: OpenAPI needs one annotated handler per operation, and each body only checks the caller's scope and calls one store operation. Scope checks and store work live in their shared owners." [[dupes.allowlist]] -paths = ["crates/sqlbuild-orchestration/src/authorization/main/read_security_audit.rs", "crates/sqlbuild-orchestration/src/credentials/_helpers/active_session.rs", "crates/sqlbuild-orchestration/src/declarations/_helpers/reads.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_person_jobs.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_person_triggers.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_triggers.rs", "crates/sqlbuild-orchestration/src/platform/main/read_location_work.rs", "crates/sqlbuild-orchestration/src/runs/main/list_person_runs.rs", "crates/sqlbuild-orchestration/src/runs/main/list_person_run_nodes.rs", "crates/sqlbuild-orchestration/src/runs/main/list_run_nodes.rs", "crates/sqlbuild-orchestration/src/runs/main/list_runs.rs", "crates/sqlbuild-orchestration/src/runs/_helpers/lifecycle/reads.rs", "crates/sqlbuild-orchestration/src/warehouse_history/_helpers/checks.rs", "crates/sqlbuild-orchestration/src/scheduler/main/list_person_ticks.rs", "crates/sqlbuild-orchestration/src/store/main/read_records.rs", "crates/sqlbuild-orchestration/src/platform/main/location_work.rs", "crates/sqlbuild-orchestration/src/scheduler/main/list_ticks.rs", "crates/sqlbuild-orchestration/src/sensors/_helpers/attempts.rs", "crates/sqlbuild-orchestration/src/sensors/_helpers/facts.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_current_record.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_snapshot_documents.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_snapshot_nodes.rs", "crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_catalog.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_check.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_checks.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_maintenance.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_microbatch_coverage.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_node_runs.rs", "crates/sqlbuild-orchestration/src/warehouse_history/_helpers/node_runs.rs", "crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_statistics.rs"] +paths = ["crates/sqlbuild-orchestration/src/authorization/main/read_security_audit.rs", "crates/sqlbuild-orchestration/src/credentials/_helpers/active_session.rs", "crates/sqlbuild-orchestration/src/declarations/_helpers/reads.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_person_jobs.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_person_triggers.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_triggers.rs", "crates/sqlbuild-orchestration/src/platform/main/read_location_work.rs", "crates/sqlbuild-orchestration/src/runs/main/list_person_runs.rs", "crates/sqlbuild-orchestration/src/runs/main/list_person_run_nodes.rs", "crates/sqlbuild-orchestration/src/runs/main/list_run_nodes.rs", "crates/sqlbuild-orchestration/src/runs/main/list_runs.rs", "crates/sqlbuild-orchestration/src/runs/_helpers/lifecycle/reads.rs", "crates/sqlbuild-orchestration/src/warehouse_history/_helpers/checks.rs", "crates/sqlbuild-orchestration/src/scheduler/main/list_person_ticks.rs", "crates/sqlbuild-orchestration/src/store/main/read_records.rs", "crates/sqlbuild-orchestration/src/platform/main/location_work.rs", "crates/sqlbuild-orchestration/src/scheduler/main/list_ticks.rs", "crates/sqlbuild-orchestration/src/sensors/_helpers/attempts.rs", "crates/sqlbuild-orchestration/src/sensors/_helpers/facts.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_current_record.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_snapshot_documents.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_snapshot_nodes.rs", "crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_catalog.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_check.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_checks.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_maintenance.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_microbatch_coverage.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_node_runs.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_run_events.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_run_statements.rs", "crates/sqlbuild-orchestration/src/warehouse_history/_helpers/node_runs.rs", "crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_statistics.rs"] reason = "Each is one control-store read with its own SQL and record type. Transactions, person authorization and decoding go through the shared begin_authorized, query_records, read_records and query_bodies helpers; what remains alike is the call shape." [[dupes.allowlist]] diff --git a/ui/src/lib/api/http-client.ts b/ui/src/lib/api/http-client.ts index 7c2b778335..81f0bf0a30 100644 --- a/ui/src/lib/api/http-client.ts +++ b/ui/src/lib/api/http-client.ts @@ -5,11 +5,11 @@ import { checkDetailOf, toCheck, toCheckEvidence, toCheckSql } from './http-chec import { findNode, nodeDetailOf, nodeJobsOf, toCatalogNodes, toNodeRuns } from './http-catalog'; import { toSnapshotChanges } from './http-changes'; import { jobDetailOf, toJob, toTick, toTrigger, triggerDetailOf } from './http-jobs'; -import { RUN_WINDOW, runDetailOf, runRowPage, runsMatching, runTabCountsOf, toRunNode, toRuns } from './http-runs'; +import { RUN_WINDOW, runDetailOf, runRowPage, runsMatching, runTabCountsOf, toRunEvent, toRunNode, toRunOutput, toRuns, toRunStatements } from './http-runs'; import type { ChangeSubscription, ChangedChannel, ChangedSignal, ConnectionState } from './signals'; import type { AuthMode, Bootstrap, Capability, Location, Scope } from './types'; import type { CatalogNode, CatalogNodeDetail, Check, CheckDetail, CheckEvidence, CheckSql, SnapshotChange } from './types/build'; -import type { Job, JobDetail, RerunMode, Run, RunDetail, RunListQuery, RunRowPage, RunRowQuery, RunTabCounts, Trigger, TriggerDetail, TriggerState } from './types/operate'; +import type { Job, JobDetail, RerunMode, Run, RunDetail, RunEventPage, RunListQuery, RunOutputLine, RunRowPage, RunRowQuery, RunStatement, RunTabCounts, StatementSql, Trigger, TriggerDetail, TriggerState } from './types/operate'; type Wire = components['schemas']; @@ -23,6 +23,10 @@ const SUPPORTED: ReadonlySet = new Set([ 'listRuns', 'getRun', 'getRunDetail', + 'listRunEvents', + 'listRunStatements', + 'getStatementSql', + 'listRunOutput', 'cancelRun', 'rerunRun', 'listJobs', @@ -191,6 +195,9 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt // nodes there whose SQL was refused; each refused read is audited, so neither is asked again until reload. let sqlLocations = new Set(); const sqlRefused = new Set(); + // The same for run output, which needs view.evidence on every node the run selected. + let evidenceLocations = new Set(); + const outputRefused = new Set(); const implemented = { supports: (method: keyof ApiClient) => SUPPORTED.has(method), changes: new HttpChangeFeed(baseUrl, (scope) => feedAllowed(baseUrl, scope, onUnauthorized)), @@ -200,6 +207,7 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt if (!data) throw new ApiError(errorText(error, response), response.status); const bootstrap = toBootstrap(data); sqlLocations = new Set(bootstrap.locations.filter((l) => l.capabilities?.includes('view.sql')).map((l) => l.id)); + evidenceLocations = new Set(bootstrap.locations.filter((l) => l.capabilities?.includes('view.evidence')).map((l) => l.id)); return bootstrap; }, listRunRows: async (scope: Scope, query: RunRowQuery = {}): Promise => runRowPage(await listedRuns(scope), scope.locationId, query), @@ -213,6 +221,42 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt const nodes = (await Promise.all(owners.map(async (owner) => (await runNodes(scope, owner.id)).map((n) => toRunNode(n, owner))))).flat(); return runDetailOf(run, wire, listed, nodes); }, + async listRunEvents(scope: Scope, runId: string, query: { after?: number; limit?: number } = {}): Promise { + const limit = query.limit ?? 1000; + const { data, error, response } = await http.GET('/api/control/locations/{location_id}/targets/{target}/runs/{run_id}/events', { + params: { path: runPath(scope, runId), query: { after: query.after, limit } } + }); + if (response.status === 401) onUnauthorized(); + if (!data) throw new ApiError(errorText(error, response), response.status); + return { events: data.flatMap((e) => toRunEvent(e, runId) ?? []), nextCursor: data.length === limit ? (data.at(-1)?.storage_id ?? null) : null }; + }, + async listRunStatements(scope: Scope, runId: string): Promise { + const { data, error, response } = await http.GET('/api/control/locations/{location_id}/targets/{target}/runs/{run_id}/statements', { params: { path: runPath(scope, runId) } }); + if (response.status === 401) onUnauthorized(); + if (!data) throw new ApiError(errorText(error, response), response.status); + return toRunStatements(data, runId); + }, + async getStatementSql(scope: Scope, runId: string, statementId: string): Promise { + const refusal = `${scope.locationId}|${runId}`; + if (!sqlLocations.has(scope.locationId) || sqlRefused.has(refusal)) throw new ApiError('You may not view the SQL of this run', 403); + const { data, error, response } = await http.GET('/api/control/locations/{location_id}/targets/{target}/runs/{run_id}/statements/sql', { + params: { path: runPath(scope, runId), query: { statement_id: statementId } } + }); + if (response.status === 401) onUnauthorized(); + if (response.status === 403) sqlRefused.add(refusal); + if (!data) throw new ApiError(errorText(error, response), response.status); + return { statementId: data.statement_id, sql: data.sql ?? null }; + }, + async listRunOutput(scope: Scope, runId: string): Promise { + const refusal = `${scope.locationId}|${runId}`; + if (!evidenceLocations.has(scope.locationId) || outputRefused.has(refusal)) return []; + const { data, error, response } = await http.GET('/api/control/locations/{location_id}/targets/{target}/runs/{run_id}/output', { params: { path: runPath(scope, runId) } }); + if (response.status === 401) onUnauthorized(); + if (response.status === 403) outputRefused.add(refusal); + if (response.status === 403) return []; + if (!data) throw new ApiError(errorText(error, response), response.status); + return toRunOutput(data); + }, async cancelRun(scope: Scope, runId: string): Promise { const body = { run_id: runId, event_id: crypto.randomUUID(), reason: 'Cancelled from the web UI' }; const { error, response } = await http.POST('/api/control/runs/cancel', { body, params: { header: { 'x-sqb-csrf': csrfToken(document.cookie) } } }); @@ -338,6 +382,11 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt } }; + function runPath(scope: Scope, runId: string) { + const { locationId, target } = parseLocationKey(scope.locationId); + return { location_id: locationId, target, run_id: runId }; + } + async function listedJobs(scope: Scope) { const { locationId, target } = parseLocationKey(scope.locationId); const { data, error, response } = await http.GET('/api/control/locations/{location_id}/targets/{target}/jobs', { params: { path: { location_id: locationId, target } } }); diff --git a/ui/src/lib/api/http-runs.test.ts b/ui/src/lib/api/http-runs.test.ts index 8e4f328486..a3ffb3fa4b 100644 --- a/ui/src/lib/api/http-runs.test.ts +++ b/ui/src/lib/api/http-runs.test.ts @@ -1,8 +1,9 @@ import { describe, expect, it } from 'vitest'; import type { components } from '$control/schema.d.ts'; -import { runDetailOf, runRowPage, runTabCountsOf, toRunNode, toRuns } from './http-runs'; +import { runDetailOf, runRowPage, runTabCountsOf, toRunNode, toRuns, toRunEvent, toRunStatements, toRunOutput } from './http-runs'; type RunRecord = components['schemas']['RunRecord']; +type W = components['schemas']; const LOCATION = '0191f0aa-orders:development'; const PERSON_ID = '0191f0cc-0000-7000-8000-000000000001'; @@ -122,3 +123,56 @@ describe('run detail', () => { expect(nodes[2].skippedBecauseOf).toEqual({ nodeKey: 'model.n1', runId: 'run', jobKey: 'orders_daily' }); }); }); + +describe('run Recall reads', () => { + const inv = 'worker/c1/execution'; + const event = (type: string, extra: Partial = {}): W['RunEventRecord'] => ({ storage_id: 1, event_id: `e-${type}`, invocation_id: inv, occurred_at: '2026-11-08T06:00:00Z', event_type: type, ...extra }); + + it('maps scheduler facts and sqb events, builds messages from metadata only and drops other kinds', () => { + const mapped = [ + event('run_claimed', { invocation_id: 'scheduler/r1' }), + event('statement_completed', { duration_ms: 1500, affected_rows: 1, resource_id: `["${inv}","model:orders"]`, statement_id: 's1' }), + event('resource_attempt_failed', { severity: 'error', error_type: 'ExecutionFailed', error_code: 'R002', resource_id: `["${inv}","model:orders"]` }), + event('operation_started'), + event('location_health', { invocation_id: 'scheduler/r1' }) + ].flatMap((e) => toRunEvent(e, 'r1') ?? []); + expect(mapped.map((e) => [e.type, e.producer, e.level, e.nodeKey, e.statementId, e.message])).toEqual([ + ['claimed', 'scheduler', 'info', null, null, ''], + ['statement_completed', 'sqb', 'info', 'model:orders', 's1', '1.5 s · 1 row'], + ['resource_attempt_failed', 'sqb', 'error', 'model:orders', null, 'ExecutionFailed: R002'] + ]); + }); + + it('orders statements as sent and tells running from finished by the end time', () => { + const statement = (id: string, extra: Partial): W['RunStatementRecord'] => ({ invocation_id: inv, statement_id: id, sql_available: true, ...extra }); + const statements = toRunStatements( + [ + statement('s1', { resource_id: `["${inv}","seed:countries"]`, statement_kind: 'CREATE', ended_at: '2026-11-08T06:00:01Z', affected_rows: 3 }), + statement('s2', { resource_name: 'orders', outcome: 'failed', error_type: 'CatalogException', sql_available: false }), + statement('s3', { purpose: 'model_build', started_at: '2026-11-08T06:00:02Z' }) + ], + 'r1' + ); + expect(statements.map((s) => [s.sequence, s.nodeKey, s.kind, s.status, s.rowsAffected, s.sqlRecorded, s.error])).toEqual([ + [1, 'seed:countries', 'CREATE', 'completed', 3, true, null], + [2, 'orders', 'statement', 'failed', null, false, 'CatalogException'], + [3, '', 'model_build', 'running', null, true, null] + ]); + }); + + it('merges error text into the output as stderr in time order', () => { + const output = toRunOutput({ + lines: [ + { invocation_id: inv, sequence: 2, stream: 'stdout', occurred_at: '2026-11-08T06:00:03Z', text: 'done' }, + { invocation_id: inv, sequence: 1, stream: 'stderr', occurred_at: '2026-11-08T06:00:01Z', text: 'warning' } + ], + errors: [{ invocation_id: inv, occurred_at: '2026-11-08T06:00:02Z', error_type: 'ValueError', message: 'RuntimeError: bad input' }] + }); + expect(output.map((l) => [l.stream, l.text])).toEqual([ + ['stderr', 'warning'], + ['stderr', 'RuntimeError: bad input'], + ['stdout', 'done'] + ]); + }); +}); + diff --git a/ui/src/lib/api/http-runs.ts b/ui/src/lib/api/http-runs.ts index 86fb68311f..33981c2f74 100644 --- a/ui/src/lib/api/http-runs.ts +++ b/ui/src/lib/api/http-runs.ts @@ -1,7 +1,8 @@ import type { components } from '$control/schema.d.ts'; import { isActiveRun } from '$lib/status'; import type { Principal } from './types'; -import type { NodeCounts, Run, RunDetail, RunListQuery, RunNode, RunNodeStatus, RunRow, RunRowPage, RunRowQuery, RunTabCounts } from './types/operate'; +import { RECALL_EVENT_TYPES, SCHEDULER_FACTS } from './types/operate'; +import type { NodeCounts, Run, RunDetail, RunEvent, RunEventLevel, RunListQuery, RunNode, RunNodeStatus, RunOutputLine, RunRow, RunRowPage, RunRowQuery, RunStatement, RunTabCounts } from './types/operate'; type Wire = components['schemas']; @@ -211,3 +212,86 @@ export function runDetailOf(run: Run, wire: Wire['RunRecord'], listed: Run[], no reruns: listed.filter((r) => r.rerunOfRunId === run.id) }; } + +const KNOWN_EVENTS: ReadonlySet = new Set(RECALL_EVENT_TYPES); +/** Scheduler facts are stored as `run_` events in the scheduler invocation. */ +const SCHEDULER_EVENTS: ReadonlySet = new Set(SCHEDULER_FACTS.map((f) => `run_${f}`)); + +/** The node key a Recall resource ID names: `[invocation, key]`, or null for run-level rows. */ +function recallNodeKey(resourceId: string | null | undefined): string | null { + if (!resourceId) return null; + try { + const parsed: unknown = JSON.parse(resourceId); + return Array.isArray(parsed) && typeof parsed[1] === 'string' ? parsed[1] : null; + } catch { + return null; + } +} + +function levelOf(severity: string | null | undefined): RunEventLevel { + if (severity === 'warning' || severity === 'warn') return 'warn'; + if (severity === 'error' || severity === 'critical') return 'error'; + return severity === 'debug' ? 'debug' : 'info'; +} + +function durationText(ms: number | null | undefined): string { + if (ms === null || ms === undefined) return ''; + return ms < 1000 ? `${Math.round(ms)} ms` : `${(ms / 1000).toFixed(1)} s`; +} + +/** + * A run event from its metadata. The server leaves payloads out because they can carry SQL and + * error text, so the message is built from the type, counts, duration and error code only. + * Event kinds the run page does not show (operations, node projections) are dropped. + */ +export function toRunEvent(wire: Wire['RunEventRecord'], runId: string): RunEvent | null { + const scheduler = SCHEDULER_EVENTS.has(wire.event_type); + if (!scheduler && !KNOWN_EVENTS.has(wire.event_type)) return null; + const type = (scheduler ? wire.event_type.slice('run_'.length) : wire.event_type) as RunEvent['type']; + const rows = wire.affected_rows ?? wire.row_count; + const failure = [wire.error_type, wire.error_code].filter(Boolean).join(': '); + const facts = [durationText(wire.duration_ms), rows === null || rows === undefined ? '' : `${rows} ${rows === 1 ? 'row' : 'rows'}`].filter(Boolean).join(' · '); + const message = type === 'requested' ? 'Requested' : type === 'cancel_requested' ? 'Cancel requested' : wire.event_type.endsWith('_failed') ? failure || facts || 'no error code recorded' : type === 'audit_completed' ? (wire.outcome ?? '') : facts; + return { + id: wire.event_id, + storageId: wire.storage_id, + runId, + producer: wire.invocation_id.startsWith('scheduler/') ? 'scheduler' : 'sqb', + type, + occurredAt: wire.occurred_at, + level: levelOf(wire.severity), + invocationId: wire.invocation_id, + nodeKey: recallNodeKey(wire.resource_id), + statementId: wire.statement_id ?? null, + attempt: null, + message + }; +} + +/** Statement metadata in start order; error text is never sent, only its type and code. */ +export function toRunStatements(wire: Wire['RunStatementRecord'][], runId: string): RunStatement[] { + return wire.map((s, i) => ({ + id: s.statement_id, + runId, + invocationId: s.invocation_id, + sequence: i + 1, + nodeKey: recallNodeKey(s.resource_id) ?? s.resource_name ?? '', + kind: s.statement_kind ?? s.purpose ?? 'statement', + status: s.outcome === 'failed' ? 'failed' : s.outcome === 'cancelled' ? 'cancelled' : s.ended_at || s.outcome ? 'completed' : 'running', + startedAt: s.started_at ?? '', + endedAt: s.ended_at ?? null, + rowsAffected: s.affected_rows ?? s.row_count ?? null, + rowsSoFar: null, + queryId: s.query_id ?? null, + sqlRecorded: s.sql_available, + error: [s.error_type, s.error_code].filter(Boolean).join(': ') || null + })); +} + +/** Captured lines and error text in time order; error text reads as stderr of its invocation. */ +export function toRunOutput(wire: Wire['RunOutput']): RunOutputLine[] { + const lines: RunOutputLine[] = wire.lines.map((l) => ({ invocationId: l.invocation_id, stream: l.stream === 'stderr' ? 'stderr' : 'stdout', at: l.occurred_at, text: l.text })); + const errors: RunOutputLine[] = wire.errors.map((e) => ({ invocationId: e.invocation_id, stream: 'stderr', at: e.occurred_at ?? '', text: e.message })); + return [...lines, ...errors].sort((a, b) => a.at.localeCompare(b.at)); +} + diff --git a/ui/src/lib/components/code/sql-block.svelte b/ui/src/lib/components/code/sql-block.svelte index bde0474f48..54a2a1c5e1 100644 --- a/ui/src/lib/components/code/sql-block.svelte +++ b/ui/src/lib/components/code/sql-block.svelte @@ -73,5 +73,8 @@ font-family: var(--font-mono); font-size: 11.5px; line-height: 1.65; + /* Wrap long lines: inside table cells a scrolling block widens its column instead. */ + white-space: pre-wrap; + overflow-wrap: anywhere; } diff --git a/ui/src/routes/runs/[id]/run-tabs.svelte b/ui/src/routes/runs/[id]/run-tabs.svelte index 080bcf569c..9e75e03dd7 100644 --- a/ui/src/routes/runs/[id]/run-tabs.svelte +++ b/ui/src/routes/runs/[id]/run-tabs.svelte @@ -19,7 +19,7 @@ const shownOutput = $derived(filtering ? output.filter((l) => view.selected.some((k) => l.text.includes(k))) : output); const grouped = $derived((view.detail?.children.length ?? 0) > 0); - const TABS = $derived(bottomTabs(app.client)); + const TABS = $derived(bottomTabs(app.client, (c) => app.can(c))); $effect(() => { if (TABS.length && !TABS.some((t) => t.value === view.tab)) view.tab = TABS[0].value; }); diff --git a/ui/src/routes/runs/[id]/run-view.svelte b/ui/src/routes/runs/[id]/run-view.svelte index f4c5cf56dd..9c21f6cc3e 100644 --- a/ui/src/routes/runs/[id]/run-view.svelte +++ b/ui/src/routes/runs/[id]/run-view.svelte @@ -25,7 +25,7 @@ let topView = $state<'timeline' | 'graph'>('timeline'); /** The graph needs dependency edges and the bottom pane needs execution reads; servers without them show neither. */ const hasGraph = $derived((detail?.edges.length ?? 0) > 0); - const hasBottom = $derived(bottomTabs(app.client).length > 0); + const hasBottom = $derived(bottomTabs(app.client, (c) => app.can(c)).length > 0); /** The axis starts at the first node start, not at queue time. */ const origin = $derived.by(() => { diff --git a/ui/src/routes/runs/[id]/state.svelte.ts b/ui/src/routes/runs/[id]/state.svelte.ts index 61957933a1..e7b5b4db85 100644 --- a/ui/src/routes/runs/[id]/state.svelte.ts +++ b/ui/src/routes/runs/[id]/state.svelte.ts @@ -15,7 +15,7 @@ export function createRunDetailState(runId: string) { const topics = [`run:${runId}`, 'runs']; const detailRes = createResource((client, scope) => client.getRunDetail(scope, runId), topics); const eventsRes = createResource((client, scope) => (client.supports('listRunEvents') ? client.listRunEvents(scope, runId, { limit: 5000 }) : Promise.resolve({ events: [], nextCursor: null })), topics); - const outputRes = createResource((client, scope) => (client.supports('listRunOutput') ? client.listRunOutput(scope, runId) : Promise.resolve([])), topics); + const outputRes = createResource((client, scope) => (client.supports('listRunOutput') && app.can('view.evidence') ? client.listRunOutput(scope, runId) : Promise.resolve([])), topics); const statementsRes = createResource((client, scope) => (client.supports('listRunStatements') ? client.listRunStatements(scope, runId) : Promise.resolve([])), topics); const detail = $derived(detailRes.data ?? null); diff --git a/ui/src/routes/runs/[id]/utils/tabs.ts b/ui/src/routes/runs/[id]/utils/tabs.ts index 7cd5014913..d3ec98f497 100644 --- a/ui/src/routes/runs/[id]/utils/tabs.ts +++ b/ui/src/routes/runs/[id]/utils/tabs.ts @@ -1,14 +1,15 @@ // The run page's bottom tabs that the server can load, in display order. -import type { ApiClient } from '$lib/api'; +import type { ApiClient, Capability } from '$lib/api'; import type { BottomTab } from '../types'; -const TABS: { value: BottomTab; label: string; loads: keyof ApiClient }[] = [ +// Output can quote warehouse data, so it shows only where the person may view evidence. +const TABS: { value: BottomTab; label: string; loads: keyof ApiClient; needs?: Capability }[] = [ { value: 'statements', label: 'Statements', loads: 'listRunStatements' }, { value: 'events', label: 'Events', loads: 'listRunEvents' }, - { value: 'output', label: 'Output', loads: 'listRunOutput' } + { value: 'output', label: 'Output', loads: 'listRunOutput', needs: 'view.evidence' } ]; -export function bottomTabs(client: Pick): { value: BottomTab; label: string }[] { - return TABS.filter((t) => client.supports(t.loads)).map(({ value, label }) => ({ value, label })); +export function bottomTabs(client: Pick, can: (capability: Capability) => boolean): { value: BottomTab; label: string }[] { + return TABS.filter((t) => client.supports(t.loads) && (!t.needs || can(t.needs))).map(({ value, label }) => ({ value, label })); } From bff5f8c121695cb8825192c0a8b187c83069da42 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 18:21:07 +0000 Subject: [PATCH 067/106] feat: require run.prioritise for runs above priority 0 A run's priority decides which queued run is claimed first, and any person allowed to run a job or rerun could set any priority. A person's job run or rerun above priority 0 now also needs run.prioritise on the run's nodes. Only Admin grants it among built-in roles, project roles may grant it, and it has no default role. Priority 0 and below need nothing extra, and trusted local launches are unchanged. --- .../administrator_lifecycle.json | 10 +++---- clients/typescript/src/schema.d.ts | 2 +- .../authorization/_helpers/person_grants.rs | 3 ++- .../src/credentials/models.rs | 2 ++ .../src/runs/_helpers/requests/permissions.rs | 10 +++++++ .../tests/api/person_sessions/test_types.rs | 2 ++ .../tests/api/test_person_sessions.rs | 26 ++++++++++++++++++- .../sqlbuild/references/docs/cli/server.md | 2 +- ui/src/lib/api/mock/access.ts | 1 + ui/src/lib/api/types.ts | 1 + ui/src/lib/platform/permission-catalog.ts | 1 + website/src/content/docs/docs/cli/server.mdx | 2 +- 12 files changed, 52 insertions(+), 10 deletions(-) diff --git a/benchmarks/duckshop/edge_cases/orchestration/administrator_lifecycle.json b/benchmarks/duckshop/edge_cases/orchestration/administrator_lifecycle.json index 8116485af0..a9896b74f6 100644 --- a/benchmarks/duckshop/edge_cases/orchestration/administrator_lifecycle.json +++ b/benchmarks/duckshop/edge_cases/orchestration/administrator_lifecycle.json @@ -12,11 +12,11 @@ {"role": "developer", "allowed": true} ], "session_roles": [ - {"role": null, "expected_metadata": 403, "expected_connection": 403, "expected_job": 403, "expected_sql": 403}, - {"role": "viewer", "expected_metadata": 200, "expected_connection": 403, "expected_job": 403, "expected_sql": 403}, - {"role": "operator", "expected_metadata": 200, "expected_connection": 403, "expected_job": 200, "expected_sql": 403}, - {"role": "developer", "expected_metadata": 200, "expected_connection": 200, "expected_job": 200, "expected_sql": 200}, - {"role": "admin", "expected_metadata": 200, "expected_connection": 200, "expected_job": 200, "expected_sql": 200} + {"role": null, "expected_metadata": 403, "expected_connection": 403, "expected_job": 403, "expected_sql": 403, "expected_prioritised": 403}, + {"role": "viewer", "expected_metadata": 200, "expected_connection": 403, "expected_job": 403, "expected_sql": 403, "expected_prioritised": 403}, + {"role": "operator", "expected_metadata": 200, "expected_connection": 403, "expected_job": 200, "expected_sql": 403, "expected_prioritised": 403}, + {"role": "developer", "expected_metadata": 200, "expected_connection": 200, "expected_job": 200, "expected_sql": 200, "expected_prioritised": 403}, + {"role": "admin", "expected_metadata": 200, "expected_connection": 200, "expected_job": 200, "expected_sql": 200, "expected_prioritised": 200} ], "job_settings": [ {"full_refresh": false}, diff --git a/clients/typescript/src/schema.d.ts b/clients/typescript/src/schema.d.ts index 2aebecfe52..a66f3d69c7 100644 --- a/clients/typescript/src/schema.d.ts +++ b/clients/typescript/src/schema.d.ts @@ -954,7 +954,7 @@ export interface components { * @description Closed permission vocabulary; unknown actions fail deserialization. * @enum {string} */ - Action: "view" | "view.sql" | "view.evidence" | "job.run" | "run.cancel" | "run.rerun" | "run.rewrite_history" | "trigger.toggle" | "sensor.reset_cursor" | "run.adhoc" | "connection.test" | "user.manage" | "role.assign" | "audit.view"; + Action: "view" | "view.sql" | "view.evidence" | "job.run" | "run.cancel" | "run.rerun" | "run.rewrite_history" | "run.prioritise" | "trigger.toggle" | "sensor.reset_cursor" | "run.adhoc" | "connection.test" | "user.manage" | "role.assign" | "audit.view"; /** * @description Administrator membership and person eligibility share the final-admin invariant. * @enum {string} diff --git a/crates/sqlbuild-orchestration/src/authorization/_helpers/person_grants.rs b/crates/sqlbuild-orchestration/src/authorization/_helpers/person_grants.rs index a6d05a8f58..59a38db9e4 100644 --- a/crates/sqlbuild-orchestration/src/authorization/_helpers/person_grants.rs +++ b/crates/sqlbuild-orchestration/src/authorization/_helpers/person_grants.rs @@ -12,7 +12,7 @@ use crate::store::main::query::query; use crate::store::models::{ControlTransaction, Parameter}; use crate::store::types::Row; -const ACTIONS: [Action; 14] = [ +const ACTIONS: [Action; 15] = [ Action::View, Action::ViewSql, Action::ViewEvidence, @@ -20,6 +20,7 @@ const ACTIONS: [Action; 14] = [ Action::RunCancel, Action::RunRerun, Action::RunRewriteHistory, + Action::RunPrioritise, Action::TriggerToggle, Action::SensorResetCursor, Action::RunAdhoc, diff --git a/crates/sqlbuild-orchestration/src/credentials/models.rs b/crates/sqlbuild-orchestration/src/credentials/models.rs index fc0a74344e..9f9e44b9df 100644 --- a/crates/sqlbuild-orchestration/src/credentials/models.rs +++ b/crates/sqlbuild-orchestration/src/credentials/models.rs @@ -89,6 +89,8 @@ pub enum Action { RunRerun, #[serde(rename = "run.rewrite_history")] RunRewriteHistory, + #[serde(rename = "run.prioritise")] + RunPrioritise, #[serde(rename = "trigger.toggle")] TriggerToggle, #[serde(rename = "sensor.reset_cursor")] diff --git a/crates/sqlbuild-orchestration/src/runs/_helpers/requests/permissions.rs b/crates/sqlbuild-orchestration/src/runs/_helpers/requests/permissions.rs index 58baf5f024..425e3abd75 100644 --- a/crates/sqlbuild-orchestration/src/runs/_helpers/requests/permissions.rs +++ b/crates/sqlbuild-orchestration/src/runs/_helpers/requests/permissions.rs @@ -68,5 +68,15 @@ pub(crate) async fn authorize_execution( ) .await?; } + if request.priority > 0 { + scope.action = Action::RunPrioritise; + (transaction, context) = authorize_person( + transaction, + (&context, credentials), + &scope, + DefaultRole::None, + ) + .await?; + } Ok((transaction, context)) } diff --git a/crates/sqlbuild-orchestration/tests/api/person_sessions/test_types.rs b/crates/sqlbuild-orchestration/tests/api/person_sessions/test_types.rs index 552889f4b8..4a8912688e 100644 --- a/crates/sqlbuild-orchestration/tests/api/person_sessions/test_types.rs +++ b/crates/sqlbuild-orchestration/tests/api/person_sessions/test_types.rs @@ -16,5 +16,7 @@ pub(crate) struct SessionRoleCase { pub(crate) expected_metadata: u16, pub(crate) expected_connection: u16, pub(crate) expected_job: u16, + /// A job run above priority 0 needs `run.prioritise`, which only Admin grants. + pub(crate) expected_prioritised: u16, pub(crate) expected_sql: u16, } diff --git a/crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs b/crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs index cc253368c7..1ea10bd974 100644 --- a/crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs +++ b/crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs @@ -186,6 +186,7 @@ fn given_browser_session_roles_when_work_is_requested_then_scope_revocation_and_ let mut last_request = Value::Null; let mut listed_person_runs = 0; let mut launched_person_runs = 0; + let mut prioritised_runs: Vec = Vec::new(); let mut listed_seed_runs = 0; for role_case in roles { for role in role_case.role.iter().copied() { @@ -260,7 +261,10 @@ fn given_browser_session_roles_when_work_is_requested_then_scope_revocation_and_ ("parent_run_id", Value::Null), ("trigger_name", Value::Null), ("rerun_of_run_id", Value::Null), - ("priority", json!(0)), + ( + "priority", + json!(i64::from(prioritised_runs.contains(&launched["run_id"]))), + ), ("rewrite_history", json!(false)), ("cancel_requested_at", Value::Null), ] { @@ -421,6 +425,24 @@ fn given_browser_session_roles_when_work_is_requested_then_scope_revocation_and_ role_case.expected_job ); launched_person_runs += usize::from(role_case.expected_job == 200); + let urgent = json!({"run_id":Uuid::now_v7(), "location_id":location, "target":"development", "job_key":"orders", "reason":"Refresh orders first", "priority":1}); + assert_eq!( + request( + &provider.client, + session, + (Method::POST, "/api/control/runs"), + (&urgent, &session.csrf) + ) + .await + .status() + .as_u16(), + role_case.expected_prioritised, + "a priority above 0 needs run.prioritise" + ); + let accepted = usize::from(role_case.expected_prioritised == 200); + launched_person_runs += accepted; + prioritised_runs + .extend(std::iter::repeat_n(urgent["run_id"].clone(), accepted)); for (endpoint, request_body) in [(&path[..], &body), ("/api/control/runs", &job)] { @@ -466,6 +488,8 @@ fn given_browser_session_roles_when_work_is_requested_then_scope_revocation_and_ } expected_work += usize::from(role_case.expected_connection == 200) * sessions.len(); expected_runs += usize::from(role_case.expected_job == 200) * sessions.len(); + expected_runs += + usize::from(role_case.expected_prioritised == 200) * sessions.len(); assert_eq!( queue_counts(store).await, json!({"work":expected_work,"runs":expected_runs,"claims":0}) diff --git a/src/sqlbuild/.agents/skills/sqlbuild/references/docs/cli/server.md b/src/sqlbuild/.agents/skills/sqlbuild/references/docs/cli/server.md index aee81e65cd..2e87b81cc6 100644 --- a/src/sqlbuild/.agents/skills/sqlbuild/references/docs/cli/server.md +++ b/src/sqlbuild/.agents/skills/sqlbuild/references/docs/cli/server.md @@ -164,7 +164,7 @@ permissions = ["view.evidence"] | Selectors | Same grammar as a job's `select`; an unknown tag or name fails publication | | No matching nodes | Publication succeeds with a warning; the grant gives nothing | | Permissions | Any non-administrative action; `user.manage`, `role.assign` and `audit.view` are rejected | -| Runs | `job.run`, `run.adhoc`, `run.rerun` and `run.rewrite_history` need the permission on every node the run writes | +| Runs | `job.run`, `run.adhoc`, `run.rerun` and `run.rewrite_history` need the permission on every node the run writes; a priority above 0 also needs `run.prioritise`, which only Admin grants among built-in roles | | Triggers | `trigger.toggle` and `sensor.reset_cursor` need the permission on every node the trigger's job writes | | Listings | The catalog, statistics and run list need `view` on every node of the current publication; partial coverage is denied, not filtered | | Several roles | Grants add up across a person's roles | diff --git a/ui/src/lib/api/mock/access.ts b/ui/src/lib/api/mock/access.ts index bfd059226a..bdc0f09599 100644 --- a/ui/src/lib/api/mock/access.ts +++ b/ui/src/lib/api/mock/access.ts @@ -113,6 +113,7 @@ const PLAIN: Record = { 'run.cancel': 'cancel runs', 'run.rerun': 'rerun runs', 'run.rewrite_history': 'full refresh or reprocess past data', + 'run.prioritise': 'run ahead of the queue', 'trigger.toggle': 'pause and resume schedules and sensors', 'sensor.reset_cursor': 'reset where a sensor is up to', 'run.adhoc': 'build any selection of nodes', diff --git a/ui/src/lib/api/types.ts b/ui/src/lib/api/types.ts index 2176c599ce..e1bf968858 100644 --- a/ui/src/lib/api/types.ts +++ b/ui/src/lib/api/types.ts @@ -18,6 +18,7 @@ export const CAPABILITIES = [ 'run.cancel', 'run.rerun', 'run.rewrite_history', + 'run.prioritise', 'trigger.toggle', 'sensor.reset_cursor', 'run.adhoc', diff --git a/ui/src/lib/platform/permission-catalog.ts b/ui/src/lib/platform/permission-catalog.ts index 8686dd3bef..8292fcaef6 100644 --- a/ui/src/lib/platform/permission-catalog.ts +++ b/ui/src/lib/platform/permission-catalog.ts @@ -24,6 +24,7 @@ export const PERMISSIONS: Record = { description: 'Rebuild tables from scratch or reprocess earlier periods, replacing data already built.', risk: 'changes_data' }, + 'run.prioritise': { label: 'Put runs ahead of the queue', description: 'Start or rerun a run with a priority above 0, so it is claimed before waiting runs.', risk: 'operate' }, 'trigger.toggle': { label: 'Pause and resume schedules and sensors', description: 'Stop or restart automatic runs.', risk: 'operate' }, 'sensor.reset_cursor': { label: 'Reset where a sensor is up to', description: 'Move a sensor’s position so it looks at events again.', risk: 'operate' }, 'run.adhoc': { label: 'Build any selection of nodes', description: 'Start an ad-hoc run of nodes outside a declared job, writing to the warehouse.', risk: 'changes_data' }, diff --git a/website/src/content/docs/docs/cli/server.mdx b/website/src/content/docs/docs/cli/server.mdx index 37144b9e86..857db8438d 100644 --- a/website/src/content/docs/docs/cli/server.mdx +++ b/website/src/content/docs/docs/cli/server.mdx @@ -156,7 +156,7 @@ permissions = ["view.evidence"] | Selectors | Same grammar as a job's `select`; an unknown tag or name fails publication | | No matching nodes | Publication succeeds with a warning; the grant gives nothing | | Permissions | Any non-administrative action; `user.manage`, `role.assign` and `audit.view` are rejected | -| Runs | `job.run`, `run.adhoc`, `run.rerun` and `run.rewrite_history` need the permission on every node the run writes | +| Runs | `job.run`, `run.adhoc`, `run.rerun` and `run.rewrite_history` need the permission on every node the run writes; a priority above 0 also needs `run.prioritise`, which only Admin grants among built-in roles | | Triggers | `trigger.toggle` and `sensor.reset_cursor` need the permission on every node the trigger's job writes | | Listings | The catalog, statistics and run list need `view` on every node of the current publication; partial coverage is denied, not filtered | | Several roles | Grants add up across a person's roles | From d10bd288fa3bd36568da23570e7e3e0ef725b3d7 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 18:28:04 +0000 Subject: [PATCH 068/106] fix(orchestration): report the server's refusal of a snapshot too large to resend plain A snapshot whose plain JSON exceeds the wire bound can never be accepted uncompressed, so a 400 for the gzip body is returned as is instead of being masked by a second upload that ends in 413. --- .../src/snapshots/main/publish_remote_snapshot.rs | 9 +++++---- .../tests/api/test_publish_encoding.rs | 10 ++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/crates/sqlbuild-orchestration/src/snapshots/main/publish_remote_snapshot.rs b/crates/sqlbuild-orchestration/src/snapshots/main/publish_remote_snapshot.rs index 0944a898fd..f278709c98 100644 --- a/crates/sqlbuild-orchestration/src/snapshots/main/publish_remote_snapshot.rs +++ b/crates/sqlbuild-orchestration/src/snapshots/main/publish_remote_snapshot.rs @@ -3,14 +3,14 @@ use reqwest::StatusCode; use reqwest::header::{CONTENT_ENCODING, CONTENT_TYPE}; -use crate::api::constants::{GZIP_ENCODING, JSON_MEDIA_TYPE}; +use crate::api::constants::{BODY_BYTES, GZIP_ENCODING, JSON_MEDIA_TYPE}; use crate::api::errors::ClientError; use crate::api::main::client_response::client_response; use crate::api::main::connect_client::connect_client; use crate::snapshots::models::{SnapshotInput, SnapshotRecord}; use crate::store::main::compress::compress; -/// Sends the snapshot gzip-encoded, and once more as plain JSON when a server refuses the encoding. +/// Sends the snapshot gzip-encoded, and once more as plain JSON when a server refuses the encoding and the plain body fits the wire bound. pub async fn publish_remote_snapshot( server_url: &str, bearer: &str, @@ -38,8 +38,9 @@ pub async fn publish_remote_snapshot( }; match client_response(request(gzipped).header(CONTENT_ENCODING, GZIP_ENCODING)).await { Err(ClientError::Rejected(status)) - if status == StatusCode::BAD_REQUEST - || status == StatusCode::UNSUPPORTED_MEDIA_TYPE => + if (status == StatusCode::BAD_REQUEST + || status == StatusCode::UNSUPPORTED_MEDIA_TYPE) + && body.len() <= BODY_BYTES => { client_response(request(body)).await } diff --git a/crates/sqlbuild-orchestration/tests/api/test_publish_encoding.rs b/crates/sqlbuild-orchestration/tests/api/test_publish_encoding.rs index 4009d6a420..9b2aefbcfc 100644 --- a/crates/sqlbuild-orchestration/tests/api/test_publish_encoding.rs +++ b/crates/sqlbuild-orchestration/tests/api/test_publish_encoding.rs @@ -10,6 +10,7 @@ use reqwest::StatusCode; use uuid::Uuid; use crate::api::constants::BODY_BYTES; +use crate::api::errors::ClientError; use crate::api::tests::helpers::{ gzip_bomb, gzipped_empty_object, large_snapshot, not_gzip, oversized_json, start_api, stop_api, }; @@ -123,6 +124,15 @@ fn given_encoded_snapshot_bodies_when_publishing_then_gzip_is_bounded_after_auth .await .expect("publish a 10k-node snapshot"); assert_eq!(published.generation, 1); + let invalid = SnapshotInput { + generation: 0, + ..input + }; + let refused = publish_remote_snapshot(&api.url, &token.bearer, &invalid).await; + assert!( + matches!(refused, Err(ClientError::Rejected(StatusCode::BAD_REQUEST))), + "a snapshot too large to resend plain reports the server's own 400, got {refused:?}" + ); let current: serde_json::Value = client .get(&path) .bearer_auth(&token.bearer) From 19a40051ea31d1be321b5c11125fa32d0724cb72 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 18:28:04 +0000 Subject: [PATCH 069/106] fix(orchestration): bound decompressed publish bodies at 320 MiB A parsed snapshot holds about 7.6 times its JSON size on the server, so a body just under 512 MiB peaked at 3.8 GiB RSS. 320 MiB keeps four times the headroom of a 10,000-model project with column lineage. --- crates/sqlbuild-orchestration/src/api/constants.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/sqlbuild-orchestration/src/api/constants.rs b/crates/sqlbuild-orchestration/src/api/constants.rs index 4d1103b818..89bb616723 100644 --- a/crates/sqlbuild-orchestration/src/api/constants.rs +++ b/crates/sqlbuild-orchestration/src/api/constants.rs @@ -1,8 +1,8 @@ //! Shared HTTP transport bounds for the server and native clients. pub const BODY_BYTES: usize = 64 * 1024 * 1024; -/// A gzip request body may decompress to at most this many bytes before it is refused. -pub(crate) const DECOMPRESSED_BODY_BYTES: usize = 512 * 1024 * 1024; +/// A gzip body may decompress to this many bytes; a parsed snapshot holds about 7.6 times its JSON size. +pub(crate) const DECOMPRESSED_BODY_BYTES: usize = 320 * 1024 * 1024; pub(crate) const GZIP_ENCODING: &str = "gzip"; pub(crate) const IDENTITY_ENCODING: &str = "identity"; pub(crate) const JSON_MEDIA_TYPE: &str = "application/json"; From 32abf512608f70f4653e61fcbc8a7a9117b22023 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 18:35:47 +0000 Subject: [PATCH 070/106] fix: keep run cancellation independent of run.prioritise Only queueing a job run or a rerun above priority 0 needs run.prioritise. Cancelling an urgent run keeps needing only run.cancel. --- .../src/runs/_helpers/requests/permissions.rs | 2 +- crates/sqlbuild-orchestration/tests/runs/test_permissions.rs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/sqlbuild-orchestration/src/runs/_helpers/requests/permissions.rs b/crates/sqlbuild-orchestration/src/runs/_helpers/requests/permissions.rs index 425e3abd75..479db66fa7 100644 --- a/crates/sqlbuild-orchestration/src/runs/_helpers/requests/permissions.rs +++ b/crates/sqlbuild-orchestration/src/runs/_helpers/requests/permissions.rs @@ -68,7 +68,7 @@ pub(crate) async fn authorize_execution( ) .await?; } - if request.priority > 0 { + if matches!(action, Action::JobRun | Action::RunRerun) && request.priority > 0 { scope.action = Action::RunPrioritise; (transaction, context) = authorize_person( transaction, diff --git a/crates/sqlbuild-orchestration/tests/runs/test_permissions.rs b/crates/sqlbuild-orchestration/tests/runs/test_permissions.rs index 131c80a45a..6e29e1d3fc 100644 --- a/crates/sqlbuild-orchestration/tests/runs/test_permissions.rs +++ b/crates/sqlbuild-orchestration/tests/runs/test_permissions.rs @@ -209,11 +209,12 @@ fn given_scoped_person_roles_when_jobs_request_sensitive_settings_then_only_admi &JobRunRequest { run_id: source_id, job_key: job_key.into(), + priority: 1, ..request.clone() }, ) .await - .expect("owner creates source with frozen settings"); + .expect("owner creates urgent source with frozen settings"); let cancellation = CancelRunRequest { run_id: source_id, event_id: Uuid::now_v7(), @@ -226,7 +227,7 @@ fn given_scoped_person_roles_when_jobs_request_sensitive_settings_then_only_admi .await .ok(), can_cancel.then_some(expected), - "cancellation permits stopping even sensitive work" + "cancellation permits stopping even sensitive or prioritised work" ); } let source_id = Uuid::now_v7(); From 58abe6f5f6c2b41fbe8f1d6f1d100383a8305f61 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 19:45:46 +0000 Subject: [PATCH 071/106] test: wait for every trigger to reach the latest due time after scheduler fault recovery The scheduler ticks only the latest missed occurrence, so a trigger first evaluated after the next minute begins ticks that minute and skips the earlier one. When the final recovery drain crossed a minute boundary on a slow runner, the fault test counted those triggers as missing their latest tick, and its duplicate probe at that minute was accepted for the same reason. Recovery now cycles until every trigger has ticked at or after the latest due time at settling, bounded by a five-minute deadline. Each trigger's newest tick must reach that time, and the duplicate probe re-inserts a time the trigger actually ticked. The duplicate, off-schedule, run, event and fencing checks are unchanged. --- .../tests/scheduler/ticks/helpers.rs | 53 ++++++++++++++----- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/crates/sqlbuild-orchestration/tests/scheduler/ticks/helpers.rs b/crates/sqlbuild-orchestration/tests/scheduler/ticks/helpers.rs index 9ae555d007..e4ab10290c 100644 --- a/crates/sqlbuild-orchestration/tests/scheduler/ticks/helpers.rs +++ b/crates/sqlbuild-orchestration/tests/scheduler/ticks/helpers.rs @@ -19,6 +19,7 @@ use crate::store::errors::ControlError; use crate::store::main::migrate_store::migrate_store; use crate::store::main::{begin::begin, commit::commit, query::query}; use crate::store::models::{ControlStore, Parameter, TransactionMode}; +use futures_util::{StreamExt, stream}; use serde_json::{Value, json}; use uuid::Uuid; @@ -36,6 +37,8 @@ use crate::store::tests::store_test_types::Backend; /// More than one due-trigger page of requesting triggers keeps every crash point reachable. const FAULT_TRIGGERS: usize = 400; const FAULT_BACKLOG_MINUTES: i64 = 1; +/// Recovery must bring every trigger to the latest due time within this many minutes. +const FAULT_CATCH_UP_MINUTES: i64 = 5; /// Calendar fixtures stay inside the tick-retention horizon: day 0 is midnight three days ago. pub(crate) fn calendar_time(day: i64) -> DateTime { @@ -618,21 +621,28 @@ pub(crate) async fn fault_run( .expect("recovery") .0; } + // Catch-up ticks only the latest missed time, so a trigger first evaluated after the next + // minute begins ticks that minute instead; each trigger must reach the latest due time. let settled = Utc::now(); - for _ in 0..FAULT_TRIGGERS.div_ceil(100) { - survivor = scheduler_cycle(&second, survivor) + let latest = settled + .duration_trunc(TimeDelta::minutes(1)) + .expect("minute"); + let deadline = settled + TimeDelta::minutes(FAULT_CATCH_UP_MINUTES); + let recovering = &second; + stream::unfold(survivor, |survivor| async move { + let survivor = scheduler_cycle(recovering, survivor) .await .expect("final recovery") .0; - } + Some((triggers_behind(recovering, latest).await, survivor)) + }) + .take_while(|behind| std::future::ready(*behind > 0 && Utc::now() < deadline)) + .for_each(|_| std::future::ready(())) + .await; let first = started .duration_trunc(TimeDelta::minutes(1)) .expect("minute") + TimeDelta::minutes(1); - let expected: BTreeSet> = (0..) - .map(|minutes| first + TimeDelta::minutes(minutes)) - .take_while(|time| *time <= settled) - .collect(); let mut ticks: Vec = Vec::new(); for trigger in &trigger_ids { let trigger_ticks = list_ticks(store, *trigger, 1000).await.expect("ticks"); @@ -641,11 +651,8 @@ pub(crate) async fn fault_run( .map(|tick| tick.scheduled_for) .collect(); anomalies.duplicate_ticks += trigger_ticks.len() - times.len(); - anomalies.missing_latest_ticks += usize::from( - expected - .last() - .is_some_and(|latest| !times.contains(latest)), - ); + anomalies.missing_latest_ticks += + usize::from(times.last().is_none_or(|time| *time < latest)); anomalies.off_schedule_ticks += times .iter() .filter(|time| **time < first || time.second() != 0 || time.nanosecond() != 0) @@ -683,13 +690,19 @@ pub(crate) async fn fault_run( let requested = usize::try_from(rows[0]["requested"].as_i64().expect("count")).expect("nonnegative count"); anomalies.request_event_mismatch = requested.abs_diff(requesting.len()); + // A second tick for a time the trigger already ticked must be refused. + let ticked = ticks + .iter() + .find(|tick| tick.trigger_id == trigger_id) + .expect("the first trigger ticked") + .scheduled_for; let transaction = begin(store, TransactionMode::Write) .await .expect("duplicate"); let duplicate = query(transaction, "INSERT INTO sqb_ticks (tick_id, trigger_id, scheduled_for, evaluated_at, epoch, outcome, run_id, covered_by_run_id) \ VALUES ($1,$2,$3,$3,1,'covered',NULL,NULL)", - vec![Parameter::Id(Uuid::now_v7()), Parameter::Id(trigger_id), Parameter::Time(*expected.last().expect("a due time"))]).await; + vec![Parameter::Id(Uuid::now_v7()), Parameter::Id(trigger_id), Parameter::Time(ticked)]).await; anomalies.accepted_duplicate_ticks = usize::from(duplicate.is_ok()); counts.insert("ticks", ticks.len()); close_store(second).await; @@ -697,6 +710,20 @@ pub(crate) async fn fault_run( Some((anomalies, counts, point)) } +/// Count triggers whose last tick precedes `latest`. +async fn triggers_behind(store: &ControlStore, latest: DateTime) -> i64 { + let transaction = begin(store, TransactionMode::Read).await.expect("progress"); + let (transaction, rows) = query( + transaction, + "SELECT COUNT(*) AS behind FROM sqb_trigger_state WHERE last_tick_for IS NULL OR last_tick_for<$1", + vec![Parameter::Time(latest)], + ) + .await + .expect("trigger progress"); + commit(transaction).await.expect("read progress"); + rows[0]["behind"].as_i64().expect("count") +} + /// Run one real scheduler loop until `deadline`, then stop it and return the trigger's ticks. pub(crate) async fn run_scheduler_until( store: &ControlStore, From 6abe6a045a371b301d1549353c80a149f2b5f7fd Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 20:05:45 +0000 Subject: [PATCH 072/106] test: count ticks scheduled after their own evaluation as off schedule Requiring each trigger's newest tick to be at or after the latest due time has no upper bound, so a scheduler that ticked the next occurrence early would pass. Each tick records the clock it was planned with, and no tick may be for a later time. --- .../sqlbuild-orchestration/tests/scheduler/ticks/helpers.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/sqlbuild-orchestration/tests/scheduler/ticks/helpers.rs b/crates/sqlbuild-orchestration/tests/scheduler/ticks/helpers.rs index e4ab10290c..0aba4ffc27 100644 --- a/crates/sqlbuild-orchestration/tests/scheduler/ticks/helpers.rs +++ b/crates/sqlbuild-orchestration/tests/scheduler/ticks/helpers.rs @@ -657,6 +657,11 @@ pub(crate) async fn fault_run( .iter() .filter(|time| **time < first || time.second() != 0 || time.nanosecond() != 0) .count(); + // A tick is never for a time after the clock reading it was planned and recorded with. + anomalies.off_schedule_ticks += trigger_ticks + .iter() + .filter(|tick| tick.scheduled_for > tick.evaluated_at) + .count(); ticks.extend(trigger_ticks); } let runs: BTreeSet = list_runs(store, (location, "development"), 5_000) From e8df6e489c704f1cd86c18d351127bf0e289d5b5 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 21:15:35 +0000 Subject: [PATCH 073/106] ci: run the full Verify matrix only on pull requests labelled full-verify Every pull request push ran the whole matrix: wheels on five platforms, Windows and macOS jobs, DuckDB end-to-end shards, warehouse, compiler golden and timing jobs, for about two hours of the organisation's shared job slots. Pushes now run a fast Linux core: static checks, unit and integration tests, native workspace checks and the UI checks. The rest runs when the pull request is labelled full-verify and is not a draft, and always on merge groups and manual runs. A run that leaves out suites its change needs reports its aggregate as a fast-core check, so the required Verify stays missing and blocks the merge; a full run fails Verify when any selected suite was skipped. Changes confined to the web UI skip the Python and native checks they cannot affect. The aggregate now runs unless the run is cancelled, so a cancelled run no longer queues it for a runner. A label other than full-verify starts a run in its own concurrency group that verifies nothing and reports under another name, so it cannot cancel or satisfy the real run. --- .github/workflows/verify.yml | 137 +++++++++++++++++++++++++++++------ CONTRIBUTING.md | 7 ++ 2 files changed, 123 insertions(+), 21 deletions(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 2dab9d96db..a68baed356 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -2,6 +2,8 @@ name: Verify on: pull_request: + # Labelling a pull request `full-verify` (or removing the label) changes how much it runs. + types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled] merge_group: workflow_dispatch: @@ -9,16 +11,25 @@ permissions: contents: read # Each merge group has its own gh-readonly-queue ref, so queued groups never cancel each other; the -# queue itself discards a group's run when it rebuilds the group. +# queue itself discards a group's run when it rebuilds the group. A push to a pull request cancels +# its older run. Adding or removing any label other than `full-verify` starts a run in a group of its +# own that verifies nothing, so it never cancels the pull request's real run. concurrency: - group: verify-${{ github.workflow }}-${{ github.event.merge_group.head_ref || github.ref }} + group: >- + verify-${{ github.workflow }}-${{ github.event.merge_group.head_ref || github.ref }}${{ + (github.event.action == 'labeled' || github.event.action == 'unlabeled') + && github.event.label.name != 'full-verify' && format('-label-{0}', github.run_id) || '' }} cancel-in-progress: ${{ github.event_name != 'merge_group' }} jobs: changes: name: Classify verification scope + # A label other than `full-verify` changes nothing this workflow verifies. + if: ${{ !((github.event.action == 'labeled' || github.event.action == 'unlabeled') && github.event.label.name != 'full-verify') }} runs-on: ubuntu-24.04 outputs: + full: ${{ steps.depth.outputs.full }} + partial: ${{ steps.depth.outputs.partial }} checks: ${{ steps.scope.outputs.checks }} compiler_differential: ${{ steps.scope.outputs.compiler_differential }} duckdb: ${{ steps.scope.outputs.duckdb }} @@ -138,6 +149,9 @@ jobs: # skill pages generated from the website docs skips the Python checks and test suites. # The website builds in its own workflow. docs_only=true + # A change that only touches the web UI (and documentation) runs the UI checks and the + # wheels that embed its build, but not the Python and native checks it cannot affect. + ui_only=true while IFS= read -r -d '' path; do changed=true case "${path}" in @@ -147,6 +161,13 @@ jobs: *.md) ;; *) docs_only=false ;; esac + case "${path}" in + ui/*|website/*|.github/workflows/website.yml|.github/demos/*) ;; + src/sqlbuild/.agents/skills/sqlbuild/references/docs/*.md) ;; + */*) ui_only=false ;; + *.md) ;; + *) ui_only=false ;; + esac case "${path}" in crates/*|ui/*|Cargo.toml|Cargo.lock|pyproject.toml|.github/workflows/verify.yml|.github/workflows/build-distributions.yml|deploy/Dockerfile|deploy/project/*|deploy/compose*|compose.yml|.dockerignore|Makefile|tests/integration/src/sqlbuild/compiler/compiled_project/publication/test_compose.py|tests/integration/src/sqlbuild/compiler/compiled_project/publication/docker/compose/*|benchmarks/duckshop/edge_cases/orchestration/container_deployment.json) wheels=true @@ -232,7 +253,7 @@ jobs: fi checks=true - if [[ "${docs_only}" == "true" ]]; then + if [[ "${docs_only}" == "true" || "${ui_only}" == "true" ]]; then checks=false fi @@ -256,8 +277,36 @@ jobs: "${recovery}" \ "${ui}" + # Pull requests run the fast Linux core: static checks, unit and integration tests, native + # workspace checks and the UI checks. The rest of the matrix (wheels, Compose, Windows, macOS, + # DuckDB E2E, warehouse, compiler-golden and timing jobs) runs only once a pull request is + # labelled `full-verify` and is not a draft, and always on merge groups and manual runs. A + # run that leaves out suites its change needs reports under another name, so the required + # Verify stays missing and blocks the merge until the full matrix has passed. + - name: Choose the fast core or the full matrix + id: depth + env: + FULL: >- + ${{ github.event_name != 'pull_request' + || (contains(github.event.pull_request.labels.*.name, 'full-verify') + && !github.event.pull_request.draft) }} + HEAVY_SELECTED: >- + ${{ steps.scope.outputs.duckdb == 'true' || steps.scope.outputs.postgres == 'true' || steps.scope.outputs.clickhouse == 'true' || steps.scope.outputs.performance == 'true' || steps.scope.outputs.format_performance == 'true' || steps.scope.outputs.rules_performance == 'true' || steps.scope.outputs.wheels == 'true' || steps.scope.outputs.compiler_differential == 'true' || steps.scope.outputs.recovery == 'true' }} + run: | + partial=false + if [[ "${FULL}" != "true" && "${HEAVY_SELECTED}" == "true" ]]; then + partial=true + echo "Fast core only: label the pull request full-verify to run the full matrix Verify needs" \ + | tee -a "${GITHUB_STEP_SUMMARY}" + fi + { + echo "full=${FULL}" + echo "partial=${partial}" + } >> "${GITHUB_OUTPUT}" + lockfile: name: Lockfile consistency + if: ${{ !((github.event.action == 'labeled' || github.event.action == 'unlabeled') && github.event.label.name != 'full-verify') }} runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 @@ -365,7 +414,7 @@ jobs: e2e_shards: name: DuckDB E2E (${{ matrix.name }}) needs: [changes, lockfile] - if: needs.changes.outputs.duckdb == 'true' + if: needs.changes.outputs.duckdb == 'true' && needs.changes.outputs.full == 'true' runs-on: ubuntu-24.04 timeout-minutes: 45 strategy: @@ -442,7 +491,7 @@ jobs: compiler_differential: name: Compiler golden outputs needs: [changes, lockfile] - if: needs.changes.outputs.compiler_differential == 'true' + if: needs.changes.outputs.compiler_differential == 'true' && needs.changes.outputs.full == 'true' runs-on: ubuntu-24.04 timeout-minutes: 45 steps: @@ -483,7 +532,7 @@ jobs: compiler_differential_python_versions: name: Native compiler on Python ${{ matrix.python }} needs: [changes, lockfile] - if: needs.changes.outputs.compiler_differential == 'true' + if: needs.changes.outputs.compiler_differential == 'true' && needs.changes.outputs.full == 'true' runs-on: ubuntu-24.04 timeout-minutes: 45 strategy: @@ -523,7 +572,7 @@ jobs: native_discovery_windows: name: Native compiler on Windows needs: [changes, lockfile] - if: needs.changes.outputs.compiler_differential == 'true' + if: needs.changes.outputs.compiler_differential == 'true' && needs.changes.outputs.full == 'true' runs-on: windows-2025 timeout-minutes: 45 defaults: @@ -580,7 +629,7 @@ jobs: scheduler_recovery: name: Scheduler recovery (${{ matrix.name }}) needs: [changes, lockfile] - if: needs.changes.outputs.recovery == 'true' + if: needs.changes.outputs.recovery == 'true' && needs.changes.outputs.full == 'true' runs-on: ${{ matrix.runner }} timeout-minutes: 75 strategy: @@ -632,7 +681,7 @@ jobs: format_performance: name: Formatter performance guards needs: [changes, lockfile] - if: needs.changes.outputs.format_performance == 'true' + if: needs.changes.outputs.format_performance == 'true' && needs.changes.outputs.full == 'true' runs-on: ubuntu-24.04 timeout-minutes: 45 steps: @@ -696,7 +745,7 @@ jobs: cold_compile_performance: name: Fresh-process compile (${{ matrix.models }} models) needs: [changes, lockfile] - if: needs.changes.outputs.performance == 'true' + if: needs.changes.outputs.performance == 'true' && needs.changes.outputs.full == 'true' runs-on: ubuntu-24.04 timeout-minutes: 45 strategy: @@ -747,7 +796,7 @@ jobs: dense_compile_performance: name: Dense all-rules compile (${{ matrix.models }} models) needs: [changes, lockfile] - if: needs.changes.outputs.performance == 'true' + if: needs.changes.outputs.performance == 'true' && needs.changes.outputs.full == 'true' runs-on: ubuntu-24.04 # The 3k and 10k cells also run the delta edit-loop guard, about five minutes at 10k. timeout-minutes: 45 @@ -791,7 +840,7 @@ jobs: compile_performance_ratio: name: Same-runner compile ratio (${{ matrix.label || matrix.kind }} 3000 models) needs: [changes, lockfile] - if: needs.changes.outputs.performance == 'true' + if: needs.changes.outputs.performance == 'true' && needs.changes.outputs.full == 'true' runs-on: ubuntu-24.04 timeout-minutes: 45 strategy: @@ -894,7 +943,7 @@ jobs: cache_compile_performance: name: Fresh-process compile cache (${{ matrix.models }} models) needs: [changes, lockfile] - if: needs.changes.outputs.performance == 'true' + if: needs.changes.outputs.performance == 'true' && needs.changes.outputs.full == 'true' runs-on: ubuntu-24.04 timeout-minutes: 45 strategy: @@ -1019,7 +1068,7 @@ jobs: postgres: name: PostgreSQL integration and E2E tests needs: [changes, lockfile] - if: needs.changes.outputs.postgres == 'true' + if: needs.changes.outputs.postgres == 'true' && needs.changes.outputs.full == 'true' runs-on: ubuntu-24.04 timeout-minutes: 45 steps: @@ -1056,7 +1105,7 @@ jobs: clickhouse: name: ClickHouse integration and E2E tests needs: [changes, lockfile] - if: needs.changes.outputs.clickhouse == 'true' + if: needs.changes.outputs.clickhouse == 'true' && needs.changes.outputs.full == 'true' runs-on: ubuntu-24.04 timeout-minutes: 45 steps: @@ -1093,7 +1142,7 @@ jobs: wheels: name: Wheels needs: [changes] - if: needs.changes.outputs.wheels == 'true' + if: needs.changes.outputs.wheels == 'true' && needs.changes.outputs.full == 'true' uses: ./.github/workflows/build-distributions.yml with: sdist: false @@ -1105,7 +1154,7 @@ jobs: compose: name: Packaged Compose lifecycle needs: [changes, lockfile, wheels] - if: needs.changes.outputs.wheels == 'true' + if: needs.changes.outputs.wheels == 'true' && needs.changes.outputs.full == 'true' runs-on: ubuntu-24.04 timeout-minutes: 20 steps: @@ -1156,14 +1205,20 @@ jobs: # release checks in release-please.yml post on the branch's current head, after the lockfile # refresh and every dispatched check. This job reports under another name there, so its quick # metadata-only pass can never satisfy Verify first; a skipped job would count as passing, so - # the job always runs and only its name changes. Until those checks post Verify on the head, - # the pull request waits for an expected check, and every new push to the branch waits again. + # the job runs unless the run is cancelled, and only its name changes. Until those checks post + # Verify on the head, the pull request waits for an expected check, and every new push to the + # branch waits again. The same holds for a fast-core run that leaves out suites its change + # needs, and for a run started by an unrelated label: neither reports as Verify. A cancelled + # run cancels this job instead of queueing it for a runner, and a cancelled Verify blocks. name: >- ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && startsWith(github.event.pull_request.head.ref, 'release-please--branches--')) - && 'Release pull request suites' || 'Verify' }} - if: always() + && 'Release pull request suites' + || ((github.event.action == 'labeled' || github.event.action == 'unlabeled') && github.event.label.name != 'full-verify') && 'Label change (nothing verified)' + || needs.changes.outputs.partial == 'true' && 'Fast core (label full-verify for Verify)' + || 'Verify' }} + if: ${{ !cancelled() }} needs: [ changes, @@ -1215,7 +1270,21 @@ jobs: COMPOSE_RESULT: ${{ needs.compose.result }} UI_SELECTED: ${{ needs.changes.outputs.ui }} UI_RESULT: ${{ needs.ui.result }} + LABEL_ONLY: ${{ (github.event.action == 'labeled' || github.event.action == 'unlabeled') && github.event.label.name != 'full-verify' }} + PARTIAL: ${{ needs.changes.outputs.partial }} + DUCKDB_SELECTED: ${{ needs.changes.outputs.duckdb }} + COMPILER_DIFFERENTIAL_SELECTED: ${{ needs.changes.outputs.compiler_differential }} + RECOVERY_SELECTED: ${{ needs.changes.outputs.recovery }} + FORMAT_PERFORMANCE_SELECTED: ${{ needs.changes.outputs.format_performance }} + PERFORMANCE_SELECTED: ${{ needs.changes.outputs.performance }} + POSTGRES_SELECTED: ${{ needs.changes.outputs.postgres }} + CLICKHOUSE_SELECTED: ${{ needs.changes.outputs.clickhouse }} + WHEELS_SELECTED: ${{ needs.changes.outputs.wheels }} run: | + if [[ "${LABEL_ONLY}" == "true" ]]; then + echo "A label other than full-verify changed; this run verifies nothing" + exit 0 + fi if [[ "${CHANGES_RESULT}" != "success" || "${LOCKFILE_RESULT}" != "success" ]]; then echo "Required classification or lockfile checks did not pass" >&2 exit 1 @@ -1258,4 +1327,30 @@ jobs: echo "Required UI checks did not pass: ${UI_RESULT}" >&2 exit 1 fi + # A full run must have run every suite its change selected; a skip here is never a pass. + if [[ "${PARTIAL}" != "true" ]]; then + for pair in \ + "${DUCKDB_SELECTED}:${E2E_SHARDS_RESULT}" \ + "${COMPILER_DIFFERENTIAL_SELECTED}:${COMPILER_DIFFERENTIAL_RESULT}" \ + "${COMPILER_DIFFERENTIAL_SELECTED}:${COMPILER_DIFFERENTIAL_PYTHON_VERSIONS_RESULT}" \ + "${COMPILER_DIFFERENTIAL_SELECTED}:${NATIVE_DISCOVERY_WINDOWS_RESULT}" \ + "${RECOVERY_SELECTED}:${SCHEDULER_RECOVERY_RESULT}" \ + "${FORMAT_PERFORMANCE_SELECTED}:${FORMAT_PERFORMANCE_RESULT}" \ + "${PERFORMANCE_SELECTED}:${COLD_COMPILE_PERFORMANCE_RESULT}" \ + "${PERFORMANCE_SELECTED}:${DENSE_COMPILE_PERFORMANCE_RESULT}" \ + "${PERFORMANCE_SELECTED}:${COMPILE_PERFORMANCE_RATIO_RESULT}" \ + "${PERFORMANCE_SELECTED}:${CACHE_COMPILE_PERFORMANCE_RESULT}" \ + "${POSTGRES_SELECTED}:${POSTGRES_RESULT}" \ + "${CLICKHOUSE_SELECTED}:${CLICKHOUSE_RESULT}" \ + "${WHEELS_SELECTED}:${WHEELS_RESULT}" \ + "${WHEELS_SELECTED}:${COMPOSE_RESULT}"; do + if [[ "${pair%%:*}" == "true" && "${pair#*:}" != "success" ]]; then + echo "A suite this change needs did not run to success: ${pair#*:}" >&2 + exit 1 + fi + done + else + echo "Fast core passed; the full matrix runs once the pull request is labelled full-verify" + exit 0 + fi echo "All path-relevant verification jobs passed" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ba45c64ca6..60ceeb00cf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -94,6 +94,13 @@ For broader validation, run: make verify ``` +In CI, every push to a pull request runs a fast Linux core: static checks, unit and integration +tests, native workspace checks and, for UI changes, the UI checks. The full matrix (wheels, Compose, +Windows, macOS, DuckDB end-to-end, warehouse, compiler-golden and timing jobs) runs once a +maintainer labels the pull request `full-verify`, normally when it is next to merge. The required +`Verify` check only reports after every suite the change needs has passed, so a pull request that +needs the full matrix cannot merge until it has run. + For focused iteration, run the narrowest relevant pytest target, for example: ```bash From ef60acc501e28ff75c5719a4b25bcb06ec2ab8c3 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 21:25:50 +0000 Subject: [PATCH 074/106] feat: run ad-hoc sqb commands from the web UI People check and launch `sqb build` from the run panel. The server resolves the selection against the current publication, authorises run.adhoc plus run.warehouse, run.prioritise and history-rewriting flags, and refuses any flag outside the ad-hoc allowlist by name. The dry run reads only and records nothing. Workers refuse an ad-hoc run whose warehouse the location does not configure for the target, before any statement runs; published jobs keep their authored warehouse. --- Cargo.lock | 1 + clients/typescript/src/schema.d.ts | 157 +++++++- crates/sqlbuild-cli/Cargo.toml | 1 + crates/sqlbuild-cli/src/adhoc/_helpers/mod.rs | 1 + .../src/adhoc/_helpers/planner.rs | 198 ++++++++++ crates/sqlbuild-cli/src/adhoc/constants.rs | 138 +++++++ .../src/adhoc/main/adhoc_planner.rs | 13 + crates/sqlbuild-cli/src/adhoc/main/mod.rs | 1 + crates/sqlbuild-cli/src/adhoc/mod.rs | 8 + crates/sqlbuild-cli/src/adhoc/tests.rs | 6 + .../sqlbuild-cli/src/adhoc/tests/helpers.rs | 27 ++ .../src/adhoc/tests/test_adhoc_planner.rs | 214 ++++++++++ .../src/adhoc/tests/test_types.rs | 8 + crates/sqlbuild-cli/src/lib.rs | 1 + .../src/native_commands/_helpers/execution.rs | 13 +- .../src/native_commands/main/_run_control.rs | 4 +- .../authentication/password_sessions.rs | 8 + .../_helpers/resources/person_operations.rs | 66 +++- .../src/api/main/serve_api.rs | 10 +- .../sqlbuild-orchestration/src/api/models.rs | 2 + .../authorization/_helpers/person_grants.rs | 3 +- .../main/decide_person_scopes.rs | 32 ++ .../src/authorization/main/mod.rs | 1 + .../src/credentials/models.rs | 2 + .../src/runs/_helpers/requests/adhoc.rs | 259 ++++++++++++ .../requests/cancel.rs} | 2 +- .../src/runs/_helpers/requests/mod.rs | 2 + .../src/runs/_helpers/requests/permissions.rs | 134 ++++--- .../src/runs/constants.rs | 5 + .../src/runs/main/check_adhoc_run.rs | 64 +++ .../src/runs/main/mod.rs | 4 +- .../src/runs/main/request_person_job_run.rs | 21 - .../src/runs/main/request_person_run.rs | 37 ++ crates/sqlbuild-orchestration/src/runs/mod.rs | 1 + .../sqlbuild-orchestration/src/runs/models.rs | 72 ++++ .../sqlbuild-orchestration/src/runs/types.rs | 19 + .../tests/api/helpers.rs | 7 +- .../tests/runs/helpers.rs | 176 ++++++++- .../tests/runs/test_adhoc.rs | 264 +++++++++++++ .../tests/runs/test_contracts.rs | 4 +- .../tests/runs/test_permissions.rs | 44 ++- .../tests/runs/test_types.rs | 77 ++++ .../_helpers/command_runtime/warehouses.rs | 72 ++-- .../compiled_project/snapshot_execution.rs | 17 +- .../_helpers/debug_command/run_debug.rs | 2 +- .../_helpers/location_command/worker.rs | 43 +- fensu.toml | 2 +- .../sqlbuild/references/docs/cli/server.md | 73 +++- .../publication/_test_types.py | 17 + .../compiled_project/publication/helpers.py | 27 ++ .../publication/test_adhoc.py | 187 +++++++++ ui/scripts/extract-command-table.mjs | 94 ++--- ui/src/lib/api/http-client.ts | 27 +- ui/src/lib/api/http-runs.ts | 17 +- ui/src/lib/api/mock/access.ts | 3 +- ui/src/lib/api/mock/operate-store.ts | 93 +++-- ui/src/lib/api/types.ts | 1 + ui/src/lib/api/types/operate.ts | 35 +- ui/src/lib/platform/permission-catalog.ts | 3 +- ui/src/lib/run/command-table.generated.json | 373 +++--------------- ui/src/lib/run/command-table.ts | 9 +- ui/src/lib/run/command.test.ts | 30 ++ ui/src/lib/run/command.ts | 99 +++-- ui/src/lib/run/run-panel.svelte | 122 +++--- ui/src/routes/runs/[id]/utils/banners.ts | 5 +- website/src/content/docs/docs/cli/server.mdx | 72 +++- 66 files changed, 2819 insertions(+), 711 deletions(-) create mode 100644 crates/sqlbuild-cli/src/adhoc/_helpers/mod.rs create mode 100644 crates/sqlbuild-cli/src/adhoc/_helpers/planner.rs create mode 100644 crates/sqlbuild-cli/src/adhoc/constants.rs create mode 100644 crates/sqlbuild-cli/src/adhoc/main/adhoc_planner.rs create mode 100644 crates/sqlbuild-cli/src/adhoc/main/mod.rs create mode 100644 crates/sqlbuild-cli/src/adhoc/mod.rs create mode 100644 crates/sqlbuild-cli/src/adhoc/tests.rs create mode 100644 crates/sqlbuild-cli/src/adhoc/tests/helpers.rs create mode 100644 crates/sqlbuild-cli/src/adhoc/tests/test_adhoc_planner.rs create mode 100644 crates/sqlbuild-cli/src/adhoc/tests/test_types.rs create mode 100644 crates/sqlbuild-orchestration/src/authorization/main/decide_person_scopes.rs create mode 100644 crates/sqlbuild-orchestration/src/runs/_helpers/requests/adhoc.rs rename crates/sqlbuild-orchestration/src/runs/{main/cancel_person_run.rs => _helpers/requests/cancel.rs} (98%) create mode 100644 crates/sqlbuild-orchestration/src/runs/main/check_adhoc_run.rs delete mode 100644 crates/sqlbuild-orchestration/src/runs/main/request_person_job_run.rs create mode 100644 crates/sqlbuild-orchestration/src/runs/main/request_person_run.rs create mode 100644 crates/sqlbuild-orchestration/src/runs/types.rs create mode 100644 crates/sqlbuild-orchestration/tests/runs/test_adhoc.rs create mode 100644 tests/integration/src/sqlbuild/compiler/compiled_project/publication/test_adhoc.py create mode 100644 ui/src/lib/run/command.test.ts diff --git a/Cargo.lock b/Cargo.lock index 77884247c9..b01d26733b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4200,6 +4200,7 @@ dependencies = [ "same-file", "serde", "serde_json", + "sqlbuild-analysis", "sqlbuild-build-features", "sqlbuild-cache", "sqlbuild-config", diff --git a/clients/typescript/src/schema.d.ts b/clients/typescript/src/schema.d.ts index a66f3d69c7..febc4edbdd 100644 --- a/clients/typescript/src/schema.d.ts +++ b/clients/typescript/src/schema.d.ts @@ -562,6 +562,38 @@ export interface paths { patch?: never; trace?: never; }; + "/api/control/runs/adhoc": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["requestAdhocRun"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/control/runs/adhoc/check": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["checkAdhocRun"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/control/runs/cancel": { parameters: { query?: never; @@ -954,7 +986,50 @@ export interface components { * @description Closed permission vocabulary; unknown actions fail deserialization. * @enum {string} */ - Action: "view" | "view.sql" | "view.evidence" | "job.run" | "run.cancel" | "run.rerun" | "run.rewrite_history" | "run.prioritise" | "trigger.toggle" | "sensor.reset_cursor" | "run.adhoc" | "connection.test" | "user.manage" | "role.assign" | "audit.view"; + Action: "view" | "view.sql" | "view.evidence" | "job.run" | "run.cancel" | "run.rerun" | "run.rewrite_history" | "run.prioritise" | "run.warehouse" | "trigger.toggle" | "sensor.reset_cursor" | "run.adhoc" | "connection.test" | "user.manage" | "role.assign" | "audit.view"; + /** @description What launching the command would run and whether this person may launch it. */ + AdhocCheck: { + /** @description Why the command cannot run regardless of permissions, such as a refused flag or an unknown selector. */ + error?: string | null; + launchable: boolean; + permissions: components["schemas"]["AdhocPermission"][]; + selection: string[]; + settings: { + [key: string]: unknown; + }; + /** Format: uuid */ + snapshot_id?: string | null; + verb?: string | null; + write_set: string[]; + }; + /** @description A dry run of a command; nothing is queued or audited. */ + AdhocCheckRequest: { + command: string[]; + /** Format: uuid */ + location_id: string; + /** Format: int64 */ + priority: number; + target: string; + }; + /** @description One permission the run needs and whether the person holds it on every resolved node. */ + AdhocPermission: { + action: components["schemas"]["Action"]; + allowed: boolean; + /** @description Why it is refused, as the launch would report it. */ + reason?: string | null; + }; + /** @description A person's command as argument tokens after `sqb`, never shell text; selectors resolve on the server. */ + AdhocRunRequest: { + command: string[]; + /** Format: uuid */ + location_id: string; + /** Format: int64 */ + priority: number; + reason: string; + /** Format: uuid */ + run_id: string; + target: string; + }; /** * @description Administrator membership and person eligibility share the final-admin invariant. * @enum {string} @@ -3542,6 +3617,86 @@ export interface operations { }; }; }; + requestAdhocRun: { + parameters: { + query?: never; + header: { + "x-sqb-csrf": string; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AdhocRunRequest"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MutationResponse"]; + }; + }; + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + /** @description Session rejected */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiErrorBody"]; + }; + }; + }; + }; + checkAdhocRun: { + parameters: { + query?: never; + header: { + "x-sqb-csrf": string; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AdhocCheckRequest"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AdhocCheck"]; + }; + }; + /** @description Session rejected */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; cancelPersonRun: { parameters: { query?: never; diff --git a/crates/sqlbuild-cli/Cargo.toml b/crates/sqlbuild-cli/Cargo.toml index 371988a3d0..5bb8a94fb2 100644 --- a/crates/sqlbuild-cli/Cargo.toml +++ b/crates/sqlbuild-cli/Cargo.toml @@ -27,6 +27,7 @@ reqwest.workspace = true rpassword.workspace = true serde.workspace = true serde_json = { workspace = true, features = ["arbitrary_precision"] } +sqlbuild-analysis.workspace = true sqlbuild-build-features.workspace = true sqlbuild-cache.workspace = true sqlbuild-config.workspace = true diff --git a/crates/sqlbuild-cli/src/adhoc/_helpers/mod.rs b/crates/sqlbuild-cli/src/adhoc/_helpers/mod.rs new file mode 100644 index 0000000000..f06700ef53 --- /dev/null +++ b/crates/sqlbuild-cli/src/adhoc/_helpers/mod.rs @@ -0,0 +1 @@ +pub(crate) mod planner; diff --git a/crates/sqlbuild-cli/src/adhoc/_helpers/planner.rs b/crates/sqlbuild-cli/src/adhoc/_helpers/planner.rs new file mode 100644 index 0000000000..d999dd7501 --- /dev/null +++ b/crates/sqlbuild-cli/src/adhoc/_helpers/planner.rs @@ -0,0 +1,198 @@ +//! Parse a command with the CLI's table, keep only allowed flags and resolve selectors to exact node keys. + +use std::collections::{BTreeMap, BTreeSet}; + +use serde_json::{Value as Json, json}; +use sqlbuild_analysis::graph::main::build_project_graph::build_project_graph; +use sqlbuild_analysis::graph::main::resolve_selectors::resolve_selectors; +use sqlbuild_analysis::graph::models::GraphResource; +use sqlbuild_core::text::main::python_str_repr::python_str_repr; +use sqlbuild_orchestration::runs::models::{CommandPlan, PlannerResource}; +use sqlbuild_orchestration::runs::types::CommandPlanner; + +use crate::adhoc::constants::{ + ADHOC_VERBS, ALLOWED_DESTS, BUILD_VERB, COMMAND_DEST, INTEGER_CONVERTERS, LOAD_SOURCES_DEST, + POSITIVE_INTEGER_CONVERTER, REFUSED_DESTS, SELECTOR_DESTS, +}; +use crate::argv::main::job_settings_arguments::job_settings_arguments; +use crate::argv::main::parse_cli_arguments::parse_cli_arguments; +use crate::argv::models::{ArgvSettings, Namespace, TypeFailure, Value}; +use crate::argv::types::ArgvHost; + +/// The planner `sqb server` is built with. +pub(crate) struct CliPlanner; + +impl CommandPlanner for CliPlanner { + fn plan( + &self, + command: &[String], + resources: &[PlannerResource], + ) -> Result { + let verb = command + .first() + .ok_or_else(|| "enter a command, such as build --select orders".to_owned())?; + if !ADHOC_VERBS.contains(&verb.as_str()) { + return Err(format!( + "sqb {verb} is not available as an ad-hoc run; use build, test, seed, audit or freshness" + )); + } + let parsed = parse(command)?; + let bare = parse(std::slice::from_ref(verb))?; + let mut selectors: BTreeMap<&str, Vec> = BTreeMap::new(); + let mut settings: BTreeMap = BTreeMap::new(); + for (dest, value) in &parsed.entries { + if bare.value(dest) == *value || dest == COMMAND_DEST { + continue; + } + if let Some(selector) = SELECTOR_DESTS.iter().find(|name| **name == dest) { + selectors.insert(selector, texts(value)); + continue; + } + settings.insert(dest.clone(), setting(dest, value)?); + } + if verb == BUILD_VERB { + job_settings_arguments(&settings)?; + } + let mut select: Vec = selectors.remove("select").unwrap_or_default(); + select.extend(selectors.remove("positional_select").unwrap_or_default()); + let exclude: Vec = selectors.remove("exclude").unwrap_or_default(); + Ok(CommandPlan { + verb: verb.clone(), + selection: resolve(resources, &select, &exclude)?, + settings, + }) + } +} + +/// Whole numbers convert as the parser needs them; paths and variables stay text so the refusal can name them. +struct PlannerHost; + +impl ArgvHost for PlannerHost { + fn convert(&mut self, converter: &str, text: &str) -> Result { + if !INTEGER_CONVERTERS.contains(&converter) { + return Ok(Value::Str(text.to_owned())); + } + let number: i64 = text.trim().parse().map_err(|_| TypeFailure::Invalid)?; + if converter == POSITIVE_INTEGER_CONVERTER && number < 1 { + return Err(TypeFailure::ArgumentType( + "must be a positive integer".to_owned(), + )); + } + Ok(Value::Int(number.to_string())) + } + + fn repr(&self, text: &str) -> String { + python_str_repr(text) + } + + fn program_version(&mut self) -> String { + String::new() + } +} + +fn parse(command: &[String]) -> Result { + let settings = ArgvSettings { + columns: 100, + use_color: false, + }; + let outcome = parse_cli_arguments(command, Vec::new(), settings, &mut PlannerHost); + outcome.parsed.map_err(|_| { + let written: String = outcome.writes.into_iter().map(|(_, text)| text).collect(); + written + .lines() + .rev() + .find(|line| line.contains("error")) + .map_or_else( + || "help is not a run; enter a command to check it".to_owned(), + |line| line.trim().to_owned(), + ) + }) +} + +fn texts(value: &Value) -> Vec { + match value { + Value::List(items) => items + .iter() + .filter_map(|item| match item { + Value::Str(text) => Some(text.clone()), + _ => None, + }) + .collect(), + _ => Vec::new(), + } +} + +/// One allowed flag as a frozen setting, or why the flag is refused. +fn setting(dest: &str, value: &Value) -> Result { + if let Some((_, flag, reason)) = REFUSED_DESTS.iter().find(|(name, _, _)| *name == dest) { + return Err(format!("{flag} is not available for ad-hoc runs: {reason}")); + } + if dest == LOAD_SOURCES_DEST && *value == Value::Bool(true) { + return Err( + "--load is not available for ad-hoc runs: source loads are not part of ad-hoc runs yet" + .into(), + ); + } + if !ALLOWED_DESTS.contains(&dest) { + return Err(format!( + "{} is not available for ad-hoc runs", + flag_of(dest) + )); + } + match value { + Value::Bool(flag) => Ok(json!(flag)), + Value::Int(text) => text + .parse::() + .map(|number| json!(number)) + .map_err(|_| format!("{} must be a whole number", flag_of(dest))), + Value::Str(text) => Ok(json!(text)), + _ => Err(format!( + "{} is not available for ad-hoc runs", + flag_of(dest) + )), + } +} + +/// The flag people type for a parser destination that has its own `--name` flag. +fn flag_of(dest: &str) -> String { + format!("--{}", dest.replace('_', "-")) +} + +/// Exact `kind:name` keys, as `sqb build` would select them from this publication. +fn resolve( + resources: &[PlannerResource], + select: &[String], + exclude: &[String], +) -> Result, String> { + let graph_resources: Vec = resources.iter().filter_map(graph_resource).collect(); + let graph = build_project_graph(&graph_resources); + let selected: BTreeSet = resolve_selectors(&graph, select, exclude) + .map_err(|error| format!("{}: {}", error.code, error.message))? + .into_iter() + .map(|(kind, name)| format!("{kind}:{name}")) + .collect(); + if selected.is_empty() { + return Err("the selectors match no nodes in the current publication".into()); + } + Ok(selected) +} + +/// A published node as the selector graph sees it; keys without a kind are not selectable. +fn graph_resource(resource: &PlannerResource) -> Option { + Some(GraphResource { + key: object_key(&resource.key)?, + deps: resource + .deps + .iter() + .map(String::as_str) + .filter_map(object_key) + .collect(), + tags: resource.tags.clone(), + folder: resource.folder.clone(), + }) +} + +fn object_key(key: &str) -> Option<(String, String)> { + key.split_once(':') + .map(|(kind, name)| (kind.to_owned(), name.to_owned())) +} diff --git a/crates/sqlbuild-cli/src/adhoc/constants.rs b/crates/sqlbuild-cli/src/adhoc/constants.rs new file mode 100644 index 0000000000..81f0d9ff24 --- /dev/null +++ b/crates/sqlbuild-cli/src/adhoc/constants.rs @@ -0,0 +1,138 @@ +//! Which commands and flags an ad-hoc run accepts; anything not listed is refused. + +/// Verbs a person may plan; workers launch only those the control plane lists. +pub(crate) const ADHOC_VERBS: [&str; 5] = ["build", "test", "seed", "audit", "freshness"]; + +/// The verb whose settings must also form valid job settings. +pub(crate) const BUILD_VERB: &str = "build"; + +/// The parser destination holding the verb itself, which is not a setting. +pub(crate) const COMMAND_DEST: &str = "command"; + +/// The destination `--load` and `--no-load` share; only `--no-load` is accepted. +pub(crate) const LOAD_SOURCES_DEST: &str = "load_sources"; + +/// Parser destinations that carry selectors, resolved to exact node keys on the server. +pub(crate) const SELECTOR_DESTS: [&str; 3] = ["select", "exclude", "positional_select"]; + +/// Destinations an ad-hoc run may set; the control plane gates history rewrites and warehouses. +pub(crate) const ALLOWED_DESTS: [&str; 27] = [ + "max_microbatches", + "run_tests", + "run_audits", + "include_python", + "fail_fast", + "concurrency", + "test_timeout", + "selection_diagnostics", + "verbose", + "case", + "inspect", + "state", + "fail_on_error", + "fail_on_stale", + "full_refresh", + "start_cursor", + "end_cursor", + "start_cursor_ts", + "end_cursor_ts", + "start_cursor_int", + "end_cursor_int", + "allow_snapshot_full_refresh", + "allow_snapshot_schema_change", + "allow_table_type_downgrade", + "allow_retention_decrease", + "warehouse", + "load_sources", +]; + +/// Parser converters that take whole numbers; the table names them as Python does. +pub(crate) const INTEGER_CONVERTERS: [&str; 2] = ["int", POSITIVE_INTEGER_CONVERTER]; + +/// The converter that also refuses zero and negative numbers. +pub(crate) const POSITIVE_INTEGER_CONVERTER: &str = "_positive_integer"; + +/// Refused destinations: the flag people typed and why an ad-hoc run cannot honour it. +pub(crate) const REFUSED_DESTS: [(&str, &str, &str); 25] = [ + ( + "select_file", + "--select-file", + "it reads a file on the worker", + ), + ( + "reload", + "--reload", + "source loads are not part of ad-hoc runs yet", + ), + ( + "allow_missing_migration_origin", + "--allow-missing-migration-origin", + "it can lose data", + ), + ("vars", "--vars", "variables change the published SQL"), + ("defer_to", "--defer-to", "it reads another target"), + ( + "defer_sources_to", + "--defer-sources-to", + "it reads another target", + ), + ( + "defer_clone_from", + "--defer-clone-from", + "it clones from another target", + ), + ( + "no_sql_validation", + "--no-sql-analysis", + "workers always verify the published SQL", + ), + ("no_cache", "--no-cache", "workers manage their own cache"), + ( + "target", + "--target", + "the run's target is the one you are viewing", + ), + ( + "project_dir", + "--project-dir", + "workers run the published project", + ), + ( + "dbt_project_dir", + "--dbt-project-dir", + "workers run the published project", + ), + ( + "dbt_profiles_dir", + "--dbt-profiles-dir", + "workers run the published project", + ), + ( + "dbt_target", + "--dbt-target", + "workers run the published project", + ), + ( + "dbt_target_path", + "--dbt-target-path", + "workers run the published project", + ), + ("json", "--json", "workers own the run's output"), + ( + "json_output", + "--json-output", + "workers own the run's output", + ), + ( + "event_output", + "--event-output", + "workers own the run's output", + ), + ("manifest", "--manifest", "workers own the run's output"), + ("no_color", "--no-color", "workers own the run's output"), + ("debug", "--debug", "it prints executed SQL"), + ("help", "--help", "help is not a run"), + ("version", "--version", "it is not a run"), + ("since", "--since", "it needs the worker's Git checkout"), + ("explain", "--explain", "it needs the worker's Git checkout"), +]; diff --git a/crates/sqlbuild-cli/src/adhoc/main/adhoc_planner.rs b/crates/sqlbuild-cli/src/adhoc/main/adhoc_planner.rs new file mode 100644 index 0000000000..64203a9c8f --- /dev/null +++ b/crates/sqlbuild-cli/src/adhoc/main/adhoc_planner.rs @@ -0,0 +1,13 @@ +//! The command planner `sqb server` resolves ad-hoc runs with. + +use std::sync::Arc; + +use sqlbuild_orchestration::runs::types::CommandPlanner; + +use crate::adhoc::_helpers::planner::CliPlanner; + +/// The CLI's grammar, flag allowlist and selector resolver behind the control plane's planner contract. +#[must_use] +pub fn adhoc_planner() -> Arc { + Arc::new(CliPlanner) +} diff --git a/crates/sqlbuild-cli/src/adhoc/main/mod.rs b/crates/sqlbuild-cli/src/adhoc/main/mod.rs new file mode 100644 index 0000000000..64db9001ca --- /dev/null +++ b/crates/sqlbuild-cli/src/adhoc/main/mod.rs @@ -0,0 +1 @@ +pub mod adhoc_planner; diff --git a/crates/sqlbuild-cli/src/adhoc/mod.rs b/crates/sqlbuild-cli/src/adhoc/mod.rs new file mode 100644 index 0000000000..d22d23c1d3 --- /dev/null +++ b/crates/sqlbuild-cli/src/adhoc/mod.rs @@ -0,0 +1,8 @@ +//! Ad-hoc commands for `sqb server`: the CLI's own grammar, a flag allowlist and selectors resolved over a publication. + +pub(crate) mod _helpers; +pub(crate) mod constants; +pub mod main; + +#[cfg(test)] +mod tests; diff --git a/crates/sqlbuild-cli/src/adhoc/tests.rs b/crates/sqlbuild-cli/src/adhoc/tests.rs new file mode 100644 index 0000000000..f06a26c26a --- /dev/null +++ b/crates/sqlbuild-cli/src/adhoc/tests.rs @@ -0,0 +1,6 @@ +#[path = "tests/test_adhoc_planner.rs"] +mod adhoc_planner; +#[path = "tests/helpers.rs"] +mod helpers; +#[path = "tests/test_types.rs"] +mod test_types; diff --git a/crates/sqlbuild-cli/src/adhoc/tests/helpers.rs b/crates/sqlbuild-cli/src/adhoc/tests/helpers.rs new file mode 100644 index 0000000000..0725ac9380 --- /dev/null +++ b/crates/sqlbuild-cli/src/adhoc/tests/helpers.rs @@ -0,0 +1,27 @@ +use serde_json::Value; +use sqlbuild_orchestration::runs::models::PlannerResource; + +/// A published node as the planner receives it from the control plane. +pub(super) fn resource( + key: &str, + deps: &[&str], + tags: &[&str], + folder: Option<&str>, +) -> PlannerResource { + PlannerResource { + key: key.into(), + deps: deps.iter().map(|dep| (*dep).to_owned()).collect(), + tags: tags.iter().map(|tag| (*tag).to_owned()).collect(), + folder: folder.map(str::to_owned), + } +} + +/// An expected plan in the planner's owned form, so the case compares whole results. +pub(super) fn owned_plan( + expected: &Result<(&[&str], Value), &str>, +) -> Result<(Vec, Value), String> { + expected + .clone() + .map(|(keys, settings)| (keys.iter().map(|key| (*key).to_owned()).collect(), settings)) + .map_err(str::to_owned) +} diff --git a/crates/sqlbuild-cli/src/adhoc/tests/test_adhoc_planner.rs b/crates/sqlbuild-cli/src/adhoc/tests/test_adhoc_planner.rs new file mode 100644 index 0000000000..03f1a5aeea --- /dev/null +++ b/crates/sqlbuild-cli/src/adhoc/tests/test_adhoc_planner.rs @@ -0,0 +1,214 @@ +//! The planner parses with the CLI's own table, refuses every flag outside the allowlist and resolves exact keys. + +use serde_json::json; + +use crate::adhoc::main::adhoc_planner::adhoc_planner; +use crate::adhoc::tests::helpers::{owned_plan, resource}; +use crate::adhoc::tests::test_types::AdhocPlanTestCase; + +#[test] +fn given_ad_hoc_commands_when_planned_then_flags_follow_the_allowlist_and_selectors_resolve_exactly() + { + let resources = [ + resource( + "model:orders", + &["source:shop.raw_orders", "seed:currencies"], + &["daily"], + Some("models/marts"), + ), + resource( + "model:order_totals", + &["model:orders"], + &["daily"], + Some("models/marts"), + ), + resource("model:customers", &[], &[], Some("models/staging")), + resource("source:shop.raw_orders", &[], &[], None), + resource("seed:currencies", &[], &[], None), + ]; + let test_cases = [ + AdhocPlanTestCase { + description: "upstream selector reaches the source and seed it reads", + command: &["build", "--select", "+orders"], + expected_plan: Ok(( + &["model:orders", "seed:currencies", "source:shop.raw_orders"], + json!({}), + )), + }, + AdhocPlanTestCase { + description: "positional selectors join --select, and --exclude removes", + command: &[ + "build", + "tag:daily", + "-s", + "customers", + "--exclude", + "order_totals", + ], + expected_plan: Ok((&["model:customers", "model:orders"], json!({}))), + }, + AdhocPlanTestCase { + description: "no selector builds every node, as on the command line", + command: &["build", "--fail-fast"], + expected_plan: Ok(( + &[ + "model:customers", + "model:order_totals", + "model:orders", + "seed:currencies", + "source:shop.raw_orders", + ], + json!({"fail_fast": true}), + )), + }, + AdhocPlanTestCase { + description: "an abbreviated flag is the same flag, so it is still detected", + command: &["build", "-s", "orders", "--full-ref"], + expected_plan: Ok((&["model:orders"], json!({"full_refresh": true}))), + }, + AdhocPlanTestCase { + description: "store-false flags and gated flags become settings by destination", + command: &[ + "build", + "-s", + "customers", + "--no-tests", + "--no-load", + "--warehouse", + "BUILD_XS", + "--concurrency", + "4", + ], + expected_plan: Ok(( + &["model:customers"], + json!({"concurrency": 4, "load_sources": false, "run_tests": false, "warehouse": "BUILD_XS"}), + )), + }, + AdhocPlanTestCase { + description: "cursor flags pass through for the server to gate", + command: &[ + "build", + "-s", + "customers", + "--start-cursor-int", + "1", + "--end-cursor-int", + "9", + ], + expected_plan: Ok(( + &["model:customers"], + json!({"end_cursor_int": "9", "start_cursor_int": "1"}), + )), + }, + AdhocPlanTestCase { + description: "path selectors use the published folder", + command: &["build", "-s", "path:models/staging"], + expected_plan: Ok((&["model:customers"], json!({}))), + }, + AdhocPlanTestCase { + description: "variables are refused", + command: &["build", "--vars", "{\"day\": 1}"], + expected_plan: Err("--vars is not available for ad-hoc runs"), + }, + AdhocPlanTestCase { + description: "a flag outside the allowlist is refused by the name people typed", + command: &["build", "--changed"], + expected_plan: Err("--changed is not available for ad-hoc runs"), + }, + AdhocPlanTestCase { + description: "a selector file is refused", + command: &["build", "--select-file", "selection.txt"], + expected_plan: Err("--select-file is not available"), + }, + AdhocPlanTestCase { + description: "loading sources is refused", + command: &["build", "--load"], + expected_plan: Err("--load is not available"), + }, + AdhocPlanTestCase { + description: "another target is refused", + command: &["build", "--target", "production"], + expected_plan: Err("--target is not available"), + }, + AdhocPlanTestCase { + description: "debug output would print executed SQL", + command: &["build", "--debug"], + expected_plan: Err("--debug is not available"), + }, + AdhocPlanTestCase { + description: "deferral reads another target", + command: &["build", "--defer-to", "production"], + expected_plan: Err("--defer-to is not available"), + }, + AdhocPlanTestCase { + description: "a missing migration origin can lose data", + command: &["build", "--allow-missing-migration-origin"], + expected_plan: Err("--allow-missing-migration-origin is not available"), + }, + AdhocPlanTestCase { + description: "an unknown flag fails the CLI's own parse", + command: &["build", "--everything"], + expected_plan: Err("unrecognized arguments: --everything"), + }, + AdhocPlanTestCase { + description: "concurrency must be positive", + command: &["build", "--concurrency", "0"], + expected_plan: Err("--concurrency must be >= 1"), + }, + AdhocPlanTestCase { + description: "the build parser's own cursor rules still apply", + command: &["build", "--start-cursor", "1", "--start-cursor-int", "1"], + expected_plan: Err("cannot be combined with the -ts or -int cursor flags"), + }, + AdhocPlanTestCase { + description: "an unknown name is the CLI's selector error", + command: &["build", "-s", "returns"], + expected_plan: Err("returns"), + }, + AdhocPlanTestCase { + description: "loads are not ad-hoc runs yet", + command: &["load"], + expected_plan: Err("sqb load is not available as an ad-hoc run"), + }, + AdhocPlanTestCase { + description: "commands that are not runs are refused", + command: &["server"], + expected_plan: Err("sqb server is not available as an ad-hoc run"), + }, + AdhocPlanTestCase { + description: "other verbs plan with their own parser", + command: &["test", "-s", "orders", "--case", "happy_path"], + expected_plan: Ok((&["model:orders"], json!({"case": "happy_path"}))), + }, + ]; + let planner = adhoc_planner(); + for test_case in test_cases { + let command: Vec = test_case + .command + .iter() + .map(|token| (*token).to_owned()) + .collect(); + let actual = planner + .plan(&command, &resources) + .map(|plan| { + ( + plan.selection.into_iter().collect::>(), + json!(plan.settings), + ) + }) + .map_err(|error| { + test_case + .expected_plan + .as_ref() + .err() + .filter(|fragment| error.contains(**fragment)) + .map_or(error.clone(), |fragment| (*fragment).to_owned()) + }); + assert_eq!( + actual, + owned_plan(&test_case.expected_plan), + "{}", + test_case.description + ); + } +} diff --git a/crates/sqlbuild-cli/src/adhoc/tests/test_types.rs b/crates/sqlbuild-cli/src/adhoc/tests/test_types.rs new file mode 100644 index 0000000000..dec62a7bb3 --- /dev/null +++ b/crates/sqlbuild-cli/src/adhoc/tests/test_types.rs @@ -0,0 +1,8 @@ +//! Table rows for the ad-hoc planner. + +/// A command, and the exact keys and settings it plans to, or a fragment of its refusal. +pub(super) struct AdhocPlanTestCase { + pub(super) description: &'static str, + pub(super) command: &'static [&'static str], + pub(super) expected_plan: Result<(&'static [&'static str], serde_json::Value), &'static str>, +} diff --git a/crates/sqlbuild-cli/src/lib.rs b/crates/sqlbuild-cli/src/lib.rs index ce5297b160..6fd859c0bb 100644 --- a/crates/sqlbuild-cli/src/lib.rs +++ b/crates/sqlbuild-cli/src/lib.rs @@ -2,6 +2,7 @@ #![forbid(unsafe_code)] +pub mod adhoc; pub mod argv; pub mod compute_logs; pub mod cost_store; diff --git a/crates/sqlbuild-cli/src/native_commands/_helpers/execution.rs b/crates/sqlbuild-cli/src/native_commands/_helpers/execution.rs index 406d840032..0b9f3ae3e4 100644 --- a/crates/sqlbuild-cli/src/native_commands/_helpers/execution.rs +++ b/crates/sqlbuild-cli/src/native_commands/_helpers/execution.rs @@ -11,9 +11,9 @@ use serde_json::json; use sqlbuild_orchestration::api::constants::BODY_BYTES; use sqlbuild_orchestration::api::errors::ClientError; use sqlbuild_orchestration::api::main::serve_api::serve_api; -use sqlbuild_orchestration::api::models::{ - ApiAuthentication, DevelopmentScope, DockerImageSource, HumanLogin, -}; + +use crate::adhoc::main::adhoc_planner::adhoc_planner; +use sqlbuild_orchestration::api::models::{ApiAuthentication, DevelopmentScope, HumanLogin}; use sqlbuild_orchestration::credentials::models::AuditRetention; use sqlbuild_orchestration::platform::constants::PLATFORM_WEBHOOK_VARIABLE; use sqlbuild_orchestration::platform::main::report_remote_location_health::report_remote_location_health; @@ -22,6 +22,7 @@ use sqlbuild_orchestration::platform::models::{ LocationHealth, LocationHealthReport, LocationWorkClient, LocationWorkPoll, WarehouseSyncFailure, }; +use sqlbuild_orchestration::runs::types::RunHandoff; use sqlbuild_orchestration::scheduler::main::run_scheduler::run_scheduler; use sqlbuild_orchestration::scheduler::models::SchedulerState; use sqlbuild_orchestration::snapshots::main::next_remote_generation::next_remote_generation; @@ -111,7 +112,7 @@ pub(crate) async fn run_development_roles( (ApiAuthentication { login: HumanLogin::Development(scope), ..ApiAuthentication::default() - }, Vec::new(), Default::default()), + }, (Vec::new(), adhoc_planner()), Default::default()), (move || { drop(write_output(&[(Stream::Stderr, &format!( "Development API ready: {address}\nDevelopment location: {location_id}\nAuthentication disabled on loopback; triggers {}\n", @@ -814,7 +815,7 @@ pub(crate) async fn open_control_store( /// Decode privileged CLI configuration before handing process ownership to the workers domain. pub(crate) fn open_control_workers( namespace: &Namespace, -) -> Result<(Workers, Vec), ControlError> { +) -> Result<(Workers, RunHandoff), ControlError> { let workers = open_workers( text(namespace, "control_launchers").as_deref(), text(namespace, "control_database_env") @@ -822,7 +823,7 @@ pub(crate) fn open_control_workers( !flag(namespace, "control_no_scheduler"), )?; let images = docker_image_sources(&workers); - Ok((workers, images)) + Ok((workers, (images, adhoc_planner()))) } pub(crate) async fn shutdown_signal() -> Result<(), ControlError> { diff --git a/crates/sqlbuild-cli/src/native_commands/main/_run_control.rs b/crates/sqlbuild-cli/src/native_commands/main/_run_control.rs index d5a88fd9d5..673283718f 100644 --- a/crates/sqlbuild-cli/src/native_commands/main/_run_control.rs +++ b/crates/sqlbuild-cli/src/native_commands/main/_run_control.rs @@ -29,7 +29,7 @@ pub(crate) async fn run_control( let address = control_address(namespace, standalone)?; let (authentication, (statistics, audit, snapshots)) = control_api_settings(namespace)?; let store = open_control_store(namespace).await?; - let (workers, images) = open_control_workers(namespace)?; + let (workers, handoff) = open_control_workers(namespace)?; let (stop, shutdown) = watch::channel(false); let mut tasks = tokio::task::JoinSet::new(); if let Some(address) = address { @@ -42,7 +42,7 @@ pub(crate) async fn run_control( tasks.spawn(serve_api( store.clone(), listener, - (authentication, images, statistics), + (authentication, handoff, statistics), ( move || { drop(write_output(&[( diff --git a/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs b/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs index 5bce2d5b43..a500be0adf 100644 --- a/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs +++ b/crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs @@ -73,6 +73,14 @@ pub(crate) fn person_routes() -> Router { "/api/control/runs", post(crate::api::_helpers::resources::person_operations::request_job), ) + .route( + "/api/control/runs/adhoc", + post(crate::api::_helpers::resources::person_operations::request_adhoc), + ) + .route( + "/api/control/runs/adhoc/check", + post(crate::api::_helpers::resources::person_operations::check_adhoc), + ) .route( "/api/control/runs/cancel", post(crate::api::_helpers::resources::person_operations::cancel_run), diff --git a/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_operations.rs b/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_operations.rs index 02fcaff030..0bc0faabba 100644 --- a/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_operations.rs +++ b/crates/sqlbuild-orchestration/src/api/_helpers/resources/person_operations.rs @@ -1,5 +1,7 @@ //! Person operations use person sessions, never the machine-resource bearer router. +use std::sync::Arc; + use axum::extract::{Path, Query, State}; use axum::{Extension, Json}; use uuid::Uuid; @@ -10,11 +12,15 @@ use crate::credentials::models::AuthenticatedPerson; use crate::credentials::models::{AuditContext, DefaultRole}; use crate::declarations::main::change_person_trigger::change_person_trigger; use crate::declarations::models::TriggerChangeRequest; -use crate::runs::main::cancel_person_run::cancel_person_run; +use crate::runs::main::check_adhoc_run::check_adhoc_run; use crate::runs::main::list_person_runs::list_person_runs; -use crate::runs::main::request_person_job_run::request_person_job_run; +use crate::runs::main::request_person_run::request_person_run; use crate::runs::main::request_rerun::request_rerun; -use crate::runs::models::{CancelRunRequest, JobRunRequest, RerunRequest, RunRecord}; +use crate::runs::models::{ + AdhocCheck, AdhocCheckRequest, AdhocRunRequest, CancelRunRequest, JobRunRequest, + PersonRunRequest, RerunRequest, RunRecord, +}; +use crate::runs::types::CommandPlanner; use crate::store::models::ControlStore; use crate::warehouse_metadata::main::read_person_catalog::read_person_catalog; use crate::warehouse_metadata::main::read_person_fingerprint::read_person_fingerprint; @@ -64,10 +70,55 @@ pub(crate) async fn request_job( Json(request): Json, ) -> Result, ApiError> { Ok(Json(MutationResponse { - applied: request_person_job_run(&store, (&context, principal), &request).await?, + applied: request_person_run( + &store, + (&context, principal), + PersonRunRequest::Job(&request), + ) + .await?, })) } +#[utoipa::path(post, path = "/api/control/runs/adhoc", operation_id = "requestAdhocRun", + params(("x-sqb-csrf" = String, Header)), request_body = AdhocRunRequest, + responses((status = 200, body = MutationResponse), (status = 400, body = crate::api::models::ApiErrorBody), + (status = 401, description = "Session rejected"), (status = 403, body = crate::api::models::ApiErrorBody)), + security(("session" = []), ("cliSession" = [])))] +pub(crate) async fn request_adhoc( + (State(store), Extension(context)): (State, Extension), + (Extension(AuthenticatedPerson(principal, _)), Extension(planner)): ( + Extension, + Extension>, + ), + Json(request): Json, +) -> Result, ApiError> { + Ok(Json(MutationResponse { + applied: request_person_run( + &store, + (&context, principal), + PersonRunRequest::Adhoc(planner.as_ref(), &request), + ) + .await?, + })) +} + +#[utoipa::path(post, path = "/api/control/runs/adhoc/check", operation_id = "checkAdhocRun", + params(("x-sqb-csrf" = String, Header)), request_body = AdhocCheckRequest, + responses((status = 200, body = AdhocCheck), (status = 401, description = "Session rejected")), + security(("session" = []), ("cliSession" = [])))] +pub(crate) async fn check_adhoc( + (State(store), Extension(context)): (State, Extension), + (Extension(AuthenticatedPerson(principal, _)), Extension(planner)): ( + Extension, + Extension>, + ), + Json(request): Json, +) -> Result, ApiError> { + Ok(Json( + check_adhoc_run(&store, (&context, principal), planner.as_ref(), &request).await?, + )) +} + #[utoipa::path(post, path = "/api/control/runs/cancel", operation_id = "cancelPersonRun", params(("x-sqb-csrf" = String, Header)), request_body = CancelRunRequest, responses((status = 200, body = MutationResponse), (status = 401, description = "Session rejected"), @@ -78,7 +129,12 @@ pub(crate) async fn cancel_run( Json(request): Json, ) -> Result, ApiError> { Ok(Json(MutationResponse { - applied: cancel_person_run(&store, (&context, principal), &request).await?, + applied: request_person_run( + &store, + (&context, principal), + PersonRunRequest::Cancel(&request), + ) + .await?, })) } diff --git a/crates/sqlbuild-orchestration/src/api/main/serve_api.rs b/crates/sqlbuild-orchestration/src/api/main/serve_api.rs index 310577350e..29eb042961 100644 --- a/crates/sqlbuild-orchestration/src/api/main/serve_api.rs +++ b/crates/sqlbuild-orchestration/src/api/main/serve_api.rs @@ -7,10 +7,9 @@ use crate::api::_helpers::authentication::{bearer::local_request, configure::con use crate::api::_helpers::resources::ui_assets::{UI_ASSETS, serve_ui}; use crate::api::_helpers::resources::{changes, router::router}; use crate::api::main::openapi_document::openapi_document; -use crate::api::models::{ - ApiAuthentication, ChangeFeed, DevelopmentScope, DockerImageSource, GroupRole, -}; +use crate::api::models::{ApiAuthentication, ChangeFeed, DevelopmentScope, GroupRole}; use crate::authorization::main::publish_group_roles::publish_group_roles; +use crate::runs::types::RunHandoff; use crate::store::errors::ControlError; use crate::store::main::migrate_store::migrate_store; use crate::store::main::subscribe_changes::subscribe_changes; @@ -20,9 +19,9 @@ use crate::warehouse_metadata::models::StatisticsRetention; pub async fn serve_api( store: ControlStore, listener: tokio::net::TcpListener, - (authentication, images, retention): ( + (authentication, (images, planner), retention): ( ApiAuthentication, - Vec, + RunHandoff, StatisticsRetention, ), (ready, shutdown): ( @@ -43,6 +42,7 @@ pub async fn serve_api( let application = router(&store, feed.clone()) .merge(authentication_routes.layer(Extension(feed))) .layer(Extension(std::sync::Arc::new(images))) + .layer(Extension(planner)) .layer(Extension(retention)) .route( "/api/openapi.json", diff --git a/crates/sqlbuild-orchestration/src/api/models.rs b/crates/sqlbuild-orchestration/src/api/models.rs index 9571345a46..501b46bde7 100644 --- a/crates/sqlbuild-orchestration/src/api/models.rs +++ b/crates/sqlbuild-orchestration/src/api/models.rs @@ -16,6 +16,8 @@ use crate::api::_helpers::resources::{ administrators::assign, audit::read, person_operations::request_job, + person_operations::request_adhoc, + person_operations::check_adhoc, person_operations::cancel_run, person_operations::rerun, person_operations::change_trigger, diff --git a/crates/sqlbuild-orchestration/src/authorization/_helpers/person_grants.rs b/crates/sqlbuild-orchestration/src/authorization/_helpers/person_grants.rs index 59a38db9e4..69aee503c9 100644 --- a/crates/sqlbuild-orchestration/src/authorization/_helpers/person_grants.rs +++ b/crates/sqlbuild-orchestration/src/authorization/_helpers/person_grants.rs @@ -12,7 +12,7 @@ use crate::store::main::query::query; use crate::store::models::{ControlTransaction, Parameter}; use crate::store::types::Row; -const ACTIONS: [Action; 15] = [ +const ACTIONS: [Action; 16] = [ Action::View, Action::ViewSql, Action::ViewEvidence, @@ -21,6 +21,7 @@ const ACTIONS: [Action; 15] = [ Action::RunRerun, Action::RunRewriteHistory, Action::RunPrioritise, + Action::RunWarehouse, Action::TriggerToggle, Action::SensorResetCursor, Action::RunAdhoc, diff --git a/crates/sqlbuild-orchestration/src/authorization/main/decide_person_scopes.rs b/crates/sqlbuild-orchestration/src/authorization/main/decide_person_scopes.rs new file mode 100644 index 0000000000..b080b05001 --- /dev/null +++ b/crates/sqlbuild-orchestration/src/authorization/main/decide_person_scopes.rs @@ -0,0 +1,32 @@ +//! Decide scopes for a person without recording anything, for dry runs that must not audit while people type. + +use uuid::Uuid; + +use crate::authorization::_helpers::custom_roles::denial_reason; +use crate::authorization::_helpers::person_grants::{ + GrantDecision, decide_grant, person_role_rows, +}; +use crate::credentials::models::{DefaultRole, PermissionScope}; +use crate::store::errors::ControlError; +use crate::store::models::ControlTransaction; + +/// Each scope's refusal as a launch would show it, `None` where granted; `None` overall for an inactive person. +pub(crate) async fn decide_person_scopes( + transaction: ControlTransaction, + principal_id: Uuid, + (target, scopes): (&str, &[PermissionScope]), +) -> Result<(ControlTransaction, Option>>), ControlError> { + let (transaction, rows) = + person_role_rows(transaction, principal_id, Some(target.into())).await?; + let Some(rows) = rows else { + return Ok((transaction, None)); + }; + let mut refusals: Vec> = Vec::new(); + for scope in scopes { + refusals.push(match decide_grant(&rows, scope, DefaultRole::None) { + GrantDecision::Granted(_) => None, + GrantDecision::Denied(missing) => Some(denial_reason(scope.action, &missing)?.1), + }); + } + Ok((transaction, Some(refusals))) +} diff --git a/crates/sqlbuild-orchestration/src/authorization/main/mod.rs b/crates/sqlbuild-orchestration/src/authorization/main/mod.rs index 03b7ff29e1..fb5c7bb76f 100644 --- a/crates/sqlbuild-orchestration/src/authorization/main/mod.rs +++ b/crates/sqlbuild-orchestration/src/authorization/main/mod.rs @@ -5,6 +5,7 @@ pub(crate) mod begin_authorized; pub mod bootstrap_user; pub mod change_administrator; pub mod compile_roles; +pub(crate) mod decide_person_scopes; pub(crate) mod deny_authorization; pub mod grant_admin; pub(crate) mod person_assigns_roles; diff --git a/crates/sqlbuild-orchestration/src/credentials/models.rs b/crates/sqlbuild-orchestration/src/credentials/models.rs index 9f9e44b9df..af4ceb3b5b 100644 --- a/crates/sqlbuild-orchestration/src/credentials/models.rs +++ b/crates/sqlbuild-orchestration/src/credentials/models.rs @@ -91,6 +91,8 @@ pub enum Action { RunRewriteHistory, #[serde(rename = "run.prioritise")] RunPrioritise, + #[serde(rename = "run.warehouse")] + RunWarehouse, #[serde(rename = "trigger.toggle")] TriggerToggle, #[serde(rename = "sensor.reset_cursor")] diff --git a/crates/sqlbuild-orchestration/src/runs/_helpers/requests/adhoc.rs b/crates/sqlbuild-orchestration/src/runs/_helpers/requests/adhoc.rs new file mode 100644 index 0000000000..a6792c1b07 --- /dev/null +++ b/crates/sqlbuild-orchestration/src/runs/_helpers/requests/adhoc.rs @@ -0,0 +1,259 @@ +//! Resolve a command against the current publication and freeze it as an ad-hoc run request. + +use std::collections::BTreeSet; + +use serde_json::{Value, json}; +use uuid::Uuid; + +use crate::authorization::main::authorize_listing::authorize_listing; +use crate::authorization::main::decide_person_scopes::decide_person_scopes; +use crate::credentials::main::audit::append_audit::append_audit; +use crate::credentials::models::{Action, AuditContext, DefaultRole, PermissionScope}; +use crate::runs::_helpers::requests::permissions::{authorize_execution, execution_scopes}; +use crate::runs::_helpers::requests::publication::{ + publish_request, record_authored, request_retry, +}; +use crate::runs::constants::{ADHOC_AUDIT_TAG, LAUNCHABLE_VERBS}; +use crate::runs::models::{ + AdhocCheckRequest, AdhocPermission, AdhocRunRequest, PlannerResource, RunKind, RunOrigin, + RunRequest, +}; +use crate::runs::types::CommandPlanner; +use crate::snapshots::main::job_write_set::job_write_set; +use crate::snapshots::main::read_current_record::read_current_record; +use crate::snapshots::main::read_snapshot_nodes::read_snapshot_nodes; +use crate::snapshots::models::SnapshotNode; +use crate::store::errors::ControlError; +use crate::store::main::query::query; +use crate::store::models::{ControlTransaction, Parameter}; + +/// Graph kinds in the order the CLI builds its project graph: models, sources, seeds, then functions. +const GRAPH_KINDS: [&str; 5] = ["model", "source", "seed", "udf", "table_fn"]; + +/// The frozen request and its verb, or why the command cannot run; `run_id` comes from the caller. +pub(crate) async fn frozen_adhoc_request( + transaction: ControlTransaction, + (context, planner): (&AuditContext, &dyn CommandPlanner), + (request, run_id): (&AdhocCheckRequest, Uuid), +) -> Result<(ControlTransaction, Result<(String, RunRequest), String>), ControlError> { + let (transaction, snapshot) = + read_current_record(transaction, (request.location_id, &request.target)).await?; + let Some(snapshot) = snapshot else { + return Ok(( + transaction, + Err("this target has no publication yet".into()), + )); + }; + let (transaction, nodes) = read_snapshot_nodes(transaction, snapshot.snapshot_id, None).await?; + let plan = match planner.plan(&request.command, &planner_resources(&nodes)) { + Ok(plan) => plan, + Err(refusal) => return Ok((transaction, Err(refusal))), + }; + let (transaction, writers) = + job_write_set(transaction, snapshot.snapshot_id, &plan.selection).await?; + let (transaction, images) = query( + transaction, + "SELECT image_id FROM sqb_snapshots WHERE snapshot_id=$1", + vec![Parameter::Id(snapshot.snapshot_id)], + ) + .await?; + let image_id = images + .first() + .and_then(|row| row["image_id"].as_str()) + .map(str::to_owned); + let write_set: BTreeSet = match plan.verb.as_str() { + "build" => writers, + "seed" => writers + .into_iter() + .filter(|key| key.starts_with("seed:")) + .collect(), + _ => BTreeSet::new(), + }; + Ok(( + transaction, + Ok(( + plan.verb, + RunRequest { + run_id, + location_id: request.location_id, + target: request.target.clone(), + snapshot_id: snapshot.snapshot_id, + image_id, + kind: RunKind::Adhoc, + origin: RunOrigin::Manual, + job_key: None, + parent_run_id: None, + root_run_id: run_id, + trigger_id: None, + tick_id: None, + rerun_of_run_id: None, + requested_by: context.actor.clone(), + priority: request.priority, + selection: plan.selection, + write_set, + settings: plan.settings, + window: None, + concurrency_groups: BTreeSet::new(), + }, + )), + )) +} + +/// The selector graph rebuilt from published definitions, in the CLI's kind order. +fn planner_resources( + nodes: &std::collections::BTreeMap, +) -> Vec { + let mut resources: Vec = Vec::new(); + for kind in GRAPH_KINDS { + for (key, node) in nodes { + if node.kind != kind { + continue; + } + let definition = &node.definition; + let (tags, folder) = match kind { + "model" => ( + definition.pointer("/config/tags"), + Some(model_folder(definition)), + ), + "seed" => (definition.pointer("/schema/tags"), None), + _ => (definition.get("tags"), None), + }; + resources.push(PlannerResource { + key: key.clone(), + deps: texts(definition.get("deps")), + tags: texts(tags), + folder, + }); + } + } + resources +} + +/// The model file's folder relative to the project, with `/` separators whichever platform published it. +fn model_folder(definition: &Value) -> String { + let path = definition + .get("path") + .and_then(Value::as_str) + .unwrap_or_default() + .replace('\\', "/"); + path.rsplit_once('/') + .map_or_else(|| ".".to_owned(), |(folder, _)| folder.to_owned()) +} + +fn texts(value: Option<&Value>) -> Vec { + value + .and_then(Value::as_array) + .into_iter() + .flatten() + .filter_map(|item| item.as_str().map(str::to_owned)) + .collect() +} + +/// Whether the person may view every current node, decided without recording a denial. +pub(in crate::runs) async fn may_view_target( + transaction: ControlTransaction, + principal_id: Uuid, + request: &AdhocCheckRequest, +) -> Result<(ControlTransaction, bool), ControlError> { + let (transaction, current) = query(transaction, + "SELECT n.node_key FROM sqb_snapshot_nodes n JOIN sqb_current_snapshots c ON c.snapshot_id=n.snapshot_id \ + WHERE c.location_id=$1 AND c.target=$2", + vec![Parameter::Id(request.location_id), Parameter::Text(request.target.clone())]).await?; + let view = PermissionScope { + action: Action::View, + location_id: Some(request.location_id), + target: Some(request.target.clone()), + nodes: current + .iter() + .filter_map(|row| row["node_key"].as_str().map(str::to_owned)) + .collect(), + }; + let (transaction, refusals) = + decide_person_scopes(transaction, principal_id, (&request.target, &[view])).await?; + Ok((transaction, refusals.as_deref() == Some(&[None]))) +} + +/// Every permission launching `frozen` needs, decided as the launch would but without recording a denial. +pub(in crate::runs) async fn permission_decisions( + transaction: ControlTransaction, + principal_id: Uuid, + frozen: &RunRequest, +) -> Result<(ControlTransaction, Vec), ControlError> { + let scopes = execution_scopes(frozen, std::slice::from_ref(frozen), Action::RunAdhoc); + let (transaction, refusals) = + decide_person_scopes(transaction, principal_id, (&frozen.target, &scopes)).await?; + let inactive = Some("authenticated active person required".to_owned()); + let refusals: Vec> = refusals.unwrap_or_else(|| vec![inactive; scopes.len()]); + Ok(( + transaction, + scopes + .iter() + .zip(refusals) + .map(|(scope, reason)| AdhocPermission { + action: scope.action, + allowed: reason.is_none(), + reason, + }) + .collect(), + )) +} + +/// Resolve, authorise on the exact resolved nodes, publish and audit; workers never re-resolve the caller's selectors. +pub(in crate::runs) async fn request_adhoc_run_in( + transaction: ControlTransaction, + (context, principal_id): (&AuditContext, Uuid), + (planner, request): (&dyn CommandPlanner, &AdhocRunRequest), +) -> Result<(ControlTransaction, bool), ControlError> { + let (transaction, context) = authorize_listing( + transaction, + (context, principal_id), + (request.location_id, &request.target), + DefaultRole::None, + ) + .await?; + if request.reason.trim().is_empty() || request.command.is_empty() { + return Err(ControlError::Invalid( + "an ad-hoc run needs a command and a reason".into(), + )); + } + let authored = json!(request); + let (transaction, retry) = + request_retry(transaction, &context, (request.run_id, &authored)).await?; + if retry { + return Ok((transaction, false)); + } + let check = AdhocCheckRequest { + location_id: request.location_id, + target: request.target.clone(), + command: request.command.clone(), + priority: request.priority, + }; + let (transaction, frozen) = + frozen_adhoc_request(transaction, (&context, planner), (&check, request.run_id)).await?; + let (verb, frozen) = frozen.map_err(ControlError::Invalid)?; + if !LAUNCHABLE_VERBS.contains(&verb.as_str()) { + return Err(ControlError::Invalid(format!( + "workers do not run sqb {verb} yet; only build can launch" + ))); + } + let (transaction, context) = authorize_execution( + transaction, + (&context, principal_id), + (&frozen, Action::RunAdhoc), + ) + .await?; + let (transaction, _) = publish_request(transaction, &frozen).await?; + let transaction = record_authored(transaction, &context, (request.run_id, &authored)).await?; + let transaction = append_audit( + transaction, + &context, + (Uuid::new_v5(&request.run_id, ADHOC_AUDIT_TAG), "run.adhoc_requested"), + ( + json!({"location_id": request.location_id, "target": request.target, "run_id": request.run_id}), + json!(null), + json!({"request": request, "verb": verb, "frozen": frozen}), + ), + ) + .await?; + Ok((transaction, true)) +} diff --git a/crates/sqlbuild-orchestration/src/runs/main/cancel_person_run.rs b/crates/sqlbuild-orchestration/src/runs/_helpers/requests/cancel.rs similarity index 98% rename from crates/sqlbuild-orchestration/src/runs/main/cancel_person_run.rs rename to crates/sqlbuild-orchestration/src/runs/_helpers/requests/cancel.rs index 9ab7039ba1..47413fa054 100644 --- a/crates/sqlbuild-orchestration/src/runs/main/cancel_person_run.rs +++ b/crates/sqlbuild-orchestration/src/runs/_helpers/requests/cancel.rs @@ -13,7 +13,7 @@ use crate::store::errors::ControlError; use crate::store::main::{begin::begin, commit::commit, query::query}; use crate::store::models::{ControlStore, Parameter, TransactionMode}; -pub async fn cancel_person_run( +pub(crate) async fn cancel_person_run( store: &ControlStore, credentials: (&AuditContext, Uuid), request: &CancelRunRequest, diff --git a/crates/sqlbuild-orchestration/src/runs/_helpers/requests/mod.rs b/crates/sqlbuild-orchestration/src/runs/_helpers/requests/mod.rs index fbd365703f..2231ab538f 100644 --- a/crates/sqlbuild-orchestration/src/runs/_helpers/requests/mod.rs +++ b/crates/sqlbuild-orchestration/src/runs/_helpers/requests/mod.rs @@ -1,3 +1,5 @@ +pub(in crate::runs) mod adhoc; +pub(in crate::runs) mod cancel; pub(in crate::runs) mod job; pub(in crate::runs) mod manual_job; pub(in crate::runs) mod manual_rerun; diff --git a/crates/sqlbuild-orchestration/src/runs/_helpers/requests/permissions.rs b/crates/sqlbuild-orchestration/src/runs/_helpers/requests/permissions.rs index 479db66fa7..9ee5c9fd27 100644 --- a/crates/sqlbuild-orchestration/src/runs/_helpers/requests/permissions.rs +++ b/crates/sqlbuild-orchestration/src/runs/_helpers/requests/permissions.rs @@ -1,5 +1,7 @@ //! Person authority covers the effective frozen execution, including sensitive group children. +use std::collections::BTreeSet; + use uuid::Uuid; use crate::authorization::main::authorize_person::authorize_person; @@ -9,6 +11,24 @@ use crate::runs::models::{RunKind, RunRequest}; use crate::store::errors::ControlError; use crate::store::models::ControlTransaction; +/// Settings that override incremental windows; any of them rewrites history. +const CURSOR_SETTINGS: [&str; 6] = [ + "start_cursor", + "end_cursor", + "start_cursor_ts", + "end_cursor_ts", + "start_cursor_int", + "end_cursor_int", +]; + +/// Confirmations of destructive changes an ad-hoc command may set; a published job's authored settings keep job.run. +const ADHOC_DESTRUCTIVE_SETTINGS: [&str; 4] = [ + "allow_snapshot_full_refresh", + "allow_snapshot_schema_change", + "allow_table_type_downgrade", + "allow_retention_decrease", +]; + pub(crate) async fn authorize_execution( mut transaction: ControlTransaction, (context, credentials): (&AuditContext, Uuid), @@ -18,24 +38,8 @@ pub(crate) async fn authorize_execution( if request.kind == RunKind::Group { (transaction, executions) = frozen_group_children(transaction, request).await?; } - let mut scope = PermissionScope { - action, - location_id: Some(request.location_id), - target: Some(request.target.clone()), - nodes: executions - .iter() - .flat_map(|run| run.write_set.iter().cloned()) - .collect(), - }; - let (mut transaction, mut context) = authorize_person( - transaction, - (context, credentials), - &scope, - DefaultRole::None, - ) - .await?; - if action == Action::RunRerun && request.kind == RunKind::Adhoc { - scope.action = Action::RunAdhoc; + let mut context = context.clone(); + for scope in execution_scopes(request, &executions, action) { (transaction, context) = authorize_person( transaction, (&context, credentials), @@ -44,39 +48,67 @@ pub(crate) async fn authorize_execution( ) .await?; } - if matches!(action, Action::JobRun | Action::RunRerun) - && (request.window.is_some() - || executions.iter().any(|run| { - run.settings - .get("full_refresh") - .and_then(serde_json::Value::as_bool) - == Some(true) - || run.settings.contains_key("start_cursor") - || run.settings.contains_key("end_cursor") - || run.settings.contains_key("start_cursor_ts") - || run.settings.contains_key("end_cursor_ts") - || run.settings.contains_key("start_cursor_int") - || run.settings.contains_key("end_cursor_int") - })) - { - scope.action = Action::RunRewriteHistory; - (transaction, context) = authorize_person( - transaction, - (&context, credentials), - &scope, - DefaultRole::None, - ) - .await?; + Ok((transaction, context)) +} + +/// Every scope `action` on `request` needs, in check order; pure, so a dry run decides the same scopes without auditing. +pub(crate) fn execution_scopes( + request: &RunRequest, + executions: &[RunRequest], + action: Action, +) -> Vec { + let nodes: BTreeSet = if request.kind == RunKind::Adhoc { + request.selection.clone() + } else { + executions + .iter() + .flat_map(|run| run.write_set.iter().cloned()) + .collect() + }; + let launches = matches!(action, Action::JobRun | Action::RunRerun | Action::RunAdhoc); + let adhoc = request.kind == RunKind::Adhoc; + let mut actions: Vec = vec![action]; + if action == Action::RunRerun && adhoc { + actions.push(Action::RunAdhoc); } - if matches!(action, Action::JobRun | Action::RunRerun) && request.priority > 0 { - scope.action = Action::RunPrioritise; - (transaction, context) = authorize_person( - transaction, - (&context, credentials), - &scope, - DefaultRole::None, - ) - .await?; + if launches && rewrites_history(request, executions) { + actions.push(Action::RunRewriteHistory); } - Ok((transaction, context)) + if launches && adhoc && request.settings.contains_key("warehouse") { + actions.push(Action::RunWarehouse); + } + if launches && request.priority > 0 { + actions.push(Action::RunPrioritise); + } + actions + .into_iter() + .map(|action| PermissionScope { + action, + location_id: Some(request.location_id), + target: Some(request.target.clone()), + nodes: nodes.clone(), + }) + .collect() +} + +fn rewrites_history(request: &RunRequest, executions: &[RunRequest]) -> bool { + if request.window.is_some() { + return true; + } + for run in executions { + let mut names: Vec<&str> = vec!["full_refresh"]; + if run.kind == RunKind::Adhoc { + names.extend(ADHOC_DESTRUCTIVE_SETTINGS); + } + let enabled = names + .iter() + .any(|name| run.settings.get(*name).and_then(serde_json::Value::as_bool) == Some(true)); + let cursor = CURSOR_SETTINGS + .iter() + .any(|name| run.settings.contains_key(*name)); + if enabled || cursor { + return true; + } + } + false } diff --git a/crates/sqlbuild-orchestration/src/runs/constants.rs b/crates/sqlbuild-orchestration/src/runs/constants.rs index 4d273c9a15..036eccdf10 100644 --- a/crates/sqlbuild-orchestration/src/runs/constants.rs +++ b/crates/sqlbuild-orchestration/src/runs/constants.rs @@ -14,3 +14,8 @@ pub(crate) const RECALL_INVOCATION_ID_BYTES: usize = 256; /// Manual requests derive their audit event ID from the run ID so retries find the original. pub(crate) const MANUAL_JOB_AUDIT_TAG: &[u8] = b"manual-job-request"; pub(crate) const RERUN_AUDIT_TAG: &[u8] = b"rerun-request"; +/// Verbs a worker runs today; other verbs parse, resolve and authorise but are refused at launch. +pub(crate) const LAUNCHABLE_VERBS: [&str; 1] = ["build"]; + +/// Ad-hoc requests derive their audit event ID from the run ID so retries find the original. +pub(crate) const ADHOC_AUDIT_TAG: &[u8] = b"adhoc-run-request"; diff --git a/crates/sqlbuild-orchestration/src/runs/main/check_adhoc_run.rs b/crates/sqlbuild-orchestration/src/runs/main/check_adhoc_run.rs new file mode 100644 index 0000000000..25ed102225 --- /dev/null +++ b/crates/sqlbuild-orchestration/src/runs/main/check_adhoc_run.rs @@ -0,0 +1,64 @@ +//! A dry run resolves and decides a command exactly as a launch would, but queues and audits nothing. + +use uuid::Uuid; + +use crate::credentials::models::AuditContext; +use crate::runs::_helpers::requests::adhoc::{ + frozen_adhoc_request, may_view_target, permission_decisions, +}; +use crate::runs::constants::LAUNCHABLE_VERBS; +use crate::runs::models::{AdhocCheck, AdhocCheckRequest}; +use crate::runs::types::CommandPlanner; +use crate::store::errors::ControlError; +use crate::store::main::{begin::begin, commit::commit}; +use crate::store::models::{ControlStore, TransactionMode}; + +/// Without view on every current node the dry run reveals nothing, and it never records a denial. +pub async fn check_adhoc_run( + store: &ControlStore, + (context, principal_id): (&AuditContext, Uuid), + planner: &dyn CommandPlanner, + request: &AdhocCheckRequest, +) -> Result { + let transaction = begin(store, TransactionMode::Read).await?; + let (transaction, viewing) = may_view_target(transaction, principal_id, request).await?; + let (transaction, frozen) = if viewing { + frozen_adhoc_request(transaction, (context, planner), (request, Uuid::nil())).await? + } else { + ( + transaction, + Err("view on every node of this target is required".to_owned()), + ) + }; + let (verb, frozen) = match frozen { + Ok(frozen) => frozen, + Err(refusal) => { + commit(transaction).await?; + return Ok(AdhocCheck { + snapshot_id: None, + verb: None, + selection: Vec::new(), + write_set: Vec::new(), + settings: Default::default(), + permissions: Vec::new(), + error: Some(refusal), + launchable: false, + }); + } + }; + let (transaction, permissions) = + permission_decisions(transaction, principal_id, &frozen).await?; + commit(transaction).await?; + let error = (!LAUNCHABLE_VERBS.contains(&verb.as_str())) + .then(|| format!("workers do not run sqb {verb} yet; only build can launch")); + Ok(AdhocCheck { + snapshot_id: Some(frozen.snapshot_id), + launchable: error.is_none() && permissions.iter().all(|permission| permission.allowed), + verb: Some(verb), + selection: frozen.selection.into_iter().collect(), + write_set: frozen.write_set.into_iter().collect(), + settings: frozen.settings, + permissions, + error, + }) +} diff --git a/crates/sqlbuild-orchestration/src/runs/main/mod.rs b/crates/sqlbuild-orchestration/src/runs/main/mod.rs index ce37545bfb..a062e002e1 100644 --- a/crates/sqlbuild-orchestration/src/runs/main/mod.rs +++ b/crates/sqlbuild-orchestration/src/runs/main/mod.rs @@ -1,6 +1,6 @@ pub mod advance_groups; -pub mod cancel_person_run; pub mod change_run; +pub mod check_adhoc_run; pub mod claim_next_run; pub(crate) mod frozen_job_request; pub mod heartbeat_run; @@ -15,6 +15,6 @@ pub mod publish_run_output; pub mod publish_run_recall; pub(crate) mod queue_run; pub mod request_job_run; -pub mod request_person_job_run; +pub mod request_person_run; pub mod request_rerun; pub mod run_execution; diff --git a/crates/sqlbuild-orchestration/src/runs/main/request_person_job_run.rs b/crates/sqlbuild-orchestration/src/runs/main/request_person_job_run.rs deleted file mode 100644 index 60259dd4e5..0000000000 --- a/crates/sqlbuild-orchestration/src/runs/main/request_person_job_run.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! Browser callers cannot take the trusted local-owner path or supply an execution write set. - -use uuid::Uuid; - -use crate::credentials::models::AuditContext; -use crate::runs::_helpers::requests::manual_job::request_job_run_in; -use crate::runs::models::JobRunRequest; -use crate::store::errors::ControlError; -use crate::store::main::write::write; -use crate::store::models::ControlStore; - -pub async fn request_person_job_run( - store: &ControlStore, - (context, credentials): (&AuditContext, Uuid), - request: &JobRunRequest, -) -> Result { - write(store, |transaction| { - request_job_run_in(transaction, (context, Some(credentials)), request) - }) - .await -} diff --git a/crates/sqlbuild-orchestration/src/runs/main/request_person_run.rs b/crates/sqlbuild-orchestration/src/runs/main/request_person_run.rs new file mode 100644 index 0000000000..c88868b7e0 --- /dev/null +++ b/crates/sqlbuild-orchestration/src/runs/main/request_person_run.rs @@ -0,0 +1,37 @@ +//! Browser callers cannot take the trusted local-owner path or supply an execution write set. + +use uuid::Uuid; + +use crate::credentials::models::AuditContext; +use crate::runs::_helpers::requests::adhoc::request_adhoc_run_in; +use crate::runs::_helpers::requests::cancel::cancel_person_run; +use crate::runs::_helpers::requests::manual_job::request_job_run_in; +use crate::runs::models::PersonRunRequest; +use crate::store::errors::ControlError; +use crate::store::main::write::write; +use crate::store::models::ControlStore; + +/// A person queues a published job or an ad-hoc command, or cancels a run; each is authorised on the nodes it covers. +pub async fn request_person_run( + store: &ControlStore, + (context, credentials): (&AuditContext, Uuid), + request: PersonRunRequest<'_>, +) -> Result { + match request { + PersonRunRequest::Cancel(request) => { + cancel_person_run(store, (context, credentials), request).await + } + PersonRunRequest::Job(request) => { + write(store, |transaction| async move { + request_job_run_in(transaction, (context, Some(credentials)), request).await + }) + .await + } + PersonRunRequest::Adhoc(planner, request) => { + write(store, |transaction| async move { + request_adhoc_run_in(transaction, (context, credentials), (planner, request)).await + }) + .await + } + } +} diff --git a/crates/sqlbuild-orchestration/src/runs/mod.rs b/crates/sqlbuild-orchestration/src/runs/mod.rs index 529a59766a..40b3232c61 100644 --- a/crates/sqlbuild-orchestration/src/runs/mod.rs +++ b/crates/sqlbuild-orchestration/src/runs/mod.rs @@ -2,6 +2,7 @@ pub(crate) mod _helpers; pub(crate) mod constants; pub mod main; pub mod models; +pub mod types; #[cfg(test)] #[path = "../../tests/runs/test_contracts.rs"] diff --git a/crates/sqlbuild-orchestration/src/runs/models.rs b/crates/sqlbuild-orchestration/src/runs/models.rs index 545660e12d..fa6239072a 100644 --- a/crates/sqlbuild-orchestration/src/runs/models.rs +++ b/crates/sqlbuild-orchestration/src/runs/models.rs @@ -6,6 +6,9 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; +use crate::credentials::models::Action; +use crate::runs::types::CommandPlanner; + #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "snake_case")] pub enum RunKind { @@ -451,3 +454,72 @@ pub struct RunObservation { pub last_heartbeat_at: Option>, pub observed_at: DateTime, } + +/// A person's command as argument tokens after `sqb`, never shell text; selectors resolve on the server. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(deny_unknown_fields)] +pub struct AdhocRunRequest { + pub run_id: Uuid, + pub location_id: Uuid, + pub target: String, + pub command: Vec, + pub reason: String, + pub priority: i64, +} + +/// A dry run of a command; nothing is queued or audited. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(deny_unknown_fields)] +pub struct AdhocCheckRequest { + pub location_id: Uuid, + pub target: String, + pub command: Vec, + pub priority: i64, +} + +/// One graph resource selectors resolve over: its `kind:name` key, upstream keys, tags and model folder. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PlannerResource { + pub key: String, + pub deps: Vec, + pub tags: Vec, + pub folder: Option, +} + +/// A parsed command: its verb, the exact node keys it selects and the flags it sets, by parser destination. +#[derive(Clone, Debug, PartialEq)] +pub struct CommandPlan { + pub verb: String, + pub selection: BTreeSet, + pub settings: BTreeMap, +} + +/// One permission the run needs and whether the person holds it on every resolved node. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] +pub struct AdhocPermission { + pub action: Action, + pub allowed: bool, + /// Why it is refused, as the launch would report it. + pub reason: Option, +} + +/// What launching the command would run and whether this person may launch it. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, utoipa::ToSchema)] +pub struct AdhocCheck { + pub snapshot_id: Option, + pub verb: Option, + pub selection: Vec, + pub write_set: Vec, + pub settings: BTreeMap, + pub permissions: Vec, + /// Why the command cannot run regardless of permissions, such as a refused flag or an unknown selector. + pub error: Option, + pub launchable: bool, +} + +/// A person's change to the run queue: a published job, a command the planner resolves on the server, or a cancellation. +pub enum PersonRunRequest<'a> { + Job(&'a JobRunRequest), + Adhoc(&'a dyn CommandPlanner, &'a AdhocRunRequest), + Cancel(&'a CancelRunRequest), +} diff --git a/crates/sqlbuild-orchestration/src/runs/types.rs b/crates/sqlbuild-orchestration/src/runs/types.rs new file mode 100644 index 0000000000..fba6ad6c7f --- /dev/null +++ b/crates/sqlbuild-orchestration/src/runs/types.rs @@ -0,0 +1,19 @@ +//! The command planner the server is built with; the CLI owns the grammar and the selector resolver. + +use crate::runs::models::{CommandPlan, PlannerResource}; + +/// Parse `command` with the CLI's grammar and resolve its selectors over `resources`; pure and side-effect free. +pub trait CommandPlanner: Send + Sync { + /// The verb, exact node keys and validated settings, or why the command is refused. + fn plan( + &self, + command: &[String], + resources: &[PlannerResource], + ) -> Result; +} + +/// What the API hands runs to workers with: their Docker images and the command planner. +pub type RunHandoff = ( + Vec, + std::sync::Arc, +); diff --git a/crates/sqlbuild-orchestration/tests/api/helpers.rs b/crates/sqlbuild-orchestration/tests/api/helpers.rs index daa60882a2..a8e654e190 100644 --- a/crates/sqlbuild-orchestration/tests/api/helpers.rs +++ b/crates/sqlbuild-orchestration/tests/api/helpers.rs @@ -3,6 +3,7 @@ use std::time::Duration; use crate::api::main::serve_api::serve_api; use crate::api::models::ApiAuthentication; use crate::api::tests::test_types::TestApi; +use crate::runs::tests::test_types::ExactKeyPlanner; use crate::store::models::ControlStore; pub(crate) async fn start_api(store: &ControlStore) -> TestApi { @@ -22,7 +23,11 @@ pub(crate) async fn start_api_with_authentication( let task = tokio::spawn(serve_api( store, listener, - (authentication, Vec::new(), Default::default()), + ( + authentication, + (Vec::new(), std::sync::Arc::new(ExactKeyPlanner)), + Default::default(), + ), (|| {}, async { let _ = signal.await; }), diff --git a/crates/sqlbuild-orchestration/tests/runs/helpers.rs b/crates/sqlbuild-orchestration/tests/runs/helpers.rs index 2b27de9ba7..4594bf388d 100644 --- a/crates/sqlbuild-orchestration/tests/runs/helpers.rs +++ b/crates/sqlbuild-orchestration/tests/runs/helpers.rs @@ -2,14 +2,25 @@ use std::collections::{BTreeMap, BTreeSet}; +use crate::api::errors::ApiError; +use crate::authorization::main::assign_builtin_role::assign_builtin_role; +use crate::authorization::main::bootstrap_user::bootstrap_user; +use crate::credentials::main::identities::create_principal::create_principal; +use crate::credentials::models::{AuditContext, BuiltinRole, RoleAssignmentRequest}; +use crate::credentials::tests::location_helpers::create_location; use crate::runs::_helpers::requests::publication::publish_request; -use crate::runs::models::{RunKind, RunOrigin, RunRequest}; +use crate::runs::models::{ + AdhocCheck, AdhocCheckRequest, AdhocRunRequest, RunKind, RunOrigin, RunRequest, +}; +use crate::runs::tests::test_types::{AdhocCommandTestCase, AdhocPeople}; +use crate::snapshots::main::publish_snapshot::publish_snapshot; use crate::snapshots::models::{SnapshotInput, SnapshotNode}; use crate::store::errors::ControlError; use crate::store::main::begin::begin; use crate::store::main::commit::commit; -use crate::store::models::{ControlStore, TransactionMode}; -use serde_json::json; +use crate::store::main::query::query; +use crate::store::models::{ControlStore, Parameter, TransactionMode}; +use serde_json::{Value, json}; use uuid::Uuid; pub(crate) async fn request_run( @@ -102,3 +113,162 @@ pub(crate) async fn queue(store: &ControlStore, request: &RunRequest) { "a reused run ID cannot change its request" ); } + +/// Rows of `sql` as JSON, read in their own transaction. +pub(crate) async fn rows( + store: &ControlStore, + sql: &str, + parameters: Vec, +) -> Vec { + let transaction = begin(store, TransactionMode::Read).await.expect("read"); + let (transaction, rows) = query(transaction, sql, parameters).await.expect("query"); + commit(transaction).await.expect("commit"); + rows.into_iter().map(|row| json!(row)).collect() +} + +/// A JSON column, which SQLite returns as text and PostgreSQL as JSON. +pub(crate) fn json_column(value: &Value) -> Value { + value.as_str().map_or(value.clone(), |text| { + serde_json::from_str(text).expect("JSON column") + }) +} + +/// A publication of two models, a seed and a source, with one person per role under test. +pub(crate) async fn adhoc_fixture( + store: &ControlStore, + context: &AuditContext, +) -> (Uuid, Uuid, AdhocPeople) { + let location_id = create_location(store, "orders", "test-owner") + .await + .expect("location"); + let mut input = snapshot(location_id); + input.nodes = [ + "model:orders", + "model:customers", + "seed:currencies", + "source:shop.orders", + ] + .into_iter() + .map(published_node) + .collect(); + let published = publish_snapshot(store, &input).await.expect("publish"); + let admin = bootstrap_user(store, context, "orders-admin") + .await + .expect("administrator"); + let developer = person_with( + store, + (context, admin), + "orders-developer", + &[BuiltinRole::Developer], + ) + .await; + let operator = person_with( + store, + (context, admin), + "orders-operator", + &[BuiltinRole::Operator], + ) + .await; + let stranger = person_with(store, (context, admin), "orders-stranger", &[]).await; + ( + location_id, + published.snapshot_id, + AdhocPeople { + admin, + developer, + operator, + stranger, + }, + ) +} + +fn published_node(key: &str) -> (String, SnapshotNode) { + let kind = key.split_once(':').expect("kind").0; + ( + key.to_owned(), + SnapshotNode { + kind: kind.into(), + definition: json!({"materialization": "table"}), + raw_sql: None, + compiled_sql: Some("select 1 as id".into()), + python_source: None, + }, + ) +} + +async fn person_with( + store: &ControlStore, + (context, admin): (&AuditContext, Uuid), + name: &str, + roles: &[BuiltinRole], +) -> Uuid { + let person = create_principal(store, context, name) + .await + .expect("person"); + for role in roles.iter().copied() { + assign_builtin_role( + store, + (context, admin), + &RoleAssignmentRequest { + principal_id: person, + role: role.into(), + location_id: None, + target: None, + revoke: false, + }, + ) + .await + .expect("assign role"); + } + person +} + +/// The case's command as a dry run in the fixture's location. +pub(crate) fn adhoc_check( + location_id: Uuid, + test_case: &AdhocCommandTestCase, +) -> AdhocCheckRequest { + AdhocCheckRequest { + location_id, + target: "development".into(), + command: test_case + .command + .iter() + .map(|token| (*token).to_owned()) + .collect(), + priority: test_case.priority, + } +} + +/// The case's command as a launch with its own run ID. +pub(crate) fn adhoc_launch( + location_id: Uuid, + test_case: &AdhocCommandTestCase, + run_id: Uuid, +) -> AdhocRunRequest { + let check = adhoc_check(location_id, test_case); + AdhocRunRequest { + run_id, + location_id, + target: check.target, + command: check.command, + reason: test_case.reason.into(), + priority: test_case.priority, + } +} + +/// What a dry run decided, in the shape the cases name it. +pub(crate) fn check_projection(check: &AdhocCheck) -> Value { + json!({ + "selection": check.selection, + "write_set": check.write_set, + "permissions": check.permissions.iter().map(|permission| json!([permission.action, permission.allowed])).collect::>(), + "error": check.error, + "launchable": check.launchable, + }) +} + +/// Whether a launch applied, or the HTTP status its refusal reaches people with. +pub(crate) fn launch_outcome(result: Result) -> Result { + result.map_err(|error| ApiError::from(error).status.as_u16()) +} diff --git a/crates/sqlbuild-orchestration/tests/runs/test_adhoc.rs b/crates/sqlbuild-orchestration/tests/runs/test_adhoc.rs new file mode 100644 index 0000000000..7cf277f638 --- /dev/null +++ b/crates/sqlbuild-orchestration/tests/runs/test_adhoc.rs @@ -0,0 +1,264 @@ +//! Ad-hoc commands resolve on the server, need their own permission per gated flag and dry runs record nothing. + +use serde_json::{Value, json}; +use uuid::Uuid; + +use crate::credentials::models::AuditContext; +use crate::runs::main::check_adhoc_run::check_adhoc_run; +use crate::runs::main::request_person_run::request_person_run; +use crate::runs::models::PersonRunRequest; +use crate::runs::tests::helpers::{ + adhoc_check, adhoc_fixture, adhoc_launch, check_projection, json_column, launch_outcome, rows, +}; +use crate::runs::tests::test_types::{AdhocCommandTestCase, AdhocPerson, ExactKeyPlanner}; +use crate::store::main::migrate_store::migrate_store; +use crate::store::models::Parameter; +use crate::store::tests::store_helpers::{close_test_store, open_test_store, run}; +use crate::store::tests::store_test_types::Backend; + +const AUDIT: &str = "SELECT event_id FROM sqb_security_audit"; + +#[test] +fn given_ad_hoc_commands_when_people_check_and_launch_them_then_each_gated_flag_needs_its_own_permission() + { + let test_cases = [ + AdhocCommandTestCase { + description: "run.adhoc covers the resolved selection, not only its writers", + person: AdhocPerson::Developer, + command: &["build", "model:orders", "source:shop.orders"], + priority: 0, + reason: "Refresh orders", + expected_check: json!({"selection": ["model:orders", "source:shop.orders"], "write_set": ["model:orders"], + "permissions": [["run.adhoc", true]], "error": null, "launchable": true}), + expected_launch: Ok(true), + }, + AdhocCommandTestCase { + description: "every gated flag is its own permission, and Developer holds none of them", + person: AdhocPerson::Developer, + command: &[ + "build", + "model:orders", + "--full-refresh", + "--warehouse", + "BUILD_XS", + ], + priority: 1, + reason: "Rebuild orders now", + expected_check: json!({"selection": ["model:orders"], "write_set": ["model:orders"], + "permissions": [["run.adhoc", true], ["run.rewrite_history", false], ["run.warehouse", false], ["run.prioritise", false]], + "error": null, "launchable": false}), + expected_launch: Err(403), + }, + AdhocCommandTestCase { + description: "Developer lacks run.rewrite_history alone", + person: AdhocPerson::Developer, + command: &["build", "model:orders", "--full-refresh"], + priority: 0, + reason: "Rebuild orders", + expected_check: json!({"selection": ["model:orders"], "write_set": ["model:orders"], + "permissions": [["run.adhoc", true], ["run.rewrite_history", false]], "error": null, "launchable": false}), + expected_launch: Err(403), + }, + AdhocCommandTestCase { + description: "Developer lacks run.warehouse alone", + person: AdhocPerson::Developer, + command: &["build", "model:orders", "--warehouse", "BUILD_XS"], + priority: 0, + reason: "Refresh orders", + expected_check: json!({"selection": ["model:orders"], "write_set": ["model:orders"], + "permissions": [["run.adhoc", true], ["run.warehouse", false]], "error": null, "launchable": false}), + expected_launch: Err(403), + }, + AdhocCommandTestCase { + description: "Developer lacks run.prioritise alone", + person: AdhocPerson::Developer, + command: &["build", "model:orders"], + priority: 1, + reason: "Refresh orders", + expected_check: json!({"selection": ["model:orders"], "write_set": ["model:orders"], + "permissions": [["run.adhoc", true], ["run.prioritise", false]], "error": null, "launchable": false}), + expected_launch: Err(403), + }, + AdhocCommandTestCase { + description: "Admin holds every gated permission", + person: AdhocPerson::Admin, + command: &[ + "build", + "model:orders", + "--full-refresh", + "--warehouse", + "BUILD_XS", + ], + priority: 1, + reason: "Rebuild orders now", + expected_check: json!({"selection": ["model:orders"], "write_set": ["model:orders"], + "permissions": [["run.adhoc", true], ["run.rewrite_history", true], ["run.warehouse", true], ["run.prioritise", true]], + "error": null, "launchable": true}), + expected_launch: Ok(true), + }, + AdhocCommandTestCase { + description: "Operator may run jobs but not ad-hoc commands", + person: AdhocPerson::Operator, + command: &["build", "model:orders"], + priority: 0, + reason: "Refresh orders", + expected_check: json!({"selection": ["model:orders"], "write_set": ["model:orders"], + "permissions": [["run.adhoc", false]], "error": null, "launchable": false}), + expected_launch: Err(403), + }, + AdhocCommandTestCase { + description: "without view the dry run reveals no node, and no role is the default", + person: AdhocPerson::Stranger, + command: &["build", "model:orders"], + priority: 0, + reason: "Refresh orders", + expected_check: json!({"selection": [], "write_set": [], "permissions": [], + "error": "view on every node of this target is required", "launchable": false}), + expected_launch: Err(403), + }, + AdhocCommandTestCase { + description: "verbs workers cannot run yet are refused at launch", + person: AdhocPerson::Developer, + command: &["test", "model:orders"], + priority: 0, + reason: "Check orders", + expected_check: json!({"selection": ["model:orders"], "write_set": [], "permissions": [["run.adhoc", true]], + "error": "workers do not run sqb test yet; only build can launch", "launchable": false}), + expected_launch: Err(400), + }, + AdhocCommandTestCase { + description: "a refused flag is refused before any permission", + person: AdhocPerson::Admin, + command: &["build", "--vars"], + priority: 0, + reason: "Refresh orders", + expected_check: json!({"selection": [], "write_set": [], "permissions": [], + "error": "--vars is not available for ad-hoc runs", "launchable": false}), + expected_launch: Err(400), + }, + AdhocCommandTestCase { + description: "a launch needs a reason, which the dry run does not", + person: AdhocPerson::Admin, + command: &["build", "model:orders"], + priority: 0, + reason: " ", + expected_check: json!({"selection": ["model:orders"], "write_set": ["model:orders"], + "permissions": [["run.adhoc", true]], "error": null, "launchable": true}), + expected_launch: Err(400), + }, + ]; + for backend in [Backend::Sqlite, Backend::Postgres] { + run(async { + let Some(fixture) = open_test_store(backend).await else { + return; + }; + let store = &fixture.store; + migrate_store(store).await.expect("migrate"); + let context = AuditContext { + actor: "local-control-store-owner".into(), + granting_role: "control-store-owner".into(), + request_id: Uuid::now_v7(), + ip_address: None, + }; + let (location_id, snapshot_id, people) = adhoc_fixture(store, &context).await; + let mut run_ids: Vec = Vec::new(); + for test_case in &test_cases { + let run_id = Uuid::now_v7(); + run_ids.push(run_id); + let person = people.of(test_case.person); + let audited = rows(store, AUDIT, Vec::new()).await.len(); + let check = check_adhoc_run( + store, + (&context, person), + &ExactKeyPlanner, + &adhoc_check(location_id, test_case), + ) + .await + .expect("dry run"); + assert_eq!( + ( + check_projection(&check), + rows(store, AUDIT, Vec::new()).await.len() + ), + (test_case.expected_check.clone(), audited), + "{backend:?}: {}; a dry run records nothing", + test_case.description + ); + let launched = request_person_run( + store, + (&context, person), + PersonRunRequest::Adhoc( + &ExactKeyPlanner, + &adhoc_launch(location_id, test_case, run_id), + ), + ) + .await; + assert_eq!( + launch_outcome(launched), + test_case.expected_launch, + "{backend:?}: {}", + test_case.description + ); + } + let retried = request_person_run( + store, + (&context, people.of(test_cases[0].person)), + PersonRunRequest::Adhoc( + &ExactKeyPlanner, + &adhoc_launch(location_id, &test_cases[0], run_ids[0]), + ), + ) + .await; + assert_eq!( + launch_outcome(retried), + Ok(false), + "{backend:?}: a retry of the same run ID is not a second run" + ); + let frozen = rows( + store, + "SELECT request_json FROM sqb_runs WHERE run_id=$1", + vec![Parameter::Id(run_ids[0])], + ) + .await; + let frozen = json_column(&frozen[0]["request_json"]); + assert_eq!( + [ + &frozen["kind"], + &frozen["selection"], + &frozen["write_set"], + &frozen["snapshot_id"] + ], + [ + &json!("adhoc"), + &json!(["model:orders", "source:shop.orders"]), + &json!(["model:orders"]), + &json!(snapshot_id) + ], + "{backend:?}: the worker receives the exact resolved selection of the pinned publication" + ); + let audit = rows( + store, + "SELECT after_json FROM sqb_security_audit WHERE action='run.adhoc_requested'", + Vec::new(), + ) + .await; + assert_eq!( + audit + .iter() + .map(|row| json_column(&row["after_json"])) + .map(|event| json!([ + event["verb"], + event["frozen"]["settings"], + event["request"]["reason"] + ])) + .collect::>(), + vec![ + json!(["build", {}, "Refresh orders"]), + json!(["build", {"full_refresh": true, "warehouse": "BUILD_XS"}, "Rebuild orders now"]), + ], + "{backend:?}: each launch is audited once with its command, reason and warehouse" + ); + close_test_store(fixture).await; + }); + } +} diff --git a/crates/sqlbuild-orchestration/tests/runs/test_contracts.rs b/crates/sqlbuild-orchestration/tests/runs/test_contracts.rs index ddc82ea434..0c5abb3f2d 100644 --- a/crates/sqlbuild-orchestration/tests/runs/test_contracts.rs +++ b/crates/sqlbuild-orchestration/tests/runs/test_contracts.rs @@ -6,6 +6,8 @@ pub(crate) mod helpers; mod monitor_helpers; #[path = "termination/helpers.rs"] pub(crate) mod terminal_helpers; +#[path = "test_adhoc.rs"] +mod test_adhoc; #[path = "test_checks.rs"] mod test_checks; #[path = "test_claims.rs"] @@ -29,4 +31,4 @@ mod test_requests; #[path = "test_reruns.rs"] mod test_reruns; #[path = "test_types.rs"] -mod test_types; +pub(crate) mod test_types; diff --git a/crates/sqlbuild-orchestration/tests/runs/test_permissions.rs b/crates/sqlbuild-orchestration/tests/runs/test_permissions.rs index 6e29e1d3fc..f6d4c726fb 100644 --- a/crates/sqlbuild-orchestration/tests/runs/test_permissions.rs +++ b/crates/sqlbuild-orchestration/tests/runs/test_permissions.rs @@ -17,12 +17,13 @@ use crate::credentials::models::{ }; use crate::credentials::tests::location_helpers::create_location; use crate::runs::main::{ - advance_groups::advance_groups, cancel_person_run::cancel_person_run, change_run::change_run, - list_person_runs::list_person_runs, list_runs::list_runs, request_job_run::request_job_run, - request_person_job_run::request_person_job_run, request_rerun::request_rerun, + advance_groups::advance_groups, change_run::change_run, list_person_runs::list_person_runs, + list_runs::list_runs, request_job_run::request_job_run, request_person_run::request_person_run, + request_rerun::request_rerun, }; use crate::runs::models::{ - CancelRunRequest, JobRunRequest, RerunRequest, RunAuthority, RunChange, RunChangeRequest, + CancelRunRequest, JobRunRequest, PersonRunRequest, RerunRequest, RunAuthority, RunChange, + RunChangeRequest, }; use crate::runs::tests::helpers::{request_run, snapshot}; use crate::runs::tests::test_types::PersonRunPermissionTestCase; @@ -142,7 +143,12 @@ fn given_scoped_person_roles_when_jobs_request_sensitive_settings_then_only_admi }; assert!( matches!( - request_person_job_run(store, (&context, credentials), &request).await, + request_person_run( + store, + (&context, credentials), + PersonRunRequest::Job(&request) + ) + .await, Err(ControlError::Denied { .. }) ), "no role is the default" @@ -180,14 +186,14 @@ fn given_scoped_person_roles_when_jobs_request_sensitive_settings_then_only_admi .expect("leader"); let mut reruns = Vec::new(); for job_key in ["daily_orders", "orders_group"] { - let result = request_person_job_run( + let result = request_person_run( store, (&context, credentials), - &JobRunRequest { + PersonRunRequest::Job(&JobRunRequest { run_id: Uuid::now_v7(), job_key: job_key.into(), ..request.clone() - }, + }), ) .await; assert_eq!( @@ -223,9 +229,13 @@ fn given_scoped_person_roles_when_jobs_request_sensitive_settings_then_only_admi let can_cancel = test_case.role != BuiltinRole::Viewer; for expected in [true, false] { assert_eq!( - cancel_person_run(store, (&context, credentials), &cancellation) - .await - .ok(), + request_person_run( + store, + (&context, credentials), + PersonRunRequest::Cancel(&cancellation), + ) + .await + .ok(), can_cancel.then_some(expected), "cancellation permits stopping even sensitive or prioritised work" ); @@ -302,14 +312,14 @@ fn given_scoped_person_roles_when_jobs_request_sensitive_settings_then_only_admi request_run(store, &adhoc) .await .expect("owner requests ad-hoc execution"); - cancel_person_run( + request_person_run( store, (&context, sessions[0].identity.principal_id), - &CancelRunRequest { + PersonRunRequest::Cancel(&CancelRunRequest { run_id: adhoc.run_id, event_id: Uuid::now_v7(), reason: "Stop ad-hoc source".into(), - }, + }), ) .await .expect("administrator stops ad-hoc execution"); @@ -376,13 +386,13 @@ fn given_scoped_person_roles_when_jobs_request_sensitive_settings_then_only_admi ); assert!( matches!( - request_person_job_run( + request_person_run( store, (&context, credentials), - &JobRunRequest { + PersonRunRequest::Job(&JobRunRequest { run_id: Uuid::now_v7(), ..request - } + }) ) .await, Err(ControlError::Denied { .. }) diff --git a/crates/sqlbuild-orchestration/tests/runs/test_types.rs b/crates/sqlbuild-orchestration/tests/runs/test_types.rs index 0214432a6d..0f4cc521f6 100644 --- a/crates/sqlbuild-orchestration/tests/runs/test_types.rs +++ b/crates/sqlbuild-orchestration/tests/runs/test_types.rs @@ -2,6 +2,8 @@ use std::collections::BTreeSet; +use uuid::Uuid; + use crate::credentials::models::BuiltinRole; use crate::runs::models::{RerunRequest, RunStatus}; @@ -114,3 +116,78 @@ pub(crate) struct MixedAdmissionTestCase { pub(crate) parent_priority: i64, pub(crate) expected_selections: [&'static str; 2], } + +/// Who runs an ad-hoc case: Admin, Developer, Operator, or a person with no role. +#[derive(Clone, Copy)] +pub(crate) enum AdhocPerson { + Admin, + Developer, + Operator, + Stranger, +} + +/// The people the ad-hoc fixture provisions. +pub(crate) struct AdhocPeople { + pub(crate) admin: Uuid, + pub(crate) developer: Uuid, + pub(crate) operator: Uuid, + pub(crate) stranger: Uuid, +} + +impl AdhocPeople { + pub(crate) fn of(&self, person: AdhocPerson) -> Uuid { + match person { + AdhocPerson::Admin => self.admin, + AdhocPerson::Developer => self.developer, + AdhocPerson::Operator => self.operator, + AdhocPerson::Stranger => self.stranger, + } + } +} + +/// One person's command: what its dry run decides and what launching it does. +pub(crate) struct AdhocCommandTestCase { + pub(crate) description: &'static str, + pub(crate) person: AdhocPerson, + pub(crate) command: &'static [&'static str], + pub(crate) priority: i64, + pub(crate) reason: &'static str, + pub(crate) expected_check: serde_json::Value, + pub(crate) expected_launch: Result, +} + +/// The CLI's planner reduced to exact node keys, `--full-refresh` and `--warehouse NAME`. +pub(crate) struct ExactKeyPlanner; + +impl crate::runs::types::CommandPlanner for ExactKeyPlanner { + fn plan( + &self, + command: &[String], + resources: &[crate::runs::models::PlannerResource], + ) -> Result { + let mut plan = crate::runs::models::CommandPlan { + verb: command.first().cloned().unwrap_or_default(), + selection: Default::default(), + settings: Default::default(), + }; + let mut tokens = command.iter().skip(1); + while let Some(token) = tokens.next() { + match token.as_str() { + "--full-refresh" => { + plan.settings + .insert("full_refresh".into(), serde_json::json!(true)); + } + "--warehouse" => { + let name = tokens.next().ok_or("--warehouse needs a name")?; + plan.settings + .insert("warehouse".into(), serde_json::json!(name)); + } + key if resources.iter().any(|resource| resource.key == key) => { + plan.selection.insert(key.to_owned()); + } + other => return Err(format!("{other} is not available for ad-hoc runs")), + } + } + Ok(plan) + } +} diff --git a/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/warehouses.rs b/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/warehouses.rs index da4b96cf18..4b9a2bd62a 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/warehouses.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/command_runtime/warehouses.rs @@ -67,12 +67,6 @@ pub(crate) struct CommandScope<'py> { pub(crate) cli_vars: Bound<'py, PyAny>, } -/// A target's authored group warehouse with the `WarehouseGroup` it was found for. -pub(crate) struct FoundWarehouse<'py> { - pub(crate) authored: AuthoredWarehouse, - pub(crate) group: Bound<'py, PyAny>, -} - /// The project inputs and target a warehouse is resolved against. #[derive(Clone, Copy)] pub(crate) struct WarehouseInputs<'a, 'py> { @@ -122,11 +116,11 @@ fn resolved_warehouse<'py>( ) } -fn group_value(target: &Bound<'_, PyAny>, group: &Bound<'_, PyAny>) -> PyResult> { +fn group_value(target: &Bound<'_, PyAny>, group_name: &str) -> PyResult> { if target.is_none() { return Ok(None); } - let attribute: &str = if group.str()?.extract::()? == BUILD_GROUP { + let attribute: &str = if group_name == BUILD_GROUP { BUILD_GROUP } else { QUERY_GROUP @@ -138,11 +132,20 @@ fn group_value(target: &Bound<'_, PyAny>, group: &Bound<'_, PyAny>) -> PyResult< } /// The active target's authored, unexpanded warehouse for one command group. -pub(crate) fn find_authored<'py>( +pub(crate) fn find_authored( + inputs: WarehouseInputs<'_, '_>, + group: &Bound<'_, PyAny>, +) -> PyResult> { + let group_name: String = native_field(group, "value")?.extract()?; + authored_group(inputs, &group_name) +} + +/// The active target's authored, unexpanded warehouse for the group named `group_name`. +fn authored_group<'py>( inputs: WarehouseInputs<'_, 'py>, - group: &Bound<'py, PyAny>, -) -> PyResult>> { - let py: Python<'py> = group.py(); + group_name: &str, +) -> PyResult> { + let py: Python<'py> = inputs.discovered_inputs.py(); let project_config: Bound<'py, PyAny> = native_field(inputs.discovered_inputs, "project_config")?; let local_config: Bound<'py, PyAny> = native_field(inputs.discovered_inputs, "local_config")?; @@ -161,25 +164,20 @@ pub(crate) fn find_authored<'py>( let project_target: Bound<'py, PyAny> = native_field(&project_config, "targets")?.call_method1("get", (&target_name,))?; let target_name: String = target_name.extract()?; - let group_name: String = native_field(group, "value")?.extract()?; Ok(authored_warehouse( - (&target_name, &group_name), - group_value(&local_target, group)?, - group_value(&project_target, group)?, - ) - .map(|authored| FoundWarehouse { - authored, - group: group.clone(), - })) + (&target_name, group_name), + group_value(&local_target, group_name)?, + group_value(&project_target, group_name)?, + )) } /// The group warehouse with `${...}` templates expanded, checked as an identifier. fn expand_authored( - found: &FoundWarehouse<'_>, + authored: &AuthoredWarehouse, inputs: WarehouseInputs<'_, '_>, cli_vars: &Bound<'_, PyAny>, ) -> PyResult { - let py: Python<'_> = found.group.py(); + let py: Python<'_> = inputs.discovered_inputs.py(); let runtime: Bound<'_, PyAny> = build_effective_runtime_config( &inputs.discovered_inputs.clone(), Some(inputs.selected_target.clone()).filter(|value| !value.is_none()), @@ -187,7 +185,7 @@ fn expand_authored( ) .and_then(|value| pyo3::IntoPyObjectExt::into_bound_py_any(value, py))?; let expanded: Bound<'_, PyAny> = expanded_template_data( - PyString::new(py, &found.authored.warehouse).as_any(), + PyString::new(py, &authored.warehouse).as_any(), (runtime.get_item(1)?.downcast()?, &PyDict::new(py)), ( TemplateOptions { @@ -195,11 +193,11 @@ fn expand_authored( preserve_context_tokens: false, preserve_unknown_context: false, }, - &found.authored.context_label(), + &authored.context_label(), ), )?; let warehouse: String = expanded.str()?.extract()?; - check_expanded_warehouse(&found.authored, &warehouse).map_err(|error| raised(py, error))?; + check_expanded_warehouse(authored, &warehouse).map_err(|error| raised(py, error))?; Ok(warehouse) } @@ -257,7 +255,7 @@ pub(crate) fn resolve_warehouse( && let Some(found) = find_authored(inputs, group)? { let warehouse: String = expand_authored(&found, inputs, cli_vars)?; - return Ok((Some(warehouse), found.authored.source())); + return Ok((Some(warehouse), found.source())); } Ok(match connection_warehouse(connection_config)? { Some(warehouse) => (Some(warehouse), CONNECTION_WAREHOUSE_SOURCE.to_owned()), @@ -265,6 +263,22 @@ pub(crate) fn resolve_warehouse( }) } +/// Every warehouse the target configures: its `build` and `query` group warehouses and its connection warehouse. +pub(crate) fn configured_warehouses( + inputs: WarehouseInputs<'_, '_>, + connection_config: &Bound<'_, PyAny>, +) -> PyResult> { + let none: Bound<'_, PyAny> = none(connection_config.py()); + let mut names: std::collections::BTreeSet = std::collections::BTreeSet::new(); + for group_name in [BUILD_GROUP, QUERY_GROUP] { + if let Some(authored) = authored_group(inputs, group_name)? { + names.insert(expand_authored(&authored, inputs, &none)?); + } + } + names.extend(connection_warehouse(connection_config)?); + Ok(names) +} + fn optional_inputs<'a, 'py>( discovered_inputs: &'a Bound<'py, PyAny>, selected_target: &'a Bound<'py, PyAny>, @@ -397,7 +411,7 @@ pub(crate) fn describe_connection_warehouse<'py>( discovered_inputs: &discovered_inputs, selected_target: &selected_target, }; - let found: Option> = if scope.group.is_none() { + let found: Option = if scope.group.is_none() { None } else { find_authored(inputs, &scope.group)? @@ -409,7 +423,7 @@ pub(crate) fn describe_connection_warehouse<'py>( Err(error) => return Err(error), }; if expands_to { - return resolved_warehouse(py, Some(&warehouse), &found.authored.source()); + return resolved_warehouse(py, Some(&warehouse), &found.source()); } } resolved_warehouse(py, Some(&warehouse), CONNECTION_WAREHOUSE_SOURCE) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/snapshot_execution.rs b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/snapshot_execution.rs index 93451ffa40..edd96b77f1 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/snapshot_execution.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/compiled_project/snapshot_execution.rs @@ -20,6 +20,8 @@ pub(crate) struct WorkerBuild { execution: RunExecution, completed: Mutex>, checks: Mutex>>, + /// Why the worker refused the run before executing anything, reported as the run's status reason. + refusal: Mutex>, } impl WorkerBuild { @@ -29,6 +31,7 @@ impl WorkerBuild { worker: worker.clone().unbind(), completed: Mutex::default(), checks: Mutex::default(), + refusal: Mutex::default(), }); for key in state.execution.skipped_nodes.keys() { state.complete(worker.py(), key, (NodeOutcome::Skipped, None, None))?; @@ -45,6 +48,12 @@ impl WorkerBuild { .any(|key| !self.execution.skipped_nodes.contains_key(key)) } + /// Record why the run is refused; the terminal report carries it as the run's status reason. + pub(crate) fn refuse(&self, reason: &str) -> PyResult<()> { + *self.refusal.lock().map_err(|_| poisoned())? = Some(reason.to_owned()); + Ok(()) + } + pub(crate) fn check_cancelled(&self, py: Python<'_>) -> PyResult<()> { if self.worker.bind(py).get().cancel_requested(py) { return Err(PyKeyboardInterrupt::new_err("run cancellation requested")); @@ -332,12 +341,18 @@ impl WorkerBuild { }, )?; } + let reason: String = self + .refusal + .lock() + .map_err(|_| poisoned())? + .clone() + .unwrap_or_else(|| "worker executor completed and closed its connections".into()); self.worker.bind(py).get().change( py, "worker-finished", RunChange::Finished { outcome, - reason: "worker executor completed and closed its connections".into(), + reason, exit_code: Some(i64::from(exit_code)), }, )?; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/debug_command/run_debug.rs b/crates/sqlbuild-python/src/bindings/_helpers/debug_command/run_debug.rs index 0d8524bc71..456358a009 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/debug_command/run_debug.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/debug_command/run_debug.rs @@ -136,7 +136,7 @@ fn warehouse_checks( checks.push(WarehouseCheck::NotUsed { group: group_name, adapter_name: adapter_name.clone(), - source: found.authored.source(), + source: found.source(), }); } continue; diff --git a/crates/sqlbuild-python/src/bindings/_helpers/location_command/worker.rs b/crates/sqlbuild-python/src/bindings/_helpers/location_command/worker.rs index 69ff6122f8..b8000d071a 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/location_command/worker.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/location_command/worker.rs @@ -10,12 +10,16 @@ use sqlbuild_cli::argv::main::job_settings_arguments::job_settings_arguments; use sqlbuild_cli::dispatch::main::command_dispatch::command_dispatch; use sqlbuild_cli::dispatch::models::Dispatch; use sqlbuild_host::entries::models::ExecutorHost; -use sqlbuild_orchestration::runs::models::{RunOutcome, RunWindow}; +use sqlbuild_orchestration::runs::models::{RunKind, RunOutcome, RunRequest, RunWindow}; use crate::bindings::_helpers::boundary::python_calls::{KeywordArguments, call_with, within}; use crate::bindings::_helpers::build_command::command::run_build; use crate::bindings::_helpers::cli::argv::parse_cli_invocation; use crate::bindings::_helpers::cli::dispatch::{Invocation, command_facts, warehouse_scope}; +use crate::bindings::_helpers::command_runtime::warehouses::{ + WarehouseInputs, configured_warehouses, +}; +use crate::bindings::_helpers::compile_attachment::effective_config::build_effective_connection_config; use crate::bindings::_helpers::compiled_project::snapshot::NativeOrchestrationSnapshot; use crate::bindings::_helpers::compiled_project::snapshot_execution::WorkerBuild; use crate::bindings::_helpers::compiled_project::snapshot_worker::NativeOrchestrationWorker; @@ -89,6 +93,37 @@ fn build_request<'py>( )) } +/// Why an ad-hoc run's `--warehouse` is refused; published jobs keep their authored warehouse. +fn unconfigured_warehouse( + discovered: &Bound<'_, PyAny>, + frozen: &RunRequest, +) -> PyResult> { + if frozen.kind != RunKind::Adhoc { + return Ok(None); + } + let Some(warehouse) = frozen.settings.get("warehouse") else { + return Ok(None); + }; + let warehouse: &str = warehouse + .as_str() + .ok_or_else(|| PyValueError::new_err("frozen run warehouse must be text"))?; + let selected_target: Bound<'_, PyAny> = + PyString::new(discovered.py(), &frozen.target).into_any(); + let connection: Bound<'_, PyAny> = + build_effective_connection_config(discovered, Some(selected_target.clone()), None, false)?; + let inputs = WarehouseInputs { + discovered_inputs: discovered, + selected_target: &selected_target, + }; + if configured_warehouses(inputs, &connection)?.contains(warehouse) { + return Ok(None); + } + Ok(Some( + "the requested warehouse is not configured for this target on this location; \ + no warehouse statement ran", + )) +} + /// The heartbeat is native; only driver cancellation crosses the executor host boundary. fn run_cancellable( py: Python<'_>, @@ -153,6 +188,12 @@ pub(super) fn execute_worker<'py>( )?; return Ok(0); } + let frozen = owner.get().execution(py)?.claim.request; + if let Some(reason) = unconfigured_warehouse(&discovered, &frozen)? { + worker.refuse(reason)?; + write_stderr(py, &format!("Run refused: {reason}\n"))?; + return Err(PyValueError::new_err(reason)); + } let (request, scope) = build_request(py, discovered, arguments, &owner)?; write_stderr(py, "Run inputs verified. Executing frozen selection...\n")?; within(&scope, |_| run_cancellable(py, &worker, &request)) diff --git a/fensu.toml b/fensu.toml index 7c1f8e7b53..7fa26d1fb0 100644 --- a/fensu.toml +++ b/fensu.toml @@ -2485,7 +2485,7 @@ pending_budgets = [ "crates/sqlbuild-python/src/bindings/_helpers/clone/clone_progress.rs=3", "crates/sqlbuild-python/src/bindings/_helpers/command_runtime/invocation.rs=4", "crates/sqlbuild-python/src/bindings/_helpers/command_runtime/warehouse_scope.rs=2", - "crates/sqlbuild-python/src/bindings/_helpers/command_runtime/warehouses.rs=5", + "crates/sqlbuild-python/src/bindings/_helpers/command_runtime/warehouses.rs=4", "crates/sqlbuild-python/src/bindings/_helpers/compile_attachment/sql_expansion_context.rs=5", "crates/sqlbuild-python/src/bindings/_helpers/compile_command/models.rs=9", "crates/sqlbuild-python/src/bindings/_helpers/compile_command/prepared_artifacts.rs=7", diff --git a/src/sqlbuild/.agents/skills/sqlbuild/references/docs/cli/server.md b/src/sqlbuild/.agents/skills/sqlbuild/references/docs/cli/server.md index 2e87b81cc6..05cafdfc59 100644 --- a/src/sqlbuild/.agents/skills/sqlbuild/references/docs/cli/server.md +++ b/src/sqlbuild/.agents/skills/sqlbuild/references/docs/cli/server.md @@ -11,6 +11,7 @@ Online: https://sqlbuild.com/docs/cli/server/ - Set retention - Grant roles to OIDC groups (beta) - Inspect published SQL +- Run ad-hoc commands - Define project roles Run the control API and scheduler without loading project Python: @@ -141,6 +142,76 @@ data. Every successful inspection records an audit entry without copying SQL int audit cannot be saved, the request fails without returning SQL. Revocation takes effect on the next request, including sessions created by [`sqb login`](login.md). +## Run ad-hoc commands + +People with `run.adhoc` can run a `sqb` command on the current publication without declaring a +job. Use the web UI's Run panel, or send the command as argument tokens after `sqb`: + +```json +{ + "location_id": "0191f0aa-0000-7000-8000-000000000001", + "target": "development", + "command": ["build", "--select", "+orders", "--full-refresh"], + "priority": 0 +} +``` + +`POST /api/control/runs/adhoc/check` with this body is a dry run. It returns the exact nodes the +command selects (`selection`), the nodes it writes (`write_set`), the options the worker receives +(`settings`), and each permission it needs with your decision (`permissions`). `error` explains a +command that can't run whatever your permissions, and `launchable` is `true` when the launch +would be accepted. A dry run queues nothing and records no audit entry. + +To launch, `POST /api/control/runs/adhoc` with the same fields plus a new `run_id` and a +`reason`. The response is `{"applied": true}`; sending the same `run_id` again returns +`{"applied": false}` and starts nothing. The server resolves selectors against the current +publication, and the worker runs exactly the resolved nodes. The target is always the one in the +request. + +| Verb | Ad-hoc runs | +| --- | --- | +| `build` | Dry run and launch | +| `test`, `seed`, `audit`, `freshness` | Dry run only; workers can't run them yet | +| Any other verb | Refused | + +Selectors use the CLI grammar: names, `seed:`, `source:`, `tag:`, `path:` and `test:`, with `+` +for upstream or downstream nodes. Positional selectors work as on the command line. + +| Flags | Verbs | Needs | +| --- | --- | --- | +| `--select`, `-s`, `--exclude` | All | `run.adhoc` on every selected node | +| `--full-refresh`, `--start-cursor`, `--end-cursor`, `--start-cursor-ts`, `--end-cursor-ts`, `--start-cursor-int`, `--end-cursor-int` | `build` | `run.rewrite_history` | +| `--allow-snapshot-full-refresh`, `--allow-snapshot-schema-change`, `--allow-table-type-downgrade`, `--allow-retention-decrease` | `build` | `run.rewrite_history` | +| `--warehouse` | All | `run.warehouse`, and the warehouse configured for the target on the location | +| `--max-microbatches`, `--no-load`, `--no-tests`, `--no-audits`, `--no-python`, `--fail-fast`, `--selection-diagnostics`, `--verbose` | `build` | `run.adhoc` | +| `--concurrency` | `build`, `test`, `seed`, `audit` | `run.adhoc` | +| `--test-timeout` | `build`, `test` | `run.adhoc` | +| `--case`, `--inspect` | `test` | `run.adhoc` | +| `--state`, `--fail-on-error`, `--fail-on-stale` | `freshness` | `run.adhoc` | + +A priority above 0 also needs `run.prioritise`. Among built-in roles only Admin grants +`run.rewrite_history`, `run.warehouse` and `run.prioritise`. Every other flag is refused with +its reason: + +| Refused flags | Reason | +| --- | --- | +| `--vars` | Variables change the published SQL | +| `--defer-to`, `--defer-sources-to`, `--defer-clone-from` | They read another target | +| `--load`, `--reload` | Source loads are not part of ad-hoc runs | +| `--allow-missing-migration-origin` | It can lose data | +| `--no-sql-analysis`, `--no-cache` | Workers verify the published SQL and manage their own cache | +| `--debug` | It prints executed SQL | +| `--since`, `--explain` | They need the worker's Git checkout | +| `--select-file` | It reads a file on the worker | +| `--target`, `--project-dir` and the `--dbt-*` paths | Workers run the published project for the request's target | +| `--json`, `--json-output`, `--event-output`, `--manifest`, `--no-color` | Workers own the run's output | + +For an ad-hoc run, the worker checks `--warehouse` against the warehouses the location +configures for the target before any statement runs. An unconfigured warehouse fails the run with +the reason `the requested warehouse is not configured for this target on this location; no +warehouse statement ran`, which the run page shows. A job's own `warehouse` setting is not +checked this way. + ## Define project roles Put project roles in `roles.toml` at the project root. `sqb location publish` resolves each @@ -164,7 +235,7 @@ permissions = ["view.evidence"] | Selectors | Same grammar as a job's `select`; an unknown tag or name fails publication | | No matching nodes | Publication succeeds with a warning; the grant gives nothing | | Permissions | Any non-administrative action; `user.manage`, `role.assign` and `audit.view` are rejected | -| Runs | `job.run`, `run.adhoc`, `run.rerun` and `run.rewrite_history` need the permission on every node the run writes; a priority above 0 also needs `run.prioritise`, which only Admin grants among built-in roles | +| Runs | `job.run`, `run.rerun` and `run.rewrite_history` need the permission on every node the run writes; an [ad-hoc command](#run-ad-hoc-commands) needs `run.adhoc` and its flags' permissions on every node it selects; a priority above 0 also needs `run.prioritise` | | Triggers | `trigger.toggle` and `sensor.reset_cursor` need the permission on every node the trigger's job writes | | Listings | The catalog, statistics and run list need `view` on every node of the current publication; partial coverage is denied, not filtered | | Several roles | Grants add up across a person's roles | diff --git a/tests/integration/src/sqlbuild/compiler/compiled_project/publication/_test_types.py b/tests/integration/src/sqlbuild/compiler/compiled_project/publication/_test_types.py index c90b11f1d8..9c8f2db146 100644 --- a/tests/integration/src/sqlbuild/compiler/compiled_project/publication/_test_types.py +++ b/tests/integration/src/sqlbuild/compiler/compiled_project/publication/_test_types.py @@ -340,3 +340,20 @@ class ChangedJobTestCase(WorkerExecutionTestCase): new_order_sql: str execute: Callable[[Path, str], list[tuple[Any, ...]]] expected_runs: tuple[tuple[str, frozenset[str], dict[str, int]], ...] + + +@dataclass(frozen=True) +class AdhocRunTestCase: + """An ad-hoc command a person checks and launches through `sqb server`, and what follows.""" + + description: str + person: Literal["admin", "developer"] + command: list[str] + expected_permissions: list[list[Any]] + expected_status: str + expected_reason: str + expected_statements: bool + expected_customers_tables: int + job_settings: str = "" + expected_scheduled_status: str = "success" + expected_worker_log: str = "" diff --git a/tests/integration/src/sqlbuild/compiler/compiled_project/publication/helpers.py b/tests/integration/src/sqlbuild/compiler/compiled_project/publication/helpers.py index 8d93e83735..83d2828953 100644 --- a/tests/integration/src/sqlbuild/compiler/compiled_project/publication/helpers.py +++ b/tests/integration/src/sqlbuild/compiler/compiled_project/publication/helpers.py @@ -1352,3 +1352,30 @@ def audit_node_keys(api: SnapshotApi, target: str) -> frozenset[str]: """Keys of the current snapshot's audit nodes.""" nodes: dict[str, Any] = current_snapshot(api, target)["nodes"] return frozenset(filter(lambda key: nodes[key]["kind"] == "audit", nodes)) + + +def worker_statements(control_store: Path, run_id: str) -> int: + """Statements the run's worker invocations recorded, as the run statement read selects them.""" + with sqlite3.connect(control_store) as control: + invocations: list[str] = [ + invocation + for (invocation,) in control.execute( + "SELECT invocation_id FROM invocations WHERE invocation_id LIKE 'worker/%' " + "AND json_extract(external_context, '$.run_id') = ?", + (run_id,), + ).fetchall() + ] + assert invocations, f"run {run_id} has no worker invocation" + return sum( + control.execute( + "SELECT count(*) FROM statements WHERE invocation_id=? OR invocation_id LIKE ?", + (invocation, invocation + "/%"), + ).fetchone()[0] + for invocation in invocations + ) + + +def security_audit_rows(control_store: Path) -> int: + """Rows in the security audit, so a test can show a request recorded nothing.""" + with sqlite3.connect(control_store) as control: + return control.execute("SELECT count(*) FROM sqb_security_audit").fetchone()[0] diff --git a/tests/integration/src/sqlbuild/compiler/compiled_project/publication/test_adhoc.py b/tests/integration/src/sqlbuild/compiler/compiled_project/publication/test_adhoc.py new file mode 100644 index 0000000000..f8054f779d --- /dev/null +++ b/tests/integration/src/sqlbuild/compiler/compiled_project/publication/test_adhoc.py @@ -0,0 +1,187 @@ +"""People check and launch ad-hoc commands through `sqb server`; workers run exactly what was resolved.""" + +from pathlib import Path +from typing import Any +from urllib.error import HTTPError +from uuid import uuid4 + +import pytest + +from tests.integration.src.sqlbuild.compiler.compiled_project.publication._test_types import ( + AdhocRunTestCase, + WorkerExecutionTestCase, +) +from tests.integration.src.sqlbuild.compiler.compiled_project.publication.helpers import ( + duckdb_rows, + scheduled_worker, + security_audit_rows, + wait_for_run, + worker_statements, +) +from tests.integration.src.sqlbuild.compiler.compiled_project.publication.person.helpers import ( + person_login, + person_read, + person_request, + provision_person, +) + +WAREHOUSE_REFUSAL: str = ( + "the requested warehouse is not configured for this target on this location; " + "no warehouse statement ran" +) + + +@pytest.mark.parametrize( + "test_case", + [ + AdhocRunTestCase( + description="a developer builds exactly the resolved selection", + person="developer", + command=["build", "--select", "customers"], + expected_permissions=[["run.adhoc", True]], + expected_status="success", + expected_reason="worker executor completed and closed its connections", + expected_statements=True, + expected_customers_tables=1, + ), + AdhocRunTestCase( + description="an unconfigured warehouse is refused before any statement, " + "while a published job keeps its authored warehouse", + person="admin", + command=["build", "--select", "customers", "--warehouse", "ANALYST_XS"], + expected_permissions=[["run.adhoc", True], ["run.warehouse", True]], + expected_status="failed", + expected_reason=WAREHOUSE_REFUSAL, + expected_statements=False, + expected_customers_tables=0, + # DuckDB has no session warehouse, so the CLI refuses the job's warehouse: the + # published job reached the CLI instead of being refused by the worker. + job_settings="settings={'warehouse': 'ANALYST_XS'}, ", + expected_scheduled_status="failed", + expected_worker_log="error[C260]: --warehouse ANALYST_XS was given", + ), + ], + ids=lambda case: case.description, +) +def test_given_an_ad_hoc_command_when_checked_and_launched_then_the_worker_runs_the_resolved_selection( + test_case: AdhocRunTestCase, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + worker_case: WorkerExecutionTestCase = WorkerExecutionTestCase( + description=test_case.description, + model_sql="SELECT 5 AS amount", + warehouse_query="SELECT amount FROM orders", + expected_status="success", + expected_rows=[(5,)], + files={ + "models/customers.sql": ( + "MODEL (description 'Customers.', materialized table);\nSELECT 1 AS id\n" + ) + } + | { + "orchestration/orders.py": ( + "from sqlbuild.orchestration import job, schedule\n" + 'annual_orders = job("annual_orders", select="orders", ' + f'description="Build orders.", {test_case.job_settings}' + 'schedule=schedule(cron="0 0 * * *", timezone="UTC", ' + 'starts_at="2000-01-01T00:00:00Z"))\n' + ) + }, + ) + with scheduled_worker( + tmp_path, + monkeypatch, + worker_case, + server_arguments=("--password-origin", "https://control.example.com"), + ) as worker: + scheduled: dict[str, Any] = wait_for_run( + worker.api, worker.process, worker.log, job_key="annual_orders" + ) + assert scheduled["status"] == test_case.expected_scheduled_status, worker.log.read_text() + assert scheduled["status_reason"] != WAREHOUSE_REFUSAL, "published jobs are not refused" + assert test_case.expected_worker_log in worker.log.read_text() + admin: dict[str, Any] = provision_person( + tmp_path / "control", "create-user", "orders-admin" + ) + developer: dict[str, Any] = provision_person( + tmp_path / "control", "create-principal", "orders-developer" + ) + sessions: dict[str, tuple[str, str]] = { + "admin": person_login(worker.api, admin["name"]), + "developer": person_login(worker.api, developer["name"]), + } + assert person_request( + worker.api, + "/api/admin/roles", + body={ + "principal_id": developer["principal_id"], + "role": "developer", + "location_id": worker.api.location_id, + "target": "development", + }, + session=sessions["admin"], + ) == {"applied": True} + session: tuple[str, str] = sessions[test_case.person] + scope: dict[str, Any] = {"location_id": worker.api.location_id, "target": "development"} + audited: int = security_audit_rows(worker.control_store) + check: dict[str, Any] = person_request( + worker.api, + "/api/control/runs/adhoc/check", + body=scope | {"command": test_case.command, "priority": 0}, + session=session, + ) + assert (check["selection"], check["write_set"], check["error"], check["launchable"]) == ( + ["model:customers"], + ["model:customers"], + None, + True, + ) + assert [ + [permission["action"], permission["allowed"]] for permission in check["permissions"] + ] == test_case.expected_permissions + assert security_audit_rows(worker.control_store) == audited, "a dry run records nothing" + with pytest.raises(HTTPError) as refused: + person_request( + worker.api, + "/api/control/runs/adhoc", + body=scope + | { + "run_id": str(uuid4()), + "command": ["build", "--vars", "{}"], + "reason": "Try variables", + "priority": 0, + }, + session=session, + ) + assert refused.value.code == 400 + assert "--vars is not available for ad-hoc runs" in refused.value.read().decode() + run_id: str = str(uuid4()) + launch: dict[str, Any] = scope | { + "run_id": run_id, + "command": test_case.command, + "reason": "Refresh customers", + "priority": 0, + } + for expected_applied in [True, False]: + assert person_request( + worker.api, "/api/control/runs/adhoc", body=launch, session=session + ) == {"applied": expected_applied} + completed: dict[str, Any] = wait_for_run( + worker.api, worker.process, worker.log, run_id=run_id + ) + assert completed["status"] == test_case.expected_status, worker.log.read_text() + assert completed["selection_json"] == ["model:customers"] + listed: list[dict[str, Any]] = person_read( + worker.api, + f"/api/control/locations/{worker.api.location_id}/targets/development/runs", + session=session, + ) + assert {run["run_id"]: run["status_reason"] for run in listed}[run_id] == ( + test_case.expected_reason + ), "the person sees why the run ended" + assert (worker_statements(worker.control_store, run_id) > 0) == ( + test_case.expected_statements + ) + assert duckdb_rows( + worker.project / "warehouse.duckdb", + "SELECT count(*) FROM information_schema.tables WHERE table_name='customers'", + ) == [(test_case.expected_customers_tables,)] diff --git a/ui/scripts/extract-command-table.mjs b/ui/scripts/extract-command-table.mjs index 7164ee94ab..07ff797501 100644 --- a/ui/scripts/extract-command-table.mjs +++ b/ui/scripts/extract-command-table.mjs @@ -1,14 +1,13 @@ #!/usr/bin/env node // Extracts the run panel's verbs and flags from the CLI's command table, so the -// panel offers exactly the flags `sqb` accepts. +// panel offers exactly the flags an ad-hoc run accepts. // // node scripts/extract-command-table.mjs # rewrite the generated file // node scripts/extract-command-table.mjs --check # fail if it is out of date // -// Flags the server controls or that only make sense on a local shell (target, -// output formats, project paths, dbt interop paths) are left out. Every name in -// the lists below must exist in the table, so a renamed flag fails the script -// instead of silently drifting. +// The verbs and accepted destinations come from the CLI's ad-hoc allowlist, and +// the permissions a flag needs come from the control plane's rules, so the panel +// cannot offer a flag the server refuses or hide a gate it applies. import { readFileSync, writeFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; @@ -16,86 +15,62 @@ import { fileURLToPath } from 'node:url'; const here = dirname(fileURLToPath(import.meta.url)); const SOURCE = resolve(here, '../../crates/sqlbuild-cli/src/argv/command_table.json'); +const ALLOWLIST = resolve(here, '../../crates/sqlbuild-cli/src/adhoc/constants.rs'); +const GATES = resolve(here, '../../crates/sqlbuild-orchestration/src/runs/_helpers/requests/permissions.rs'); const OUTPUT = resolve(here, '../src/lib/run/command-table.generated.json'); -/** Verbs a server run can launch, with the panel's one-line descriptions. */ -const VERBS = { +/** The panel's one-line description of each verb the allowlist accepts. */ +const VERB_DESCRIPTIONS = { build: 'Build the selected nodes, running their tests and audits', test: 'Run unit and scenario tests for the selection', seed: 'Load seed files', - load: 'Run loaders into their source tables', audit: 'Evaluate audits against what is already built', freshness: 'Check source freshness' }; -/** Set by the server from the location's target, the run token and the event stream. */ -const SERVER_CONTROLLED = [ - '--help', - '--target', - '--json', - '--json-output', - '--event-output', - '--project-dir', - '--no-color', - '--debug', - '--manifest', - '--select-file', - '--selection-diagnostics', - '--dbt-project-dir', - '--dbt-profiles-dir', - '--dbt-target', - '--dbt-target-path', - '--since', - '--explain', - '--inspect' -]; - -/** Flags that rewrite history and need `run.rewrite_history`. */ -const REWRITES_HISTORY = [ - '--full-refresh', - '--allow-snapshot-full-refresh', - '--reload', - '--start-cursor', - '--end-cursor', - '--start-cursor-ts', - '--end-cursor-ts', - '--start-cursor-int', - '--end-cursor-int' -]; +/** Flags the planner refuses although their destination is accepted: `--no-load` shares `--load`'s. */ +const REFUSED_FLAGS = ['--load']; /** Descriptions for flags the table leaves undocumented. */ const DESCRIPTIONS = { - '--select': 'Selector expression, e.g. +fact_orders, tag:finance, group:marts', + '--select': 'Selector expression, e.g. +fact_orders, tag:finance, path:models/marts/', '--exclude': 'Selector expression to leave out', - '--defer-to': 'Read unselected upstream nodes from another target', - '--defer-clone-from': 'Clone unselected upstream nodes from another target', - '--defer-sources-to': 'Read sources from another target', '--no-python': 'Skip Python nodes', '--no-tests': 'Skip unit tests during the build', '--no-audits': 'Skip audits during the build', '--max-microbatches': 'Process at most this many microbatches per incremental model', - '--load': 'Run loaders as part of the build', '--no-load': 'Skip loaders', - '--reload': 'Reload loader sources from the beginning', '--fail-fast': 'Stop at the first failed node', '--full-refresh': 'Rebuild incremental models and snapshots from scratch', '--allow-snapshot-full-refresh': 'Allow a full refresh of snapshot models', '--allow-table-type-downgrade': 'Allow replacing a table with a less durable table type', '--allow-retention-decrease': 'Allow lowering a table retention setting', - '--allow-missing-migration-origin': 'Allow a migration whose origin relation no longer exists', '--allow-snapshot-schema-change': 'Allow a schema change on snapshot models', '--concurrency': 'Nodes to run in parallel within this run', '--verbose': 'More detailed output', - '--vars': 'Project variables as a JSON or key=value mapping', '--warehouse': 'Warehouse for this run, overriding the target default', - '--no-sql-analysis': 'Disable SQL syntax, binding and type validation', - '--no-cache': 'Bypass the compile analysis cache', '--case': 'Run one named parameter case', '--state': 'Record freshness results in the state tables', '--fail-on-error': 'Fail when a freshness check errors', '--fail-on-stale': 'Fail when a source is stale' }; +/** The quoted strings of a Rust `const NAME: [...] = [...];` array, which must exist. */ +function rustStrings(path, name) { + const source = readFileSync(path, 'utf8'); + const match = source.match(new RegExp(`const ${name}: \\[[^\\]]+\\] = \\[([\\s\\S]*?)\\];`)); + if (!match) throw new Error(`${name} is not a string array in ${path}`); + return [...match[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]); +} + +const ADHOC_VERBS = rustStrings(ALLOWLIST, 'ADHOC_VERBS'); +const ACCEPTED = new Set([...rustStrings(ALLOWLIST, 'ALLOWED_DESTS'), ...rustStrings(ALLOWLIST, 'SELECTOR_DESTS')]); +/** Destinations whose setting needs a permission beyond run.adhoc, as the control plane decides it. */ +const REQUIRES = new Map([ + ...['full_refresh', ...rustStrings(GATES, 'CURSOR_SETTINGS'), ...rustStrings(GATES, 'ADHOC_DESTRUCTIVE_SETTINGS')].map((dest) => [dest, 'run.rewrite_history']), + ['warehouse', 'run.warehouse'] +]); + function valueType(action) { const flag = action.options[0]; if (action.kind === 'store_true' || action.kind === 'store_false') return 'bool'; @@ -108,7 +83,9 @@ function valueType(action) { function extract(table) { const parsers = table.actions.find((a) => a.kind === 'parsers')?.parsers ?? []; const known = new Set(); - const verbs = Object.entries(VERBS).map(([name, description]) => { + const verbs = ADHOC_VERBS.map((name) => { + const description = VERB_DESCRIPTIONS[name]; + if (!description) throw new Error(`Verb ${name} has no panel description`); const parser = parsers.find((p) => p.name === name); if (!parser) throw new Error(`Verb ${name} is not in the command table`); const flags = []; @@ -116,7 +93,7 @@ function extract(table) { if (!action.options) continue; for (const option of action.options) known.add(option); const flag = action.options.find((o) => o.startsWith('--')); - if (!flag || action.options.some((o) => SERVER_CONTROLLED.includes(o))) continue; + if (!flag || !ACCEPTED.has(action.dest) || REFUSED_FLAGS.includes(flag)) continue; const help = typeof action.help === 'string' ? action.help : null; if (action.help && typeof action.help === 'object') continue; flags.push({ @@ -125,14 +102,17 @@ function extract(table) { type: valueType(action), choices: action.choices ?? null, repeatable: action.kind === 'extend' || action.kind === 'append', - rewritesHistory: REWRITES_HISTORY.includes(flag), + requires: REQUIRES.get(action.dest) ?? null, description: help ?? DESCRIPTIONS[flag] ?? '' }); } return { name, description, flags }; }); - for (const flag of [...SERVER_CONTROLLED, ...REWRITES_HISTORY, ...Object.keys(DESCRIPTIONS)]) { - if (!known.has(flag)) throw new Error(`${flag} is listed here but not in the command table for any launchable verb`); + for (const flag of [...REFUSED_FLAGS, ...Object.keys(DESCRIPTIONS)]) { + if (!known.has(flag)) throw new Error(`${flag} is described here but not in the command table for any ad-hoc verb`); + } + for (const dest of REQUIRES.keys()) { + if (!ACCEPTED.has(dest)) throw new Error(`${dest} is gated by the control plane but not accepted by the ad-hoc allowlist`); } return { source: 'crates/sqlbuild-cli/src/argv/command_table.json', verbs }; } diff --git a/ui/src/lib/api/http-client.ts b/ui/src/lib/api/http-client.ts index 03baee8ca5..2990d3917f 100644 --- a/ui/src/lib/api/http-client.ts +++ b/ui/src/lib/api/http-client.ts @@ -5,11 +5,11 @@ import { checkDetailOf, toCheck, toCheckEvidence, toCheckSql } from './http-chec import { toCatalogNodes } from './http-catalog'; import { toSnapshotChanges } from './http-changes'; import { jobDetailOf, toJob, toTick, toTrigger, triggerDetailOf } from './http-jobs'; -import { RUN_WINDOW, runDetailOf, runRowPage, runsMatching, runTabCountsOf, toRunNode, toRuns } from './http-runs'; +import { RUN_WINDOW, toAdhocCheck, runDetailOf, runRowPage, runsMatching, runTabCountsOf, toRunNode, toRuns } from './http-runs'; import type { ChangeSubscription, ChangedChannel, ChangedSignal, ConnectionState } from './signals'; import type { AuthMode, Bootstrap, Capability, Location, Scope } from './types'; import type { CatalogNode, Check, CheckDetail, CheckEvidence, CheckSql, SnapshotChange } from './types/build'; -import type { Job, JobDetail, RerunMode, Run, RunDetail, RunListQuery, RunRowPage, RunRowQuery, RunTabCounts, Trigger, TriggerDetail, TriggerState } from './types/operate'; +import type { AdhocRunCheck, AdhocRunRequest, Job, JobDetail, RerunMode, SnapshotNode, Run, RunDetail, RunListQuery, RunRowPage, RunRowQuery, RunTabCounts, Trigger, TriggerDetail, TriggerState } from './types/operate'; type Wire = components['schemas']; @@ -38,6 +38,9 @@ const SUPPORTED: ReadonlySet = new Set([ 'getCheckSql', 'getCheckEvidence', 'listCatalogNodes', + 'listSnapshotNodes', + 'checkAdhocRun', + 'launchAdhocRun', 'listSnapshotChanges' ]); @@ -246,6 +249,26 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt if (!data) throw new ApiError(errorText(error, response), response.status); return toCatalogNodes(data); }, + async listSnapshotNodes(scope: Scope): Promise { + const nodes = await implemented.listCatalogNodes(scope); + return nodes.map((n) => ({ key: n.key, kind: n.kind, group: n.group ?? '', tags: n.tags, path: n.path, cursor: null, upstream: n.upstream })); + }, + async checkAdhocRun(scope: Scope, request: AdhocRunRequest): Promise { + const { locationId, target } = parseLocationKey(scope.locationId); + const body = { location_id: locationId, target, command: request.command, priority: request.priority }; + const { data, error, response } = await http.POST('/api/control/runs/adhoc/check', { body, params: { header: { 'x-sqb-csrf': csrfToken(document.cookie) } } }); + if (response.status === 401) onUnauthorized(); + if (!data) throw new ApiError(errorText(error, response), response.status); + return toAdhocCheck(data); + }, + async launchAdhocRun(scope: Scope, request: AdhocRunRequest): Promise { + const { locationId, target } = parseLocationKey(scope.locationId); + const body = { run_id: crypto.randomUUID(), location_id: locationId, target, command: request.command, priority: request.priority, reason: 'Started from the web UI' }; + const { error, response } = await http.POST('/api/control/runs/adhoc', { body, params: { header: { 'x-sqb-csrf': csrfToken(document.cookie) } } }); + if (response.status === 401) onUnauthorized(); + if (!response.ok) throw new ApiError(errorText(error, response), response.status); + return requireRun(scope, body.run_id); + }, async listSnapshotChanges(scope: Scope, query: { limit?: number } = {}): Promise { const { locationId, target } = parseLocationKey(scope.locationId); const [snapshots, runs] = await Promise.all([ diff --git a/ui/src/lib/api/http-runs.ts b/ui/src/lib/api/http-runs.ts index 86fb68311f..782748807c 100644 --- a/ui/src/lib/api/http-runs.ts +++ b/ui/src/lib/api/http-runs.ts @@ -1,7 +1,7 @@ import type { components } from '$control/schema.d.ts'; import { isActiveRun } from '$lib/status'; -import type { Principal } from './types'; -import type { NodeCounts, Run, RunDetail, RunListQuery, RunNode, RunNodeStatus, RunRow, RunRowPage, RunRowQuery, RunTabCounts } from './types/operate'; +import type { Capability, Principal } from './types'; +import type { AdhocRunCheck, LaunchVerb, NodeCounts, Run, RunDetail, RunListQuery, RunNode, RunNodeStatus, RunRow, RunRowPage, RunRowQuery, RunTabCounts } from './types/operate'; type Wire = components['schemas']; @@ -10,6 +10,19 @@ export const RUN_WINDOW = 1000; const PERSON = 'principal:'; +/** The server's dry run of an ad-hoc command. */ +export function toAdhocCheck(wire: Wire['AdhocCheck']): AdhocRunCheck { + return { + verb: (wire.verb ?? null) as LaunchVerb | null, + selection: wire.selection, + writeSet: wire.write_set, + settings: wire.settings, + permissions: wire.permissions.map((p) => ({ action: p.action as Capability, allowed: p.allowed, reason: p.reason ?? null })), + error: wire.error ?? null, + launchable: wire.launchable + }; +} + /** Who requested a run: a person by display name and principal ID, otherwise the trigger or system actor. */ export function requesterOf(wire: Wire['RunRecord']): Principal { if (wire.requested_by.startsWith(PERSON)) { diff --git a/ui/src/lib/api/mock/access.ts b/ui/src/lib/api/mock/access.ts index bdc0f09599..3ad8df2bb2 100644 --- a/ui/src/lib/api/mock/access.ts +++ b/ui/src/lib/api/mock/access.ts @@ -114,9 +114,10 @@ const PLAIN: Record = { 'run.rerun': 'rerun runs', 'run.rewrite_history': 'full refresh or reprocess past data', 'run.prioritise': 'run ahead of the queue', + 'run.warehouse': 'choose the warehouse a run uses', 'trigger.toggle': 'pause and resume schedules and sensors', 'sensor.reset_cursor': 'reset where a sensor is up to', - 'run.adhoc': 'build any selection of nodes', + 'run.adhoc': 'run sqb commands on any selection of nodes', 'connection.test': 'test warehouse connections', 'plan.preview': 'preview build plans', 'location.manage': 'manage locations', diff --git a/ui/src/lib/api/mock/operate-store.ts b/ui/src/lib/api/mock/operate-store.ts index 813a2bc267..7f814a49db 100644 --- a/ui/src/lib/api/mock/operate-store.ts +++ b/ui/src/lib/api/mock/operate-store.ts @@ -2,10 +2,12 @@ // its claims, jobs, triggers and sensors. A small loop advances live runs so // cancels, launches and claims behave like they do on the server. -import { flagSpec } from '$lib/run/command-table'; +import { parseCommand } from '$lib/run/command'; +import { flagSpec, VERBS } from '$lib/run/command-table'; import { ApiError } from '../client'; import type { LocalChangeBus } from '../signals'; import type { + AdhocPermission, AdhocRunCheck, AdhocRunRequest, BuiltInRole, @@ -14,6 +16,7 @@ import type { JobDetail, JobGroupMember, JobLaunchOptions, + LaunchVerb, OverlapOutcome, Principal, QueueEntry, @@ -501,20 +504,21 @@ export class MockOperate { launchAdhocRun(scope: Scope, request: AdhocRunRequest): Promise { const check = this.#check(scope, request); - if (!check.adhoc.allowed) throw new ApiError(`Refused: run.adhoc doesn't cover ${check.adhoc.outOfScope.join(', ') || 'this run'}`, 403); - if (check.rewriteHistory.required && !check.rewriteHistory.allowed) throw new ApiError('Refused: requires run.rewrite_history', 403); - if (!check.ok) throw new ApiError(check.problems[0]?.message ?? 'Invalid request', 422); + if (check.error) throw new ApiError(check.error, 400); + const denied = check.permissions.find((p) => !p.allowed); + if (denied) throw new ApiError(denied.reason ?? `missing ${denied.action} permission for this target`, 403); + const parts = parseCommand(request.command.join(' ')); const rec = this.#create({ id: this.#newId(), origin: 'manual', jobKey: null, by: this.#ctx.user(), - select: request.select, - exclude: request.exclude, - flags: request.flags, - nodes: check.writeSet.length ? check.writeSet : check.resolvedNodes.filter((n) => snapshotNode(n)?.kind !== 'source'), - priority: 0, - verb: request.verb + select: parts.select, + exclude: parts.exclude, + flags: parts.flags, + nodes: check.writeSet.length ? check.writeSet : check.selection.filter((n) => snapshotNode(n)?.kind !== 'source'), + priority: request.priority, + verb: parts.verb }); this.#ctx.changes.emit('runs'); return this.#ctx.reply(this.#view(rec, scope)); @@ -864,7 +868,7 @@ export class MockOperate { priority: number; parent?: string; rerunOf?: string; - verb?: AdhocRunRequest['verb']; + verb?: LaunchVerb; }): RunRecord { const now = new Date().toISOString(); const job = jobDef(spec.jobKey); @@ -887,7 +891,7 @@ export class MockOperate { rerunOfRunId: spec.rerunOf ?? null, requestedBy: spec.by, priority: spec.priority, - rewriteHistory: Object.keys(spec.flags).some((f) => flagSpec(spec.verb ?? 'build', f)?.rewritesHistory), + rewriteHistory: Object.keys(spec.flags).some((f) => flagSpec(spec.verb ?? 'build', f)?.requires === 'run.rewrite_history'), locationId: '', target: '', snapshotId: 'snap-7d2e41a', @@ -926,49 +930,44 @@ export class MockOperate { if (!this.#ctx.can(capability)) throw new ApiError(`Missing permission: ${capability}`, 403); } + /** The server's dry run, simplified: the same shape, verbs and permission rules. */ #check(scope: Scope, request: AdhocRunRequest): AdhocRunCheck { - const problems: AdhocRunCheck['problems'] = []; - const resolution = resolveSelection(request.select, request.exclude); - for (const term of resolution.unknown) problems.push({ code: 'unknown_selector', message: `No node or selector matches ${term}` }); - if (!resolution.nodes.length) problems.push({ code: 'empty_selection', message: 'The selection resolves to no nodes' }); + void scope; + const refused = (error: string): AdhocRunCheck => ({ verb: null, selection: [], writeSet: [], settings: {}, permissions: [], error, launchable: false }); + const verbName = request.command[0] ?? ''; + if (!VERBS.some((v) => v.name === verbName)) return refused(`sqb ${verbName} is not available for ad-hoc runs`); + const parts = parseCommand(request.command.join(' ')); + const unknownFlag = Object.keys(parts.flags).find((f) => !flagSpec(parts.verb, f)); + if (unknownFlag) return refused(`${unknownFlag} is not available for ad-hoc runs`); + const resolution = resolveSelection(parts.select, parts.exclude); + if (resolution.unknown.length) return refused(`no nodes match selector '${resolution.unknown[0]}'`); + if (!resolution.nodes.length) return refused('the selection resolves to no nodes'); const writable = resolution.nodes.filter((n) => snapshotNode(n)?.kind !== 'source'); const writeSet = - request.verb === 'build' - ? writable.filter((n) => !(request.flags['--no-load'] && snapshotNode(n)?.kind === 'loader')) - : request.verb === 'seed' + parts.verb === 'build' + ? writable.filter((n) => !(parts.flags['--no-load'] && snapshotNode(n)?.kind === 'loader')) + : parts.verb === 'seed' ? writable.filter((n) => snapshotNode(n)?.kind === 'seed') - : request.verb === 'load' - ? writable.filter((n) => snapshotNode(n)?.kind === 'loader') - : []; - const reasons: string[] = []; - for (const [flag, value] of Object.entries(request.flags)) { - const spec = flagSpec(request.verb, flag); - if (!spec) { - problems.push({ code: 'unknown_flag', message: `sqb ${request.verb} has no flag ${flag}` }); - continue; - } - if (spec.type !== 'bool' && value === true) problems.push({ code: 'missing_value', message: `${flag} needs a value` }); - if (spec.type === 'int' && value !== true && !/^\d+$/.test(value)) problems.push({ code: 'invalid_value', message: `${flag} must be a whole number` }); - if (spec.type === 'timestamp' && value !== true && Number.isNaN(Date.parse(value))) problems.push({ code: 'invalid_value', message: `${flag} must be a timestamp, e.g. 2026-10-01T00:00:00Z` }); - if (spec.rewritesHistory) reasons.push(flag); - if (flag.includes('cursor') && !writeSet.some((n) => snapshotNode(n)?.cursor)) problems.push({ code: 'no_cursor', message: `${flag}: no selected node has a cursor` }); - } + : []; const scopeTerms = ADHOC_SCOPE[this.#ctx.role()]; const granted = scopeTerms === null ? null : new Set(writeSetOf(scopeTerms.length ? scopeTerms : ['__none__'], [])); - const outOfScope = !this.#ctx.can('run.adhoc') ? writeSet : granted === null ? [] : writeSet.filter((n) => !granted.has(n)); - const allowed = this.#ctx.can('run.adhoc') && outOfScope.length === 0; - const holders = this.#records.filter((r) => HOLDING.has(r.run.status)); - const claimConflicts = writeSet.flatMap((n) => holders.filter((h) => h.script.nodes.includes(n)).map((h) => ({ runId: h.run.id, nodeKey: n }))); - const rewriteAllowed = this.#ctx.can('run.rewrite_history'); - void scope; + const covered = this.#ctx.can('run.adhoc') && (granted === null || resolution.nodes.every((n) => granted.has(n))); + const decide = (action: Capability, allowed: boolean): AdhocPermission => ({ action, allowed, reason: allowed ? null : `missing ${action} permission for this target` }); + const gates = [...new Set(Object.keys(parts.flags).flatMap((f) => flagSpec(parts.verb, f)?.requires ?? []))]; + const permissions = [ + decide('run.adhoc', covered), + ...gates.map((action) => decide(action, this.#ctx.can(action))), + ...(request.priority > 0 ? [decide('run.prioritise', this.#ctx.can('run.prioritise'))] : []) + ]; + const error = parts.verb === 'build' ? null : `workers do not run sqb ${parts.verb} yet; only build can launch`; return { - resolvedNodes: resolution.nodes, + verb: parts.verb, + selection: resolution.nodes, writeSet, - adhoc: { required: 'run.adhoc', allowed, outOfScope }, - rewriteHistory: { required: reasons.length > 0, reasons, allowed: rewriteAllowed }, - claimConflicts, - problems, - ok: problems.length === 0 && allowed && (!reasons.length || rewriteAllowed) + settings: Object.fromEntries(Object.entries(parts.flags).map(([f, v]) => [f.replace(/^--/, '').replaceAll('-', '_'), v])), + permissions, + error, + launchable: error === null && permissions.every((p) => p.allowed) }; } diff --git a/ui/src/lib/api/types.ts b/ui/src/lib/api/types.ts index e1bf968858..da588447be 100644 --- a/ui/src/lib/api/types.ts +++ b/ui/src/lib/api/types.ts @@ -19,6 +19,7 @@ export const CAPABILITIES = [ 'run.rerun', 'run.rewrite_history', 'run.prioritise', + 'run.warehouse', 'trigger.toggle', 'sensor.reset_cursor', 'run.adhoc', diff --git a/ui/src/lib/api/types/operate.ts b/ui/src/lib/api/types/operate.ts index 5d0a831c31..27fe6998d1 100644 --- a/ui/src/lib/api/types/operate.ts +++ b/ui/src/lib/api/types/operate.ts @@ -448,30 +448,31 @@ export type SnapshotNode = { // ---- ad-hoc runs (the run panel) ---- -export type LaunchVerb = 'build' | 'test' | 'seed' | 'load' | 'audit' | 'freshness'; +export type LaunchVerb = 'build' | 'test' | 'seed' | 'audit' | 'freshness'; +/** A command as argument tokens after `sqb`, never shell text; the server resolves its selectors. */ export type AdhocRunRequest = { - verb: LaunchVerb; - select: string[]; - exclude: string[]; - /** Other flags exactly as typed, e.g. `{ '--full-refresh': true, '--max-microbatches': '4' }`. */ - flags: Record; + command: string[]; + /** Above 0 the run jumps the queue and needs `run.prioritise`. */ + priority: number; }; -export type RunCheckProblem = { code: string; message: string }; +/** One permission the run needs and whether the person holds it on every resolved node. */ +export type AdhocPermission = { action: Capability; allowed: boolean; reason: string | null }; +/** The server's dry run: what launching would run and whether this person may launch it. */ export type AdhocRunCheck = { - resolvedNodes: string[]; - /** Nodes the run would write; empty for read-only verbs. */ + verb: LaunchVerb | null; + /** The exact nodes the worker would run. */ + selection: string[]; + /** Nodes the run would write and reserve; empty for read-only verbs. */ writeSet: string[]; - /** `run.adhoc` must cover every node in the write set; otherwise the whole request is refused. */ - adhoc: { required: Capability; allowed: boolean; outOfScope: string[] }; - rewriteHistory: { required: boolean; reasons: string[]; allowed: boolean }; - /** Active runs holding write claims on nodes in the write set: the run would queue behind them. */ - claimConflicts: { runId: string; nodeKey: string }[]; - problems: RunCheckProblem[]; - /** Launchable as requested. */ - ok: boolean; + /** Accepted flags as the worker receives them, by setting name. */ + settings: Record; + permissions: AdhocPermission[]; + /** Why the command cannot run regardless of permissions, such as a refused flag or a verb workers can't run yet. */ + error: string | null; + launchable: boolean; }; /** Cursor window for a launch, typed from the selected nodes' cursor type. */ diff --git a/ui/src/lib/platform/permission-catalog.ts b/ui/src/lib/platform/permission-catalog.ts index 8292fcaef6..78e2caffe6 100644 --- a/ui/src/lib/platform/permission-catalog.ts +++ b/ui/src/lib/platform/permission-catalog.ts @@ -25,9 +25,10 @@ export const PERMISSIONS: Record = { risk: 'changes_data' }, 'run.prioritise': { label: 'Put runs ahead of the queue', description: 'Start or rerun a run with a priority above 0, so it is claimed before waiting runs.', risk: 'operate' }, + 'run.warehouse': { label: 'Choose a run’s warehouse', description: 'Start an ad-hoc run on another warehouse configured for the target, which can change cost and speed.', risk: 'operate' }, 'trigger.toggle': { label: 'Pause and resume schedules and sensors', description: 'Stop or restart automatic runs.', risk: 'operate' }, 'sensor.reset_cursor': { label: 'Reset where a sensor is up to', description: 'Move a sensor’s position so it looks at events again.', risk: 'operate' }, - 'run.adhoc': { label: 'Build any selection of nodes', description: 'Start an ad-hoc run of nodes outside a declared job, writing to the warehouse.', risk: 'changes_data' }, + 'run.adhoc': { label: 'Run ad-hoc commands', description: 'Run sqb build and other commands on any selection of nodes outside a declared job, writing to the warehouse.', risk: 'changes_data' }, 'connection.test': { label: 'Test warehouse connections', description: 'Run the connection check from a location.', risk: 'operate' }, 'plan.preview': { label: 'Preview build plans', description: 'See what a run would build without running it.', risk: 'read' }, 'location.manage': { label: 'Manage locations and their tokens', description: 'Register locations and create, rotate or revoke their tokens.', risk: 'administer' }, diff --git a/ui/src/lib/run/command-table.generated.json b/ui/src/lib/run/command-table.generated.json index 7186bfc624..7a49b42549 100644 --- a/ui/src/lib/run/command-table.generated.json +++ b/ui/src/lib/run/command-table.generated.json @@ -6,51 +6,13 @@ "description": "Build the selected nodes, running their tests and audits", "flags": [ { - "flag": "--no-sql-analysis", - "aliases": [ - "--no-sql-validation" - ], - "type": "bool", - "choices": null, - "repeatable": false, - "rewritesHistory": false, - "description": "disable SQL syntax, binding, type inference, and semantic validation" - }, - { - "flag": "--no-cache", + "flag": "--selection-diagnostics", "aliases": [], "type": "bool", "choices": null, "repeatable": false, - "rewritesHistory": false, - "description": "Bypass the compile analysis cache for this invocation" - }, - { - "flag": "--defer-to", - "aliases": [], - "type": "string", - "choices": null, - "repeatable": false, - "rewritesHistory": false, - "description": "Read unselected upstream nodes from another target" - }, - { - "flag": "--defer-clone-from", - "aliases": [], - "type": "string", - "choices": null, - "repeatable": false, - "rewritesHistory": false, - "description": "Clone unselected upstream nodes from another target" - }, - { - "flag": "--defer-sources-to", - "aliases": [], - "type": "string", - "choices": null, - "repeatable": false, - "rewritesHistory": false, - "description": "Read sources from another target" + "requires": null, + "description": "Include direct-mode stale-out-of-selection diagnostics." }, { "flag": "--no-python", @@ -58,7 +20,7 @@ "type": "bool", "choices": null, "repeatable": false, - "rewritesHistory": false, + "requires": null, "description": "Skip Python nodes" }, { @@ -67,7 +29,7 @@ "type": "bool", "choices": null, "repeatable": false, - "rewritesHistory": false, + "requires": null, "description": "Skip unit tests during the build" }, { @@ -76,7 +38,7 @@ "type": "bool", "choices": null, "repeatable": false, - "rewritesHistory": false, + "requires": null, "description": "Skip audits during the build" }, { @@ -85,7 +47,7 @@ "type": "string", "choices": null, "repeatable": false, - "rewritesHistory": true, + "requires": "run.rewrite_history", "description": "override the start cursor, typed from the selected models' cursor type" }, { @@ -94,7 +56,7 @@ "type": "string", "choices": null, "repeatable": false, - "rewritesHistory": true, + "requires": "run.rewrite_history", "description": "override the end cursor, typed from the selected models' cursor type" }, { @@ -103,7 +65,7 @@ "type": "timestamp", "choices": null, "repeatable": false, - "rewritesHistory": true, + "requires": "run.rewrite_history", "description": "timestamp start cursor override" }, { @@ -112,7 +74,7 @@ "type": "timestamp", "choices": null, "repeatable": false, - "rewritesHistory": true, + "requires": "run.rewrite_history", "description": "timestamp end cursor override" }, { @@ -121,7 +83,7 @@ "type": "int", "choices": null, "repeatable": false, - "rewritesHistory": true, + "requires": "run.rewrite_history", "description": "integer start cursor override" }, { @@ -130,7 +92,7 @@ "type": "int", "choices": null, "repeatable": false, - "rewritesHistory": true, + "requires": "run.rewrite_history", "description": "integer end cursor override" }, { @@ -139,43 +101,25 @@ "type": "int", "choices": null, "repeatable": false, - "rewritesHistory": false, + "requires": null, "description": "Process at most this many microbatches per incremental model" }, - { - "flag": "--load", - "aliases": [], - "type": "bool", - "choices": null, - "repeatable": false, - "rewritesHistory": false, - "description": "Run loaders as part of the build" - }, { "flag": "--no-load", "aliases": [], "type": "bool", "choices": null, "repeatable": false, - "rewritesHistory": false, + "requires": null, "description": "Skip loaders" }, - { - "flag": "--reload", - "aliases": [], - "type": "bool", - "choices": null, - "repeatable": false, - "rewritesHistory": true, - "description": "Reload loader sources from the beginning" - }, { "flag": "--fail-fast", "aliases": [], "type": "bool", "choices": null, "repeatable": false, - "rewritesHistory": false, + "requires": null, "description": "Stop at the first failed node" }, { @@ -184,7 +128,7 @@ "type": "bool", "choices": null, "repeatable": false, - "rewritesHistory": true, + "requires": "run.rewrite_history", "description": "Rebuild incremental models and snapshots from scratch" }, { @@ -193,7 +137,7 @@ "type": "bool", "choices": null, "repeatable": false, - "rewritesHistory": true, + "requires": "run.rewrite_history", "description": "Allow a full refresh of snapshot models" }, { @@ -202,7 +146,7 @@ "type": "bool", "choices": null, "repeatable": false, - "rewritesHistory": false, + "requires": "run.rewrite_history", "description": "Allow replacing a table with a less durable table type" }, { @@ -211,25 +155,16 @@ "type": "bool", "choices": null, "repeatable": false, - "rewritesHistory": false, + "requires": "run.rewrite_history", "description": "Allow lowering a table retention setting" }, - { - "flag": "--allow-missing-migration-origin", - "aliases": [], - "type": "bool", - "choices": null, - "repeatable": false, - "rewritesHistory": false, - "description": "Allow a migration whose origin relation no longer exists" - }, { "flag": "--allow-snapshot-schema-change", "aliases": [], "type": "bool", "choices": null, "repeatable": false, - "rewritesHistory": false, + "requires": "run.rewrite_history", "description": "Allow a schema change on snapshot models" }, { @@ -238,7 +173,7 @@ "type": "int", "choices": null, "repeatable": false, - "rewritesHistory": false, + "requires": null, "description": "Nodes to run in parallel within this run" }, { @@ -247,7 +182,7 @@ "type": "string", "choices": null, "repeatable": false, - "rewritesHistory": false, + "requires": null, "description": "Per-test timeout for unit tests this build runs, such as '30s', or 'disabled'" }, { @@ -258,7 +193,7 @@ "type": "bool", "choices": null, "repeatable": false, - "rewritesHistory": false, + "requires": null, "description": "More detailed output" }, { @@ -269,8 +204,8 @@ "type": "selector", "choices": null, "repeatable": true, - "rewritesHistory": false, - "description": "Selector expression, e.g. +fact_orders, tag:finance, group:marts" + "requires": null, + "description": "Selector expression, e.g. +fact_orders, tag:finance, path:models/marts/" }, { "flag": "--exclude", @@ -278,25 +213,16 @@ "type": "selector", "choices": null, "repeatable": true, - "rewritesHistory": false, + "requires": null, "description": "Selector expression to leave out" }, - { - "flag": "--vars", - "aliases": [], - "type": "string", - "choices": null, - "repeatable": false, - "rewritesHistory": false, - "description": "Project variables as a JSON or key=value mapping" - }, { "flag": "--warehouse", "aliases": [], "type": "string", "choices": null, "repeatable": false, - "rewritesHistory": false, + "requires": "run.warehouse", "description": "warehouse for this invocation; overrides the target's command-group warehouse and the connection warehouse" } ] @@ -305,24 +231,13 @@ "name": "test", "description": "Run unit and scenario tests for the selection", "flags": [ - { - "flag": "--no-sql-analysis", - "aliases": [ - "--no-sql-validation" - ], - "type": "bool", - "choices": null, - "repeatable": false, - "rewritesHistory": false, - "description": "disable SQL syntax, binding, type inference, and semantic validation" - }, { "flag": "--concurrency", "aliases": [], "type": "int", "choices": null, "repeatable": false, - "rewritesHistory": false, + "requires": null, "description": "Nodes to run in parallel within this run" }, { @@ -331,7 +246,7 @@ "type": "string", "choices": null, "repeatable": false, - "rewritesHistory": false, + "requires": null, "description": "Per-test timeout for this run, such as '30s' or '2m', or 'disabled'; overrides TEST() and project settings" }, { @@ -340,61 +255,17 @@ "type": "string", "choices": null, "repeatable": false, - "rewritesHistory": false, + "requires": null, "description": "Run one named parameter case" }, { - "flag": "--select", - "aliases": [ - "-s" - ], - "type": "selector", - "choices": null, - "repeatable": true, - "rewritesHistory": false, - "description": "Selector expression, e.g. +fact_orders, tag:finance, group:marts" - }, - { - "flag": "--exclude", - "aliases": [], - "type": "selector", - "choices": null, - "repeatable": true, - "rewritesHistory": false, - "description": "Selector expression to leave out" - }, - { - "flag": "--vars", + "flag": "--inspect", "aliases": [], - "type": "string", - "choices": null, - "repeatable": false, - "rewritesHistory": false, - "description": "Project variables as a JSON or key=value mapping" - }, - { - "flag": "--warehouse", - "aliases": [], - "type": "string", - "choices": null, - "repeatable": false, - "rewritesHistory": false, - "description": "warehouse for this invocation; overrides the target's command-group warehouse and the connection warehouse" - } - ] - }, - { - "name": "seed", - "description": "Load seed files", - "flags": [ - { - "flag": "--concurrency", - "aliases": [], - "type": "int", + "type": "bool", "choices": null, "repeatable": false, - "rewritesHistory": false, - "description": "Nodes to run in parallel within this run" + "requires": null, + "description": "Show the resolved test plan" }, { "flag": "--select", @@ -404,8 +275,8 @@ "type": "selector", "choices": null, "repeatable": true, - "rewritesHistory": false, - "description": "Selector expression, e.g. +fact_orders, tag:finance, group:marts" + "requires": null, + "description": "Selector expression, e.g. +fact_orders, tag:finance, path:models/marts/" }, { "flag": "--exclude", @@ -413,49 +284,31 @@ "type": "selector", "choices": null, "repeatable": true, - "rewritesHistory": false, + "requires": null, "description": "Selector expression to leave out" }, - { - "flag": "--vars", - "aliases": [], - "type": "string", - "choices": null, - "repeatable": false, - "rewritesHistory": false, - "description": "Project variables as a JSON or key=value mapping" - }, { "flag": "--warehouse", "aliases": [], "type": "string", "choices": null, "repeatable": false, - "rewritesHistory": false, + "requires": "run.warehouse", "description": "warehouse for this invocation; overrides the target's command-group warehouse and the connection warehouse" } ] }, { - "name": "load", - "description": "Run loaders into their source tables", + "name": "seed", + "description": "Load seed files", "flags": [ - { - "flag": "--reload", - "aliases": [], - "type": "bool", - "choices": null, - "repeatable": false, - "rewritesHistory": true, - "description": "Reload loader sources from the beginning" - }, { "flag": "--concurrency", "aliases": [], "type": "int", "choices": null, "repeatable": false, - "rewritesHistory": false, + "requires": null, "description": "Nodes to run in parallel within this run" }, { @@ -466,8 +319,8 @@ "type": "selector", "choices": null, "repeatable": true, - "rewritesHistory": false, - "description": "Selector expression, e.g. +fact_orders, tag:finance, group:marts" + "requires": null, + "description": "Selector expression, e.g. +fact_orders, tag:finance, path:models/marts/" }, { "flag": "--exclude", @@ -475,79 +328,16 @@ "type": "selector", "choices": null, "repeatable": true, - "rewritesHistory": false, + "requires": null, "description": "Selector expression to leave out" }, - { - "flag": "--start-cursor", - "aliases": [], - "type": "string", - "choices": null, - "repeatable": false, - "rewritesHistory": true, - "description": "override the start cursor, typed from the selected models' cursor type" - }, - { - "flag": "--end-cursor", - "aliases": [], - "type": "string", - "choices": null, - "repeatable": false, - "rewritesHistory": true, - "description": "override the end cursor, typed from the selected models' cursor type" - }, - { - "flag": "--start-cursor-ts", - "aliases": [], - "type": "timestamp", - "choices": null, - "repeatable": false, - "rewritesHistory": true, - "description": "timestamp start cursor override" - }, - { - "flag": "--end-cursor-ts", - "aliases": [], - "type": "timestamp", - "choices": null, - "repeatable": false, - "rewritesHistory": true, - "description": "timestamp end cursor override" - }, - { - "flag": "--start-cursor-int", - "aliases": [], - "type": "int", - "choices": null, - "repeatable": false, - "rewritesHistory": true, - "description": "integer start cursor override" - }, - { - "flag": "--end-cursor-int", - "aliases": [], - "type": "int", - "choices": null, - "repeatable": false, - "rewritesHistory": true, - "description": "integer end cursor override" - }, - { - "flag": "--vars", - "aliases": [], - "type": "string", - "choices": null, - "repeatable": false, - "rewritesHistory": false, - "description": "Project variables as a JSON or key=value mapping" - }, { "flag": "--warehouse", "aliases": [], "type": "string", "choices": null, "repeatable": false, - "rewritesHistory": false, + "requires": "run.warehouse", "description": "warehouse for this invocation; overrides the target's command-group warehouse and the connection warehouse" } ] @@ -556,33 +346,13 @@ "name": "audit", "description": "Evaluate audits against what is already built", "flags": [ - { - "flag": "--no-sql-analysis", - "aliases": [ - "--no-sql-validation" - ], - "type": "bool", - "choices": null, - "repeatable": false, - "rewritesHistory": false, - "description": "disable SQL syntax, binding, type inference, and semantic validation" - }, - { - "flag": "--defer-to", - "aliases": [], - "type": "string", - "choices": null, - "repeatable": false, - "rewritesHistory": false, - "description": "Read unselected upstream nodes from another target" - }, { "flag": "--concurrency", "aliases": [], "type": "int", "choices": null, "repeatable": false, - "rewritesHistory": false, + "requires": null, "description": "Nodes to run in parallel within this run" }, { @@ -593,8 +363,8 @@ "type": "selector", "choices": null, "repeatable": true, - "rewritesHistory": false, - "description": "Selector expression, e.g. +fact_orders, tag:finance, group:marts" + "requires": null, + "description": "Selector expression, e.g. +fact_orders, tag:finance, path:models/marts/" }, { "flag": "--exclude", @@ -602,25 +372,16 @@ "type": "selector", "choices": null, "repeatable": true, - "rewritesHistory": false, + "requires": null, "description": "Selector expression to leave out" }, - { - "flag": "--vars", - "aliases": [], - "type": "string", - "choices": null, - "repeatable": false, - "rewritesHistory": false, - "description": "Project variables as a JSON or key=value mapping" - }, { "flag": "--warehouse", "aliases": [], "type": "string", "choices": null, "repeatable": false, - "rewritesHistory": false, + "requires": "run.warehouse", "description": "warehouse for this invocation; overrides the target's command-group warehouse and the connection warehouse" } ] @@ -629,24 +390,13 @@ "name": "freshness", "description": "Check source freshness", "flags": [ - { - "flag": "--no-sql-analysis", - "aliases": [ - "--no-sql-validation" - ], - "type": "bool", - "choices": null, - "repeatable": false, - "rewritesHistory": false, - "description": "disable SQL syntax, binding, type inference, and semantic validation" - }, { "flag": "--state", "aliases": [], "type": "bool", "choices": null, "repeatable": false, - "rewritesHistory": false, + "requires": null, "description": "Record freshness results in the state tables" }, { @@ -655,7 +405,7 @@ "type": "bool", "choices": null, "repeatable": false, - "rewritesHistory": false, + "requires": null, "description": "Fail when a freshness check errors" }, { @@ -664,7 +414,7 @@ "type": "bool", "choices": null, "repeatable": false, - "rewritesHistory": false, + "requires": null, "description": "Fail when a source is stale" }, { @@ -675,8 +425,8 @@ "type": "selector", "choices": null, "repeatable": true, - "rewritesHistory": false, - "description": "Selector expression, e.g. +fact_orders, tag:finance, group:marts" + "requires": null, + "description": "Selector expression, e.g. +fact_orders, tag:finance, path:models/marts/" }, { "flag": "--exclude", @@ -684,25 +434,16 @@ "type": "selector", "choices": null, "repeatable": true, - "rewritesHistory": false, + "requires": null, "description": "Selector expression to leave out" }, - { - "flag": "--vars", - "aliases": [], - "type": "string", - "choices": null, - "repeatable": false, - "rewritesHistory": false, - "description": "Project variables as a JSON or key=value mapping" - }, { "flag": "--warehouse", "aliases": [], "type": "string", "choices": null, "repeatable": false, - "rewritesHistory": false, + "requires": "run.warehouse", "description": "warehouse for this invocation; overrides the target's command-group warehouse and the connection warehouse" } ] diff --git a/ui/src/lib/run/command-table.ts b/ui/src/lib/run/command-table.ts index c8266b043f..27bbb8e1a4 100644 --- a/ui/src/lib/run/command-table.ts +++ b/ui/src/lib/run/command-table.ts @@ -1,7 +1,7 @@ -// The launchable verbs and flags, generated from the CLI's command table by -// `npm run gen:command-table`. Never hand-edit the generated JSON. +// The ad-hoc verbs and flags, generated from the CLI's command table and ad-hoc +// allowlist by `npm run gen:command-table`. Never hand-edit the generated JSON. -import type { LaunchVerb } from '$lib/api'; +import type { Capability, LaunchVerb } from '$lib/api'; import table from './command-table.generated.json'; export type FlagType = 'bool' | 'string' | 'int' | 'timestamp' | 'selector'; @@ -12,7 +12,8 @@ export type FlagSpec = { type: FlagType; choices: string[] | null; repeatable: boolean; - rewritesHistory: boolean; + /** The permission beyond run.adhoc that setting this flag needs, as the server decides it. */ + requires: Capability | null; description: string; }; diff --git a/ui/src/lib/run/command.test.ts b/ui/src/lib/run/command.test.ts new file mode 100644 index 0000000000..f52159bd19 --- /dev/null +++ b/ui/src/lib/run/command.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest'; +import type { SnapshotNode } from '$lib/api'; +import { commandTokens, suggest, withVerb } from './command'; + +const node = (key: string, kind: SnapshotNode['kind'], path: string, tags: string[] = []): SnapshotNode => ({ key, kind, group: '', tags, path, cursor: null, upstream: [] }); + +const NODES: SnapshotNode[] = [ + node('model:customers', 'table', 'models/marts/customers.sql', ['finance']), + node('seed:countries', 'seed', 'seeds/countries.csv'), + node('source:shop.orders', 'source', 'sources.yml') +]; + +describe('the run panel command line', () => { + it('sends the tokens after sqb exactly as typed, for the server to resolve', () => { + expect(commandTokens(' sqb build customers+ --exclude tag:finance --warehouse WH_S ')).toEqual(['build', 'customers+', '--exclude', 'tag:finance', '--warehouse', 'WH_S']); + }); + + it('suggests selectors in the CLI grammar: bare model names, seed: and source: keys, tags and folders', () => { + const values = (typed: string) => suggest(typed, typed.length, NODES).map((s) => s.value); + expect(values('sqb build --select +cust')).toEqual(['+customers']); + expect(values('sqb build --select countr')).toEqual(['seed:countries']); + expect(values('sqb build --select shop.ord')).toEqual(['source:shop.orders']); + expect(values('sqb build --select fin')).toEqual(['tag:finance']); + expect(values('sqb build --select marts')).toEqual(['path:models/marts']); + }); + + it('keeps the selection and only the flags the new verb accepts when switching verbs', () => { + expect(withVerb('sqb build --select customers --full-refresh --warehouse WH_S', 'test')).toBe('sqb test --select customers --warehouse WH_S '); + }); +}); diff --git a/ui/src/lib/run/command.ts b/ui/src/lib/run/command.ts index 5570861b7a..be044befd1 100644 --- a/ui/src/lib/run/command.ts +++ b/ui/src/lib/run/command.ts @@ -1,14 +1,18 @@ -// Parses the run panel's command line into a launch request, and produces -// autocomplete suggestions from the command table and the snapshot's nodes. -// Pure: no side effects. +// Splits the run panel's command line into the tokens the server checks, and +// produces autocomplete suggestions from the command table and the snapshot's +// nodes. The server resolves selectors and refuses flags; nothing here decides +// whether a command may run. Pure: no side effects. -import type { AdhocRunRequest, LaunchVerb, SnapshotNode } from '$lib/api'; +import type { LaunchVerb, SnapshotNode } from '$lib/api'; import { flagSpec, VERBS, type FlagSpec } from './command-table'; -export type ParsedCommand = { - request: AdhocRunRequest; - /** Problems the panel reports before asking the server. */ - errors: string[]; +/** The command line read for the panel's own controls: verb tabs, flag hints and verb switching. */ +export type CommandParts = { + verb: LaunchVerb; + select: string[]; + exclude: string[]; + /** Other flags exactly as typed, e.g. `{ '--full-refresh': true, '--max-microbatches': '4' }`. */ + flags: Record; }; const SELECTOR_FLAGS = new Set(['--select', '-s', '--exclude']); @@ -17,66 +21,56 @@ export function tokenize(text: string): string[] { return text.trim().split(/\s+/).filter(Boolean); } -export function parseCommand(text: string): ParsedCommand { +/** The argument tokens after `sqb`, exactly as the server receives them. */ +export function commandTokens(text: string): string[] { const tokens = tokenize(text); - const errors: string[] = []; - if (tokens[0] === 'sqb') tokens.shift(); + return tokens[0] === 'sqb' ? tokens.slice(1) : tokens; +} + +export function parseCommand(text: string): CommandParts { + const tokens = commandTokens(text); const verbName = tokens.shift() ?? 'build'; - const verb = (VERBS.find((v) => v.name === verbName)?.name ?? 'build') as LaunchVerb; - if (!VERBS.some((v) => v.name === verbName)) errors.push(`${verbName} can't be launched from the panel; use ${VERBS.map((v) => v.name).join(', ')}`); + const verb = VERBS.find((v) => v.name === verbName)?.name ?? 'build'; const select: string[] = []; const exclude: string[] = []; const flags: Record = {}; - let mode: 'select' | 'exclude' | null = 'select'; + let mode: 'select' | 'exclude' = 'select'; for (let i = 0; i < tokens.length; i++) { const token = tokens[i]; if (token.startsWith('-')) { - mode = null; - if (token === '--target') { - errors.push('The target is set by the location; remove --target'); - i++; - continue; - } if (SELECTOR_FLAGS.has(token)) { mode = token === '--exclude' ? 'exclude' : 'select'; continue; } + mode = 'select'; const spec = flagSpec(verb, token); - if (!spec) { - flags[token] = true; - continue; - } - if (spec.type === 'bool') flags[spec.flag] = true; + const value = tokens[i + 1]; + if (!spec || spec.type === 'bool' || value === undefined || value.startsWith('-')) flags[spec?.flag ?? token] = true; else { - const value = tokens[i + 1]; - if (value === undefined || value.startsWith('--')) flags[spec.flag] = true; - else { - flags[spec.flag] = value; - i++; - } + flags[spec.flag] = value; + i++; } continue; } if (mode === 'exclude') exclude.push(token); - else if (mode === 'select') select.push(token); - else errors.push(`Unexpected ${token}; selectors go after --select or --exclude`); + else select.push(token); } - return { request: { verb, select, exclude, flags }, errors }; + return { verb, select, exclude, flags }; } -export function buildCommand(request: AdhocRunRequest): string { - const parts = ['sqb', request.verb]; - if (request.select.length) parts.push('--select', ...request.select); - if (request.exclude.length) parts.push('--exclude', ...request.exclude); - for (const [flag, value] of Object.entries(request.flags)) parts.push(value === true ? flag : `${flag} ${value}`); - return parts.join(' '); +export function buildCommand(parts: CommandParts): string { + const out = ['sqb', parts.verb]; + if (parts.select.length) out.push('--select', ...parts.select); + if (parts.exclude.length) out.push('--exclude', ...parts.exclude); + for (const [flag, value] of Object.entries(parts.flags)) out.push(value === true ? flag : `${flag} ${value}`); + return out.join(' '); } /** Switches the verb, keeping the selection and the flags the new verb accepts. */ export function withVerb(text: string, verb: LaunchVerb): string { - const { request } = parseCommand(text); - const flags = Object.fromEntries(Object.entries(request.flags).filter(([flag]) => flagSpec(verb, flag))); - return buildCommand({ ...request, verb, flags }) + ' '; + const parts = parseCommand(text); + const flags = Object.fromEntries(Object.entries(parts.flags).filter(([flag]) => flagSpec(verb, flag))); + return buildCommand({ ...parts, verb, flags }) + ' '; } export type Suggestion = { value: string; label: string; hint: string }; @@ -93,29 +87,32 @@ export function tokenAt(text: string, caret: number): TokenContext { return { start, end, text: text.slice(start, caret), previousFlag: lastFlag }; } +/** How `sqb` names a node in a selector: seeds and sources by kind, everything else by bare name. */ +export function selectorOf(key: string): string { + return key.startsWith('seed:') || key.startsWith('source:') ? key : key.slice(key.indexOf(':') + 1); +} + function selectorPool(nodes: SnapshotNode[]): { value: string; hint: string }[] { const unique = (values: string[]) => [...new Set(values)].sort(); return [ ...unique(nodes.flatMap((n) => n.tags)).map((t) => ({ value: `tag:${t}`, hint: 'tag' })), - ...unique(nodes.map((n) => n.group)).map((g) => ({ value: `group:${g}`, hint: 'group' })), - ...unique(nodes.map((n) => n.kind)).map((k) => ({ value: `kind:${k}`, hint: 'kind' })), - ...unique(nodes.map((n) => n.path.replace(/\/[^/]+$/, '/'))).map((p) => ({ value: `path:${p}`, hint: 'path' })), - ...nodes.map((n) => ({ value: n.key, hint: n.kind })) + ...unique(nodes.filter((n) => n.path.includes('/')).map((n) => n.path.replace(/\/[^/]+$/, ''))).map((p) => ({ value: `path:${p}`, hint: 'folder' })), + ...nodes.map((n) => ({ value: selectorOf(n.key), hint: n.kind })) ]; } export function suggest(text: string, caret: number, nodes: SnapshotNode[]): Suggestion[] { - const { request } = parseCommand(text); + const parts = parseCommand(text); const ctx = tokenAt(text, caret); - const verb = VERBS.find((v) => v.name === request.verb)!; + const verb = VERBS.find((v) => v.name === parts.verb)!; const tokens = tokenize(text); if (ctx.text.startsWith('-')) { return verb.flags .filter((f) => f.flag.startsWith(ctx.text) && (f.repeatable || !tokens.includes(f.flag) || ctx.text === f.flag)) .slice(0, 14) - .map((f) => ({ value: f.flag, label: f.flag, hint: `${f.type}${f.rewritesHistory ? ' · needs run.rewrite_history' : ''} · ${f.description}` })); + .map((f) => ({ value: f.flag, label: f.flag, hint: `${f.type}${f.requires ? ` · needs ${f.requires}` : ''} · ${f.description}` })); } - const spec: FlagSpec | undefined = ctx.previousFlag ? flagSpec(request.verb, ctx.previousFlag) : undefined; + const spec: FlagSpec | undefined = ctx.previousFlag ? flagSpec(parts.verb, ctx.previousFlag) : undefined; const prevToken = tokenize(text.slice(0, ctx.start)).at(-1) ?? ''; if (spec && spec.type !== 'selector' && prevToken === ctx.previousFlag) { if (spec.choices) return spec.choices.filter((c) => c.startsWith(ctx.text)).map((c) => ({ value: c, label: c, hint: spec.flag })); diff --git a/ui/src/lib/run/run-panel.svelte b/ui/src/lib/run/run-panel.svelte index b993daca1e..90f3030dd5 100644 --- a/ui/src/lib/run/run-panel.svelte +++ b/ui/src/lib/run/run-panel.svelte @@ -1,17 +1,18 @@ - + {#if signingIn} diff --git a/ui/src/routes/login/+page.svelte b/ui/src/routes/login/+page.svelte index 298181bdd4..4727d67cc5 100644 --- a/ui/src/routes/login/+page.svelte +++ b/ui/src/routes/login/+page.svelte @@ -1,4 +1,6 @@ @@ -65,8 +68,8 @@
    {#if m}
    - {#if m.stale.changed}{m.stale.changed} changed{/if} - {#if m.stale.behind}{m.stale.behind} behind{/if} + {#if m.stale?.changed}{m.stale.changed} changed{/if} + {#if m.stale?.behind}{m.stale.behind} behind{/if} {#if m.checks.failing}{m.checks.failing} failing {m.checks.failing === 1 ? 'check' : 'checks'}{/if}
    {/if} @@ -93,7 +96,7 @@ {/if}
    -
    +
    {#if m}
    Models @@ -110,11 +113,13 @@
    {m.checks.passing}of {m.checks.total}
    0}>{m.checks.failing} failing · {m.checks.warning} warning
    -
    - Stale -
    {m.stale.nodes}nodes
    - {m.stale.changed} changed · {m.stale.behind} behind · {m.stale.sources} {m.stale.sources === 1 ? 'source' : 'sources'} late -
    + {#if m.stale} +
    + Stale +
    {m.stale.nodes}nodes
    + {m.stale.changed} changed · {m.stale.behind} behind · {m.stale.sources} {m.stale.sources === 1 ? 'source' : 'sources'} late +
    + {/if} {#if m.cost}
    Cost · 24h @@ -125,16 +130,18 @@ {/if}
    -
    -
    -

    Activity

    Past runs, and when schedules will run next
    - Open full timeline → -
    - -
    + {#if showTimeline} +
    +
    +

    Activity

    Past runs, and when schedules will run next
    + Open full timeline → +
    + +
    + {/if} -
    -
    +
    + {#if showGroups}

    Build status by group

    Current, stale and failed nodes · {app.location?.name}
    Open catalog → @@ -158,7 +165,7 @@
    {overview.loading ? 'Loading…' : 'Nothing deployed yet. Deploy the project to see its models here.'}
    {/each}
    -
    +
    {/if}

    Recent changes

    Recent deploys
    @@ -167,12 +174,12 @@
    {#each data?.recentChanges ?? [] as c (c.snapshotId)} - {c.gitRevision}version {c.generation} + {c.gitRevision?.slice(0, 7) ?? '—'}version {c.generation} {summarizeChange(c)} {c.nodes.slice(0, 3).map((n) => `${changeOpLabel[n.op].toLowerCase()} ${n.key}`).join(' · ') || 'No node changes'} - {c.gitAuthor} · {formatAgo(c.publishedAt, app.now)} + {c.gitAuthor ? `${c.gitAuthor} · ` : ''}{formatAgo(c.publishedAt, app.now)} {:else}
    {overview.loading ? 'Loading…' : 'Nothing deployed yet.'}
    @@ -221,8 +228,7 @@ .health-facts span { display: flex; align-items: center; gap: 6px; } .health-facts b { color: var(--foreground); font-weight: 500; } .health-head > a, .panel-heading > a { margin-left: auto; color: var(--primary); font: 500 12.5px var(--font-sans); } - .metric-strip { display: grid; min-height: 98px; grid-template-columns: repeat(4, minmax(0, 1fr)); overflow: hidden; } - .metric-strip.with-cost { grid-template-columns: repeat(5, minmax(0, 1fr)); } + .metric-strip { display: grid; min-height: 98px; grid-template-columns: repeat(var(--metrics), minmax(0, 1fr)); overflow: hidden; } .metric { min-width: 0; padding: 15px 16px 14px; border-right: 1px solid var(--border); } .metric:last-child { border-right: 0; } .metric-label { display: block; color: var(--muted-foreground); font: 500 12px var(--font-sans); } @@ -234,6 +240,7 @@ .metric .warning-note { color: var(--sb-warning); } .stale-value { color: var(--sb-stale); } .lower-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(360px, 0.9fr); gap: 14px; align-items: start; } + .lower-grid.single { grid-template-columns: 1fr; } .panel-heading { display: flex; min-height: 53px; align-items: center; padding: 0 14px 0 16px; border-bottom: 1px solid var(--border); } .panel-heading > div:first-child { margin-right: auto; } .panel-heading h2 { margin: 0; font: 600 13.5px var(--font-sans); } From 69e9de549f596a876c7e05fd751b55940c6f7d54 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Mon, 9 Nov 2026 00:51:28 +0000 Subject: [PATCH 087/106] feat(ui): tell a partial viewer why the Overview is empty A person with view on only part of a target signed in to a page error, because every listing the Overview reads needs view on the whole target. The client now answers null for that refusal and the page explains it, naming the target and linking the pages that do not list it. Other errors still show as errors, and no partial counts are shown. --- ui/src/lib/api/client.ts | 7 +- ui/src/lib/api/http-client.test.ts | 28 +++ ui/src/lib/api/http-client.ts | 11 +- ui/src/lib/api/mock/build.ts | 2 +- ui/src/lib/nav.test.ts | 10 +- ui/src/lib/nav.ts | 32 ++-- ui/src/routes/+page.svelte | 265 ++++++++++++++++------------- 7 files changed, 217 insertions(+), 138 deletions(-) diff --git a/ui/src/lib/api/client.ts b/ui/src/lib/api/client.ts index e0fb906e31..0e66db6d41 100644 --- a/ui/src/lib/api/client.ts +++ b/ui/src/lib/api/client.ts @@ -56,8 +56,11 @@ export interface ApiClient extends PlatformApi { navCounts(scope: Scope): Promise; // ---- BUILD: overview, catalog and lineage, checks, changes (types/build.ts) ---- - /** Health, attention items, metrics, build status by group and recent changes. */ - overview(scope: Scope): Promise; + /** + * Health, attention items, metrics, build status by group and recent changes; null when the + * person may view only part of the target, because its listings need view on every node. + */ + overview(scope: Scope): Promise; /** Every node in the current snapshot with its build state; also the lineage graph. */ listCatalogNodes(scope: Scope): Promise; getCatalogNode(scope: Scope, key: string): Promise; diff --git a/ui/src/lib/api/http-client.test.ts b/ui/src/lib/api/http-client.test.ts index 4e0ca36b56..fba340a5f7 100644 --- a/ui/src/lib/api/http-client.test.ts +++ b/ui/src/lib/api/http-client.test.ts @@ -326,6 +326,34 @@ function queuedAdhocRun(id: string) { }; } +describe('overview', () => { + function server(nodesStatus: number) { + vi.stubGlobal('document', { cookie: '' }); + vi.stubGlobal( + 'fetch', + vi.fn(async (request: Request) => { + const path = new URL(request.url).pathname; + if (path.endsWith('/nodes') && nodesStatus !== 200) return Response.json({ error: 'refused' }, { status: nodesStatus }); + if (path.endsWith('/platform/problems')) return Response.json({ scheduler_leader: 'server-1', scheduler_lease_expires_at: null, broken_locations: [], overdue_ticks: [] }); + return Response.json([]); + }) + ); + return createHttpClient({ baseUrl: 'http://ui.test', onUnauthorized: () => {} }); + } + + it('is null for a person who may view only part of the target, with no partial counts', async () => { + expect(await server(403).overview(ORDERS)).toBeNull(); + }); + + it('still fails on any other error', async () => { + await expect(server(500).overview(ORDERS)).rejects.toMatchObject({ status: 500 }); + }); + + it('summarises the target when every listing is allowed', async () => { + expect((await server(200).overview(ORDERS))?.metrics.runs24h.total).toBe(0); + }); +}); + describe('getCatalogNode', () => { /** Answers the reads a node page makes; anything else, such as node status, is refused. */ function serve(actions: string[], sqlStatus = 200) { diff --git a/ui/src/lib/api/http-client.ts b/ui/src/lib/api/http-client.ts index 2cf433e8c7..ae9c220376 100644 --- a/ui/src/lib/api/http-client.ts +++ b/ui/src/lib/api/http-client.ts @@ -449,15 +449,22 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt // Status and SQL have their own permissions; a refusal leaves that part out rather than failing the page. return nodeDetailOf(nodes, node, status.data ?? null, sql?.data ?? null, checks.map(toCheck), { runs: runs?.data ? toNodeRuns(runs.data) : null, jobs }); }, - async overview(scope: Scope): Promise { + async overview(scope: Scope): Promise { // A week of publications for the models metric; the server returns at most 100. - const [runs, checks, nodes, changes, events] = await Promise.all([ + const reads = Promise.all([ recentRuns(scope), implemented.listChecks(scope), implemented.listCatalogNodes(scope), implemented.listSnapshotChanges(scope, { limit: 100 }), implemented.listPlatformEvents() ]); + // Every listing needs view on all of the target, so a partial viewer gets no counts at all. + const read = await reads.catch((error: unknown) => { + if (error instanceof ApiError && error.status === 403) return null; + throw error; + }); + if (!read) return null; + const [runs, checks, nodes, changes, events] = read; return overviewOf({ runs, checks, nodes, changes, events, now: Date.now() }); }, listChecks: async (scope: Scope): Promise => (await checkResults(scope)).map(toCheck), diff --git a/ui/src/lib/api/mock/build.ts b/ui/src/lib/api/mock/build.ts index ba93c80a50..907039815c 100644 --- a/ui/src/lib/api/mock/build.ts +++ b/ui/src/lib/api/mock/build.ts @@ -218,7 +218,7 @@ export class MockBuild { // ---- overview ---- - async overview(scope: Scope): Promise { + async overview(scope: Scope): Promise { const states = this.#states(scope); const checks = this.#checks(scope, states); const nodes = SNAPSHOT_NODES.map((sn) => this.#catalogNode(states.get(sn.key)!, checks)); diff --git a/ui/src/lib/nav.test.ts b/ui/src/lib/nav.test.ts index f0eaa47e62..9f5478bfb3 100644 --- a/ui/src/lib/nav.test.ts +++ b/ui/src/lib/nav.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import type { ApiClient, Capability } from '$lib/api'; -import { pageAvailable, visibleNav } from './nav'; +import { pageAvailable, pagesBeyondTarget, visibleNav } from './nav'; const serving = (...methods: (keyof ApiClient)[]) => ({ supports: (method: keyof ApiClient) => methods.includes(method) }); @@ -33,6 +33,14 @@ describe('visibleNav', () => { }); }); +describe('pagesBeyondTarget', () => { + it('offers a partial viewer only the pages that do not list the whole target', () => { + const client = serving('overview', 'listCatalogNodes', 'listRunRows', 'listJobs', 'maintenanceHistory', 'listLocations', 'listAccounts', 'listAuditEntries'); + expect(pagesBeyondTarget(client, ['view', 'audit.view']).map((i) => i.label)).toEqual(['Locations', 'Audit log']); + expect(pagesBeyondTarget(client, ['view']).map((i) => i.label)).toEqual(['Locations']); + }); +}); + describe('pageAvailable', () => { const client = serving('listRunRows'); diff --git a/ui/src/lib/nav.ts b/ui/src/lib/nav.ts index 789191fae9..0cef4bc332 100644 --- a/ui/src/lib/nav.ts +++ b/ui/src/lib/nav.ts @@ -17,27 +17,30 @@ import type { Component } from 'svelte'; import { ADMIN_CAPABILITIES, hasAnyCapability, type ApiClient, type Capability, type NavCounts } from '$lib/api'; export type NavBadge = { value: (counts: NavCounts) => number; tone: 'muted' | 'warning' | 'error' | 'live' }; -/** `loads` is the client method the page cannot work without; the entry hides when the server lacks it. */ -export type NavItem = { label: string; href: string; icon: Component; loads: keyof ApiClient; badge?: NavBadge; requires?: readonly Capability[] }; +/** + * `loads` is the client method the page cannot work without; the entry hides when the server lacks it. + * `wholeTarget` pages list the selected target, which needs view on every node in it. + */ +export type NavItem = { label: string; href: string; icon: Component; loads: keyof ApiClient; badge?: NavBadge; requires?: readonly Capability[]; wholeTarget?: true }; export type NavGroup = { section: string | null; items: NavItem[]; requires?: readonly Capability[] }; export const NAV: readonly NavGroup[] = [ - { section: null, items: [{ label: 'Overview', href: '/', icon: LayoutDashboardIcon, loads: 'overview' }] }, + { section: null, items: [{ label: 'Overview', href: '/', icon: LayoutDashboardIcon, loads: 'overview', wholeTarget: true }] }, { section: 'Build', items: [ - { label: 'Lineage', href: '/lineage', icon: GitBranchIcon, loads: 'listCatalogNodes' }, - { label: 'Catalog', href: '/catalog', icon: DatabaseIcon, loads: 'listCatalogNodes', badge: { value: (c) => c.catalogNodes, tone: 'muted' } }, - { label: 'Checks', href: '/checks', icon: CheckCircleIcon, loads: 'listChecks', badge: { value: (c) => c.failingChecks, tone: 'warning' } }, - { label: 'Changes', href: '/changes', icon: GitCompareIcon, loads: 'listSnapshotChanges' } + { label: 'Lineage', href: '/lineage', icon: GitBranchIcon, loads: 'listCatalogNodes', wholeTarget: true }, + { label: 'Catalog', href: '/catalog', icon: DatabaseIcon, loads: 'listCatalogNodes', badge: { value: (c) => c.catalogNodes, tone: 'muted' }, wholeTarget: true }, + { label: 'Checks', href: '/checks', icon: CheckCircleIcon, loads: 'listChecks', badge: { value: (c) => c.failingChecks, tone: 'warning' }, wholeTarget: true }, + { label: 'Changes', href: '/changes', icon: GitCompareIcon, loads: 'listSnapshotChanges', wholeTarget: true } ] }, { section: 'Operate', items: [ - { label: 'Runs', href: '/runs', icon: ListIcon, loads: 'listRunRows', badge: { value: (c) => c.activeRuns, tone: 'live' } }, - { label: 'Jobs', href: '/jobs', icon: PlayIcon, loads: 'listJobs' }, - { label: 'Triggers', href: '/triggers', icon: ClockIcon, loads: 'listTriggers', badge: { value: (c) => c.pausedTriggers, tone: 'muted' } } + { label: 'Runs', href: '/runs', icon: ListIcon, loads: 'listRunRows', badge: { value: (c) => c.activeRuns, tone: 'live' }, wholeTarget: true }, + { label: 'Jobs', href: '/jobs', icon: PlayIcon, loads: 'listJobs', wholeTarget: true }, + { label: 'Triggers', href: '/triggers', icon: ClockIcon, loads: 'listTriggers', badge: { value: (c) => c.pausedTriggers, tone: 'muted' }, wholeTarget: true } ] }, { @@ -46,7 +49,7 @@ export const NAV: readonly NavGroup[] = [ { label: 'Locations', href: '/locations', icon: MapPinIcon, loads: 'listLocations', badge: { value: (c) => c.activePlatformEvents, tone: 'error' } }, { label: 'Connections', href: '/connections', icon: PlugIcon, loads: 'listWarehouseConnections' }, { label: 'Providers', href: '/providers', icon: PuzzleIcon, loads: 'listProviders' }, - { label: 'Maintenance', href: '/maintenance', icon: WrenchIcon, loads: 'maintenanceHistory' } + { label: 'Maintenance', href: '/maintenance', icon: WrenchIcon, loads: 'maintenanceHistory', wholeTarget: true } ] }, { @@ -70,6 +73,13 @@ export function visibleNav(client: Pick, capabilities: re .filter((g) => g.items.length > 0); } +/** Pages that don't list the selected target, so a person with view on only part of it can still use them. */ +export function pagesBeyondTarget(client: Pick, capabilities: readonly Capability[]): NavItem[] { + return visibleNav(client, capabilities) + .flatMap((g) => g.items) + .filter((i) => !i.wholeTarget); +} + /** Whether the server can load the page at `path`; pages outside the navigation are not gated here. */ export function pageAvailable(client: Pick, path: string): boolean { const item = NAV.flatMap((g) => g.items) diff --git a/ui/src/routes/+page.svelte b/ui/src/routes/+page.svelte index ae67ed9b0c..c85dbe3954 100644 --- a/ui/src/routes/+page.svelte +++ b/ui/src/routes/+page.svelte @@ -16,9 +16,13 @@ import { attentionRank, attentionView, type AttentionTone } from '$lib/build-area/attention'; import { changeOpLabel, formatCredits } from '$lib/build-area/labels'; import { summarizeChange } from '$lib/build-area/changes'; + import { pagesBeyondTarget } from '$lib/nav'; const overview = createResource((client, scope) => client.overview(scope), ['runs', 'checks', 'node_status', 'snapshot', 'platform_events', 'triggers']); const data = $derived(overview.data ?? null); + // Null data means the person may view only part of the target, so the page offers others instead. + const partial = $derived(overview.data === null); + const otherPages = $derived(pagesBeyondTarget(app.client, app.capabilities)); const attentionIcon: Record = { error: AlertCircleIcon, @@ -55,139 +59,153 @@
    -
    - {#if overview.error}
    {overview.error}
    {/if} -
    -
    -
    - {#if issues.length}{:else}{/if} -
    -
    - {issues.length ? `${app.location?.name} needs attention` : `${app.location?.name} is healthy`} - {issues.length ? `${issues.length} ${issues.length === 1 ? 'item' : 'items'} · ${latestText}` : latestText} -
    - {#if m} -
    - {#if m.stale?.changed}{m.stale.changed} changed{/if} - {#if m.stale?.behind}{m.stale.behind} behind{/if} - {#if m.checks.failing}{m.checks.failing} failing {m.checks.failing === 1 ? 'check' : 'checks'}{/if} + {#if partial} +
    +

    You can see only part of {app.location?.name} · {app.target?.name}

    +

    The Overview sums up the whole target, so it needs view on every node in it. An administrator can give you view on the rest.

    + {#if otherPages.length} +

    Pages you can use:

    + + {/if} + {#if (app.bootstrap?.locations.length ?? 0) > 1}

    Or choose another location from the menu at the top of the sidebar.

    {/if} +
    + {:else} +
    + {#if overview.error}
    {overview.error}
    {/if} +
    +
    +
    + {#if issues.length}{:else}{/if}
    - {/if} - {#if latest}View latest run →{/if} -
    - {#if issues.length} -
    - {#each issues as { item, view } (view.key)} - {@const Glyph = attentionIcon[view.tone]} -
    - - {view.title} - {view.detail} - {view.since} - - {#if item.kind === 'stale_models'} - (rebuildOpen = true)}>Rebuild {staleSelection.length} - {/if} - {view.action} → - +
    + {issues.length ? `${app.location?.name} needs attention` : `${app.location?.name} is healthy`} + {issues.length ? `${issues.length} ${issues.length === 1 ? 'item' : 'items'} · ${latestText}` : latestText} +
    + {#if m} +
    + {#if m.stale?.changed}{m.stale.changed} changed{/if} + {#if m.stale?.behind}{m.stale.behind} behind{/if} + {#if m.checks.failing}{m.checks.failing} failing {m.checks.failing === 1 ? 'check' : 'checks'}{/if}
    - {/each} + {/if} + {#if latest}View latest run →{/if}
    - {/if} -
    + {#if issues.length} +
    + {#each issues as { item, view } (view.key)} + {@const Glyph = attentionIcon[view.tone]} +
    + + {view.title} + {view.detail} + {view.since} + + {#if item.kind === 'stale_models'} + (rebuildOpen = true)}>Rebuild {staleSelection.length} + {/if} + {view.action} → + +
    + {/each} +
    + {/if} +
    -
    - {#if m} -
    - Models -
    {m.models.total}deployed
    - 0}>{m.models.addedThisWeek || m.models.removedThisWeek ? `+${m.models.addedThisWeek} / −${m.models.removedThisWeek} this week` : 'No changes this week'} -
    -
    - Runs · 24h -
    {m.runs24h.total}{successRate === null ? 'none finished' : `${successRate}% succeeded`}
    - 0}>{m.runs24h.partial} partial · {m.runs24h.failed} failed · {m.runs24h.active} active -
    -
    - Checks passing -
    {m.checks.passing}of {m.checks.total}
    - 0}>{m.checks.failing} failing · {m.checks.warning} warning -
    - {#if m.stale} +
    + {#if m}
    - Stale -
    {m.stale.nodes}nodes
    - {m.stale.changed} changed · {m.stale.behind} behind · {m.stale.sources} {m.stale.sources === 1 ? 'source' : 'sources'} late + Models +
    {m.models.total}deployed
    + 0}>{m.models.addedThisWeek || m.models.removedThisWeek ? `+${m.models.addedThisWeek} / −${m.models.removedThisWeek} this week` : 'No changes this week'} +
    +
    + Runs · 24h +
    {m.runs24h.total}{successRate === null ? 'none finished' : `${successRate}% succeeded`}
    + 0}>{m.runs24h.partial} partial · {m.runs24h.failed} failed · {m.runs24h.active} active
    - {/if} - {#if m.cost}
    - Cost · 24h -
    {formatCredits(m.cost.last24h)}
    - = 20}>{costChange === null ? '—' : `${costChange >= 0 ? '+' : ''}${costChange}%`} vs {m.cost.baselineDays}-day average + Checks passing +
    {m.checks.passing}of {m.checks.total}
    + 0}>{m.checks.failing} failing · {m.checks.warning} warning
    + {#if m.stale} +
    + Stale +
    {m.stale.nodes}nodes
    + {m.stale.changed} changed · {m.stale.behind} behind · {m.stale.sources} {m.stale.sources === 1 ? 'source' : 'sources'} late +
    + {/if} + {#if m.cost} +
    + Cost · 24h +
    {formatCredits(m.cost.last24h)}
    + = 20}>{costChange === null ? '—' : `${costChange >= 0 ? '+' : ''}${costChange}%`} vs {m.cost.baselineDays}-day average +
    + {/if} {/if} - {/if} -
    - - {#if showTimeline} -
    -
    -

    Activity

    Past runs, and when schedules will run next
    - Open full timeline → -
    -
    - {/if} -
    - {#if showGroups}
    -
    -

    Build status by group

    Current, stale and failed nodes · {app.location?.name}
    - Open catalog → -
    -
    -
    GroupCurrentChangedBehindFailed
    - {#each groups as g (g.group)} - - {g.group} - - - - - - {g.current}/{g.total} - {g.changed || '—'} - {g.behind || '—'} - {g.failed || '—'} - - {:else} -
    {overview.loading ? 'Loading…' : 'Nothing deployed yet. Deploy the project to see its models here.'}
    - {/each} -
    -
    {/if} -
    -
    -

    Recent changes

    Recent deploys
    - All changes → -
    - -
    + {#if showTimeline} +
    +
    +

    Activity

    Past runs, and when schedules will run next
    + Open full timeline → +
    + +
    + {/if} + +
    + {#if showGroups}
    +
    +

    Build status by group

    Current, stale and failed nodes · {app.location?.name}
    + Open catalog → +
    +
    +
    GroupCurrentChangedBehindFailed
    + {#each groups as g (g.group)} + + {g.group} + + + + + + {g.current}/{g.total} + {g.changed || '—'} + {g.behind || '—'} + {g.failed || '—'} + + {:else} +
    {overview.loading ? 'Loading…' : 'Nothing deployed yet. Deploy the project to see its models here.'}
    + {/each} +
    +
    {/if} +
    +
    +

    Recent changes

    Recent deploys
    + All changes → +
    + +
    +
    -
    + {/if}
    diff --git a/ui/src/routes/admin/people/[name]/+page.svelte b/ui/src/routes/admin/people/[name]/+page.svelte index 037735ebfa..329bc4a736 100644 --- a/ui/src/routes/admin/people/[name]/+page.svelte +++ b/ui/src/routes/admin/people/[name]/+page.svelte @@ -26,8 +26,11 @@ const account = $derived(d.accounts.data?.find((a) => a.name === name && a.kind === 'user') ?? null); const canSee = $derived(app.can('user.manage') || app.can('role.assign')); const canExplain = $derived(app.can('role.assign') || app.can('audit.view') || name === app.user?.name); + // Panels the server has no read for stay hidden. + const showsAccess = $derived(app.client.supports('effectiveAccess')); + const showsChecker = $derived(app.client.supports('explainAccess')); - const access = createResource((client) => (canExplain && name ? client.effectiveAccess(name) : Promise.resolve(null))); + const access = createResource((client) => (showsAccess && canExplain && name ? client.effectiveAccess(name) : Promise.resolve(null))); let why = $state(null); let checkerEl = $state(null); @@ -101,7 +104,7 @@ {/if} - + {#if showsAccess} {#if !canExplain}

    Requires “Give roles” (role.assign) or “See the audit log” (audit.view).

    {:else if access.data} @@ -111,9 +114,9 @@ {:else}

    Loading…

    {/if} -
    +
    {/if} - {#if canExplain} + {#if canExplain && showsChecker}
    From 0a69cf70c44839c47cccd834c443c4d946042d79 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Mon, 9 Nov 2026 01:20:31 +0000 Subject: [PATCH 093/106] feat(ui): test a location's warehouse connection from the Locations page Each location row now has Test connection for people who may test that location. The UI asks the location through the server's connection-test request, then reads the result until the location answers or the request expires, and shows whether it connected. The location reports no reason, so a failure points to sqb debug in the location. --- ui/src/lib/api/http-client.test.ts | 52 ++++++++++++++++++++++++++++ ui/src/lib/api/http-client.ts | 34 ++++++++++++++++-- ui/src/lib/api/http-locations.ts | 18 ++++++++++ ui/src/routes/locations/+page.svelte | 42 +++++++++++++++++++--- 4 files changed, 139 insertions(+), 7 deletions(-) diff --git a/ui/src/lib/api/http-client.test.ts b/ui/src/lib/api/http-client.test.ts index de27adb892..1145cff64a 100644 --- a/ui/src/lib/api/http-client.test.ts +++ b/ui/src/lib/api/http-client.test.ts @@ -413,6 +413,58 @@ describe('administration writes', () => { }); }); +describe('testConnection', () => { + const record = (completed: string | null, outcome: unknown = null) => ({ + generation: 1, + requested_at: '2026-11-09T10:00:00Z', + requested_by: 'ada', + expires_at: '2026-11-09T10:05:00Z', + completed_at: completed, + request_json: { location_id: 'loc', target: 'development', request_id: 'r', reason: 'x', work: { kind: 'check_connection' } }, + result_json: outcome ? { claim_id: 'c', session_id: 's', outcome } : null + }); + + function server(answers: unknown[]) { + const calls: string[] = []; + vi.stubGlobal('document', { cookie: '' }); + vi.stubGlobal( + 'fetch', + vi.fn(async (request: Request) => { + calls.push(request.method); + if (request.method === 'POST') return Response.json({ applied: true }); + return Response.json(answers.length > 1 ? answers.shift() : answers[0]); + }) + ); + return { calls, client: createHttpClient({ baseUrl: 'http://ui.test', onUnauthorized: () => {} }) }; + } + + it('asks the location once and waits until it answers', async () => { + vi.setSystemTime(Date.parse('2026-11-09T10:00:00Z')); + const { calls, client } = server([null, record(null), record('2026-11-09T10:00:03Z', { kind: 'connection', healthy: false })]); + const result = client.testConnection(ORDERS, 'warehouse'); + await vi.advanceTimersByTimeAsync(3000); + expect(await result).toMatchObject({ outcome: 'failed', durationMs: 3000, trigger: 'manual' }); + expect(calls).toEqual(['POST', 'GET', 'GET', 'GET']); + }); + + it('reports a test replaced by a newer request instead of a result', async () => { + const { client } = server([record('2026-11-09T10:00:01Z', { kind: 'superseded' })]); + const result = client.testConnection(ORDERS, 'warehouse'); + const settled = expect(result).rejects.toThrow('A newer request replaced this connection test'); + await vi.advanceTimersByTimeAsync(1000); + await settled; + }); + + it('gives up when the location never answers', async () => { + const { calls, client } = server([null]); + const result = client.testConnection(ORDERS, 'warehouse'); + const settled = expect(result).rejects.toMatchObject({ status: 504 }); + await vi.advanceTimersByTimeAsync(61_000); + await settled; + expect(calls.length).toBe(61); + }); +}); + describe('getCatalogNode', () => { /** Answers the reads a node page makes; anything else, such as node status, is refused. */ function serve(actions: string[], sqlStatus = 200) { diff --git a/ui/src/lib/api/http-client.ts b/ui/src/lib/api/http-client.ts index fb8fe1d656..4d6d125f21 100644 --- a/ui/src/lib/api/http-client.ts +++ b/ui/src/lib/api/http-client.ts @@ -5,7 +5,7 @@ import { checkDetailOf, toCheck, toCheckEvidence, toCheckSql } from './http-chec import { findNode, nodeDetailOf, toCatalogNodes, nodeJobsOf, toNodeRuns, toColumnLineage } from './http-catalog'; import { auditMatches, toAuditEntry } from './http-audit'; import { toSnapshotChanges } from './http-changes'; -import { toLocationSummary, warehouseSyncOf } from './http-locations'; +import { manualConnectionTest, toLocationSummary, warehouseSyncOf } from './http-locations'; import { toAccount, toRoleAssignments } from './http-people'; import { toMaintenanceHistory } from './http-maintenance'; import { overviewOf } from './http-overview'; @@ -15,7 +15,7 @@ import { jobDetailOf, toJob, toTick, toTrigger, triggerDetailOf } from './http-j import { RUN_WINDOW, toAdhocCheck, runDetailOf, runRowPage, runsMatching, runTabCountsOf, toRunNode, toRuns, toRunEvent, toRunOutput, toRunStatements } from './http-runs'; import type { ChangeSubscription, ChangedChannel, ChangedSignal, ConnectionState } from './signals'; import type { AuthMode, Bootstrap, Capability, Location, PlatformEvent, Scope, WarehouseSync } from './types'; -import type { Account, AuditEntry, AuditQuery, LocationSummary, MaintenanceHistory, RoleAssignment, RoleAssignmentRequest, RoleDefinition, RoleDetail } from './types/platform'; +import type { Account, AuditEntry, AuditQuery, ConnectionTest, LocationSummary, MaintenanceHistory, RoleAssignment, RoleAssignmentRequest, RoleDefinition, RoleDetail } from './types/platform'; import type { CatalogNode, CatalogNodeDetail, Check, CheckDetail, CheckEvidence, CheckSql, Overview, SnapshotChange, ColumnLineage } from './types/build'; import type { AdhocRunCheck, AdhocRunRequest, Job, JobDetail, RerunMode, SnapshotNode, Run, RunDetail, RunListQuery, RunRowPage, RunRowQuery, RunTabCounts, Trigger, TriggerDetail, TriggerState, RunEventPage, RunOutputLine, RunStatement, StatementSql } from './types/operate'; @@ -67,11 +67,16 @@ const SUPPORTED: ReadonlySet = new Set([ 'getRole', 'getWarehouseSync', 'maintenanceHistory', + 'testConnection', 'listSnapshotNodes', 'checkAdhocRun', 'launchAdhocRun' ]); +/** How often a connection test asks for the location's answer, and the longest it waits. */ +const CONNECTION_TEST_POLL_MS = 1000; +const CONNECTION_TEST_WAIT_MS = 60_000; + /** The server's largest audit page, and how many the audit log reads before it stops looking. */ const AUDIT_PAGE = 100; const AUDIT_PAGES = 10; @@ -383,6 +388,31 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt if (!data) throw new ApiError(errorText(error, response), response.status); return warehouseSyncOf(data); }, + /** Asks the location to connect, then waits for its answer until the request expires. */ + async testConnection(scope: Scope): Promise { + const { locationId, target } = parseLocationKey(scope.locationId); + const path = { location_id: locationId, target }; + const requestId = crypto.randomUUID(); + const asked = await http.POST('/api/control/locations/{location_id}/targets/{target}/connection-tests', { + body: { request_id: requestId, reason: 'Tested from the web UI' }, + params: { path, header: { 'x-sqb-csrf': csrfToken(document.cookie) } } + }); + if (asked.response.status === 401) onUnauthorized(); + if (!asked.response.ok) throw new ApiError(errorText(asked.error, asked.response), asked.response.status); + let expiresAt = Date.now() + CONNECTION_TEST_WAIT_MS; + while (Date.now() < expiresAt) { + await new Promise((resolve) => setTimeout(resolve, CONNECTION_TEST_POLL_MS)); + const { data, error, response } = await http.GET('/api/control/locations/{location_id}/targets/{target}/connection-tests/{request_id}', { + params: { path: { ...path, request_id: requestId } } + }); + if (response.status === 401) onUnauthorized(); + if (!response.ok) throw new ApiError(errorText(error, response), response.status); + const finished = manualConnectionTest(data ?? null); + if (finished) return finished; + if (data) expiresAt = Math.min(expiresAt, Date.parse(data.expires_at)); + } + throw new ApiError('The location did not answer the connection test in time; check that it is running', 504); + }, async maintenanceHistory(scope: Scope): Promise { const { locationId, target } = parseLocationKey(scope.locationId); const { data, error, response } = await http.GET('/api/control/locations/{location_id}/targets/{target}/maintenance', { params: { path: { location_id: locationId, target } } }); diff --git a/ui/src/lib/api/http-locations.ts b/ui/src/lib/api/http-locations.ts index 4136768b3e..cc674f3c05 100644 --- a/ui/src/lib/api/http-locations.ts +++ b/ui/src/lib/api/http-locations.ts @@ -35,6 +35,24 @@ export function connectionTestOf(observations: Wire['LocationHealthObservation'] return { outcome: ok ? 'ok' : 'failed', at: latest.received_at, durationMs: 0, trigger: 'publish', by: null, message: ok ? 'Connected after publishing' : 'Could not connect after publishing' }; } +/** + * A finished manual connection test, or null while the location hasn't answered. The location + * reports only whether it connected; `sqb debug` in the location shows why it couldn't. + */ +export function manualConnectionTest(record: Wire['LocationWorkRecord'] | null): ConnectionTest | null { + const outcome = record?.result_json?.outcome; + if (!record?.completed_at || !outcome) return null; + if (outcome.kind !== 'connection') throw new Error('A newer request replaced this connection test; test again'); + return { + outcome: outcome.healthy ? 'ok' : 'failed', + at: record.completed_at, + durationMs: Date.parse(record.completed_at) - Date.parse(record.requested_at), + trigger: 'manual', + by: null, + message: outcome.healthy ? 'Connected to the warehouse' : 'Could not connect to the warehouse; run sqb debug in the location to see why' + }; +} + /** `observations` is null when the person may not list the whole target. */ export function toLocationSummary(location: Location, observations: Wire['LocationHealthObservation'][] | null, published: Wire['PublishedSnapshot'] | null, now: number): LocationSummary { const known = observations ? healthOf(observations, now) : { health: null, liveInstances: 0, lastSeenAt: null }; diff --git a/ui/src/routes/locations/+page.svelte b/ui/src/routes/locations/+page.svelte index f7cc7c541e..369f177c7d 100644 --- a/ui/src/routes/locations/+page.svelte +++ b/ui/src/routes/locations/+page.svelte @@ -7,8 +7,9 @@ import LoadRow from '$lib/components/list/load-row.svelte'; import RelativeTime from '$lib/components/list/relative-time.svelte'; import StatusLabel from '$lib/components/list/status-label.svelte'; + import { hasCapability, type ConnectionTest } from '$lib/api'; import { formatAgo, formatStamp } from '$lib/format/time'; - import { healthTone, healthWords, targetWords, testTone } from '$lib/platform/labels'; + import { errorMessage, healthTone, healthWords, targetWords, testTone } from '$lib/platform/labels'; import { app } from '$lib/state/app.svelte'; import { createResource } from '$lib/state/resource.svelte'; import ServerStatus from './server-status.svelte'; @@ -23,6 +24,26 @@ const opensDetail = $derived(app.client.supports('getLocation')); const showsServer = $derived(app.client.supports('platformHealth')); const knowsPolicy = $derived(all.some((l) => l.brokenDeployPolicy)); + + // Connection tests run per location, so each row checks that location's own permission. + const mayTest = (id: string) => + app.client.supports('testConnection') && + (hasCapability(app.bootstrap?.capabilities, 'connection.test') || hasCapability(app.bootstrap?.locations.find((l) => l.id === id)?.capabilities, 'connection.test')); + // A fresh result can be newer than the page clock's last tick; it then reads "just now". + let testing = $state>({}); + let tested = $state>({}); + let testFailures = $state>({}); + async function testConnection(id: string) { + testing[id] = true; + delete testFailures[id]; + try { + tested[id] = await app.client.testConnection({ locationId: id }, 'warehouse'); + } catch (error) { + testFailures[id] = errorMessage(error); + } finally { + testing[id] = false; + } + } @@ -52,6 +73,7 @@ {#each filtered as l (l.id)} + {@const test = tested[l.id] ?? l.connectionTest} opensDetail && goto(`/locations/${l.id}`)}>
    @@ -81,14 +103,20 @@ {/if} - {#if l.connectionTest} - - + {#if testing[l.id]} + + {:else if test} + + -
    tested {formatAgo(l.connectionTest.at, app.now)}
    +
    {test.trigger === 'manual' ? `tested ${formatAgo(test.at, Math.max(app.now, Date.parse(test.at)))} by you` : `tested ${formatAgo(test.at, app.now)} after deploying`}
    {:else} {/if} + {#if testFailures[l.id]}
    {testFailures[l.id]}
    {/if} + {#if mayTest(l.id)} + + {/if}
    {l.liveInstances === 1 ? 'Running' : l.liveInstances === 0 ? 'Not running' : `${l.liveInstances} running`}
    @@ -107,4 +135,8 @@ tr.static { cursor: default; } + .test { display: block; margin-top: 4px; color: var(--primary); font-size: 12px; } + .test:hover:not(:disabled) { text-decoration: underline; } + .test:disabled { color: var(--sb-text-faint); } + .err { color: var(--sb-error) !important; } From 1d34a8a95699290cd173b568ff066f32715f5320 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Mon, 9 Nov 2026 01:59:36 +0000 Subject: [PATCH 094/106] feat(ui): location detail and scheduler status from the server's health reads The location page now opens from the server's target health, newest snapshot and live problems: one process per reporting session, the publish-time connection check, a failed reload shown as its health, and open problems. The scheduler strip reports whether a server holds the lease. Facts the server doesn't report (images, commits, deploy policy, heartbeat, forwarding, problem history, tokens) stay hidden, and the list counts open problems per location and no longer offers the page to people who can't read the target's health. --- ui/src/lib/api/http-client.test.ts | 59 ++++++++++ ui/src/lib/api/http-client.ts | 67 +++++++---- ui/src/lib/api/http-locations.test.ts | 26 ++++- ui/src/lib/api/http-locations.ts | 73 +++++++++--- ui/src/lib/api/http-problems.test.ts | 14 ++- ui/src/lib/api/http-problems.ts | 10 ++ ui/src/lib/api/types/platform.ts | 37 +++--- ui/src/routes/locations/+page.svelte | 18 ++- ui/src/routes/locations/[id]/+page.svelte | 67 ++++++++--- ui/src/routes/locations/[id]/utils.ts | 2 +- ui/src/routes/locations/server-status.svelte | 114 +++++++++++-------- ui/src/routes/locations/utils.ts | 10 +- 12 files changed, 373 insertions(+), 124 deletions(-) diff --git a/ui/src/lib/api/http-client.test.ts b/ui/src/lib/api/http-client.test.ts index 1145cff64a..b26c692767 100644 --- a/ui/src/lib/api/http-client.test.ts +++ b/ui/src/lib/api/http-client.test.ts @@ -354,6 +354,65 @@ describe('overview', () => { }); }); +describe('locations', () => { + const access = (target: string) => ({ location_id: '0191f0aa-orders', name: 'Orders', target, actions: [] }); + const report = (session: string, secondsAgo: number) => ({ + received_at: new Date(Date.now() - secondsAgo * 1000).toISOString(), + report: { event_id: `e-${session}-${secondsAgo}`, session_id: session, target: 'development', health: { kind: 'heartbeat' } } + }); + + /** Orders development is readable and orders preview refuses its health read, as for a partial viewer. */ + function server() { + const paths: string[] = []; + vi.stubGlobal('document', { cookie: '' }); + vi.stubGlobal( + 'fetch', + vi.fn(async (request: Request) => { + const path = new URL(request.url).pathname; + paths.push(path); + if (path.endsWith('/bootstrap')) return Response.json({ login_mode: 'password', access: { name: 'ada', principal_id: 'p', actions: [], locations: [access('development'), access('preview')] } }); + if (path.endsWith('/platform/problems')) + return Response.json({ + scheduler_leader: true, + scheduler_lease_expires_at: '2026-11-09T10:00:30Z', + broken_locations: [{ location_id: '0191f0aa-orders', location_name: 'Orders', target: 'development', last_seen_at: '2026-11-09T09:00:00Z' }], + overdue_ticks: [] + }); + if (path.includes('/targets/preview/')) return Response.json({ error: 'refused' }, { status: 403 }); + if (path.endsWith('/health')) return Response.json([report('s-2', 10), report('s-1', 400), report('s-2', 20)]); + return Response.json([]); + }) + ); + return { paths, client: createHttpClient({ baseUrl: 'http://ui.test', onUnauthorized: () => {} }) }; + } + + it('opens a readable location with one process per session', async () => { + const detail = await server().client.getLocation('0191f0aa-orders:development'); + expect(detail?.instances.map((i) => [i.id, i.state])).toEqual([ + ['s-2', 'live'], + ['s-1', 'stale'] + ]); + }); + + it('fails the page with the refusal when the person may not read the target health', async () => { + await expect(server().client.getLocation('0191f0aa-orders:preview')).rejects.toMatchObject({ status: 403 }); + }); + + it('is null for a location the person is not given, without reading its health', async () => { + const { paths, client } = server(); + expect(await client.getLocation('0191f0aa-orders:production')).toBeNull(); + expect(paths.some((p) => p.endsWith('/health'))).toBe(false); + }); + + it('counts open problems per location target', async () => { + const listed = await server().client.listLocations(); + expect(listed.map((l) => [l.id, l.activeProblems, l.health])).toEqual([ + ['0191f0aa-orders:development', 1, 'healthy'], + ['0191f0aa-orders:preview', 0, null] + ]); + }); +}); + describe('administration writes', () => { const ada = { principal_id: 'p-ada', name: 'ada', created_at: '2026-11-01T00:00:00Z', disabled_at: null, has_password: true, groups: [], active_sessions: 2, last_signed_in_at: null }; diff --git a/ui/src/lib/api/http-client.ts b/ui/src/lib/api/http-client.ts index 4d6d125f21..606cd2c49d 100644 --- a/ui/src/lib/api/http-client.ts +++ b/ui/src/lib/api/http-client.ts @@ -5,17 +5,17 @@ import { checkDetailOf, toCheck, toCheckEvidence, toCheckSql } from './http-chec import { findNode, nodeDetailOf, toCatalogNodes, nodeJobsOf, toNodeRuns, toColumnLineage } from './http-catalog'; import { auditMatches, toAuditEntry } from './http-audit'; import { toSnapshotChanges } from './http-changes'; -import { manualConnectionTest, toLocationSummary, warehouseSyncOf } from './http-locations'; +import { manualConnectionTest, toLocationDetail, toLocationSummary, warehouseSyncOf } from './http-locations'; import { toAccount, toRoleAssignments } from './http-people'; import { toMaintenanceHistory } from './http-maintenance'; import { overviewOf } from './http-overview'; -import { toPlatformEvents } from './http-problems'; +import { toPlatformEvents, toPlatformHealth } from './http-problems'; import { roleDetailOf, toRoleDefinition } from './http-roles'; import { jobDetailOf, toJob, toTick, toTrigger, triggerDetailOf } from './http-jobs'; import { RUN_WINDOW, toAdhocCheck, runDetailOf, runRowPage, runsMatching, runTabCountsOf, toRunNode, toRuns, toRunEvent, toRunOutput, toRunStatements } from './http-runs'; import type { ChangeSubscription, ChangedChannel, ChangedSignal, ConnectionState } from './signals'; import type { AuthMode, Bootstrap, Capability, Location, PlatformEvent, Scope, WarehouseSync } from './types'; -import type { Account, AuditEntry, AuditQuery, ConnectionTest, LocationSummary, MaintenanceHistory, RoleAssignment, RoleAssignmentRequest, RoleDefinition, RoleDetail } from './types/platform'; +import type { Account, AuditEntry, AuditQuery, ConnectionTest, LocationDetail, LocationSummary, PlatformHealth, MaintenanceHistory, RoleAssignment, RoleAssignmentRequest, RoleDefinition, RoleDetail } from './types/platform'; import type { CatalogNode, CatalogNodeDetail, Check, CheckDetail, CheckEvidence, CheckSql, Overview, SnapshotChange, ColumnLineage } from './types/build'; import type { AdhocRunCheck, AdhocRunRequest, Job, JobDetail, RerunMode, SnapshotNode, Run, RunDetail, RunListQuery, RunRowPage, RunRowQuery, RunTabCounts, Trigger, TriggerDetail, TriggerState, RunEventPage, RunOutputLine, RunStatement, StatementSql } from './types/operate'; @@ -62,7 +62,9 @@ const SUPPORTED: ReadonlySet = new Set([ 'disableAccount', 'enableAccount', 'listLocations', + 'getLocation', 'listPlatformEvents', + 'platformHealth', 'listRoles', 'getRole', 'getWarehouseSync', @@ -420,30 +422,27 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt if (!data) throw new ApiError(errorText(error, response), response.status); return toMaintenanceHistory(data); }, - async listPlatformEvents(): Promise { - const { data, error, response } = await http.GET('/api/control/platform/problems'); - if (response.status === 401) onUnauthorized(); - if (!data) throw new ApiError(errorText(error, response), response.status); - return toPlatformEvents(data, Date.now()); - }, + listPlatformEvents: async (): Promise => toPlatformEvents(await problems(), Date.now()), + platformHealth: async (): Promise => toPlatformHealth(await problems()), /** Each location the person sees, with health only where they may list the whole target. */ async listLocations(): Promise { - const { data, error, response } = await http.GET('/api/control/bootstrap'); - if (response.status === 401) onUnauthorized(); - if (!data) throw new ApiError(errorText(error, response), response.status); + const [locations, events] = await Promise.all([bootstrapLocations(), problems().then((p) => toPlatformEvents(p, Date.now()))]); return Promise.all( - toBootstrap(data).locations.map(async (location) => { - const { locationId, target } = parseLocationKey(location.id); - const path = { location_id: locationId, target }; - const [health, snapshots] = await Promise.all([ - http.GET('/api/control/locations/{location_id}/targets/{target}/health', { params: { path } }), - http.GET('/api/control/locations/{location_id}/targets/{target}/snapshots', { params: { path, query: { limit: 1 } } }) - ]); - if (health.response.status === 401 || snapshots.response.status === 401) onUnauthorized(); - return toLocationSummary(location, health.data ?? null, snapshots.data?.[0] ?? null, Date.now()); + locations.map(async (location) => { + const [health, snapshot] = await locationHealth(location.id); + const open = events.filter((e) => e.locationId === location.id).length; + return toLocationSummary(location, health.data ?? null, snapshot, open, Date.now()); }) ); }, + /** Null for a location the person can't see; a refused health read fails the page, as the target's listings do. */ + async getLocation(locationId: string): Promise { + const location = (await bootstrapLocations()).find((l) => l.id === locationId); + if (!location) return null; + const [health, snapshot] = await locationHealth(location.id); + if (!health.data) throw new ApiError(errorText(health.error, health.response), health.response.status); + return toLocationDetail(location, health.data, snapshot, Date.now()); + }, listAccounts: async (): Promise => (await people()).map(toAccount), listRoles: async (locationId: string): Promise => (await roleOverviews(locationId)).map(toRoleDefinition), async getRole(locationId: string, name: string): Promise { @@ -673,6 +672,32 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt return data; } + async function problems() { + const { data, error, response } = await http.GET('/api/control/platform/problems'); + if (response.status === 401) onUnauthorized(); + if (!data) throw new ApiError(errorText(error, response), response.status); + return data; + } + + async function bootstrapLocations() { + const { data, error, response } = await http.GET('/api/control/bootstrap'); + if (response.status === 401) onUnauthorized(); + if (!data) throw new ApiError(errorText(error, response), response.status); + return toBootstrap(data).locations; + } + + /** The target's health read, kept whole so a refusal can be shown or left out, and its newest snapshot. */ + async function locationHealth(locationKey: string) { + const { locationId, target } = parseLocationKey(locationKey); + const path = { location_id: locationId, target }; + const [health, snapshots] = await Promise.all([ + http.GET('/api/control/locations/{location_id}/targets/{target}/health', { params: { path } }), + http.GET('/api/control/locations/{location_id}/targets/{target}/snapshots', { params: { path, query: { limit: 1 } } }) + ]); + if (health.response.status === 401 || snapshots.response.status === 401) onUnauthorized(); + return [health, snapshots.data?.[0] ?? null] as const; + } + /** The newest result of every check, or one check's results newest first. */ async function checkResults(scope: Scope, bindingKey?: string) { const { locationId, target } = parseLocationKey(scope.locationId); diff --git a/ui/src/lib/api/http-locations.test.ts b/ui/src/lib/api/http-locations.test.ts index 6d07abda32..5547d25018 100644 --- a/ui/src/lib/api/http-locations.test.ts +++ b/ui/src/lib/api/http-locations.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { connectionTestOf, healthOf, warehouseSyncOf } from './http-locations'; +import { connectionTestOf, healthOf, instancesOf, toLocationDetail, warehouseSyncOf } from './http-locations'; const now = Date.parse('2026-11-08T12:00:00Z'); const at = (secondsAgo: number) => new Date(now - secondsAgo * 1000).toISOString(); @@ -32,6 +32,30 @@ describe('location health', () => { }); }); +describe('location processes', () => { + it('lists each session once by its newest check-in, newest first, and stale after 90 seconds', () => { + const observations = [seen(200, { kind: 'heartbeat' }, 'old'), seen(30, { kind: 'heartbeat' }), seen(95, { kind: 'heartbeat' }, 'old'), seen(5, { kind: 'connection', generation: 2, healthy: true })]; + expect(instancesOf(observations, now).map((i) => [i.id, i.lastSeenAt, i.state])).toEqual([ + ['s-1', at(5), 'live'], + ['old', at(95), 'stale'] + ]); + }); +}); + +describe('toLocationDetail', () => { + const location = { id: 'loc:prod', name: 'orders', health: null, lastSeenAt: null, target: { name: 'prod', adapter: null, connectionSummary: null }, warehouseSync: { lastSyncedAt: null, lastError: null } }; + + it('shows a failed reload as its health, without inventing a failed deploy or error text', () => { + const detail = toLocationDetail(location, [seen(5, { kind: 'heartbeat' }), seen(20, { kind: 'compile_failed', generation: 3 }), seen(40, { kind: 'connection', generation: 2, healthy: true })], null, now); + expect([detail.health, detail.failedDeploy, detail.lastLoadError, detail.snapshot]).toEqual(['falling_back', null, null, null]); + }); + + it('is healthy again once a newer generation connects', () => { + const detail = toLocationDetail(location, [seen(5, { kind: 'connection', generation: 4, healthy: true }), seen(20, { kind: 'compile_failed', generation: 3 })], null, now); + expect([detail.health, detail.connectionTest?.outcome]).toEqual(['healthy', 'ok']); + }); +}); + describe('warehouseSyncOf', () => { const sync = (at: string, table: 'catalog' | 'statistics', failure: 'read' | 'delivery' | null) => ({ received_at: at, diff --git a/ui/src/lib/api/http-locations.ts b/ui/src/lib/api/http-locations.ts index cc674f3c05..b10cc818d8 100644 --- a/ui/src/lib/api/http-locations.ts +++ b/ui/src/lib/api/http-locations.ts @@ -1,6 +1,6 @@ import type { components } from '$control/schema.d.ts'; -import type { Location, LocationHealth, WarehouseSync } from './types'; -import type { ConnectionTest, LocationSummary } from './types/platform'; +import type { Location, LocationHealth, Snapshot, WarehouseSync } from './types'; +import type { ConnectionTest, LocationDetail, LocationInstance, LocationSummary } from './types/platform'; type Wire = components['schemas']; @@ -53,8 +53,28 @@ export function manualConnectionTest(record: Wire['LocationWorkRecord'] | null): }; } -/** `observations` is null when the person may not list the whole target. */ -export function toLocationSummary(location: Location, observations: Wire['LocationHealthObservation'][] | null, published: Wire['PublishedSnapshot'] | null, now: number): LocationSummary { +function snapshotOf(published: Wire['PublishedSnapshot'] | null): Snapshot | null { + return published + ? { + id: published.snapshot_id, + generation: published.generation, + gitRevision: published.git_revision ?? '', + gitDirty: published.git_dirty, + gitAuthor: published.git_author ?? '', + publishedAt: published.published_at, + sqbVersion: published.sqb_version + } + : null; +} + +/** `observations` is null when the person may not list the whole target; `activeProblems` counts the problems read's events. */ +export function toLocationSummary( + location: Location, + observations: Wire['LocationHealthObservation'][] | null, + published: Wire['PublishedSnapshot'] | null, + activeProblems: number, + now: number +): LocationSummary { const known = observations ? healthOf(observations, now) : { health: null, liveInstances: 0, lastSeenAt: null }; return { id: location.id, @@ -64,18 +84,39 @@ export function toLocationSummary(location: Location, observations: Wire['Locati brokenDeployPolicy: null, failedDeploy: null, connectionTest: observations ? connectionTestOf(observations) : null, - activeProblems: 0, - snapshot: published - ? { - id: published.snapshot_id, - generation: published.generation, - gitRevision: published.git_revision ?? '', - gitDirty: published.git_dirty, - gitAuthor: published.git_author ?? '', - publishedAt: published.published_at, - sqbVersion: published.sqb_version - } - : null + activeProblems, + snapshot: snapshotOf(published) + }; +} + +/** One process per reporting session, newest check-in first; the server keeps no image, commit or start time for them. */ +export function instancesOf(observations: Wire['LocationHealthObservation'][], now: number): LocationInstance[] { + const lastSeen = new Map(); + for (const o of observations) { + const seen = lastSeen.get(o.report.session_id); + if (!seen || o.received_at > seen) lastSeen.set(o.report.session_id, o.received_at); + } + return [...lastSeen] + .sort(([, a], [, b]) => b.localeCompare(a)) + .map(([id, lastSeenAt]) => ({ id, imageId: null, gitRevision: null, startedAt: null, lastSeenAt, state: now - Date.parse(lastSeenAt) <= LIVE_WINDOW_MS ? 'live' : 'stale' })); +} + +/** A location's page from its health observations and newest snapshot; locations never send compile error text, so a failed reload shows only as its health. */ +export function toLocationDetail(location: Location, observations: Wire['LocationHealthObservation'][], published: Wire['PublishedSnapshot'] | null, now: number): LocationDetail { + const { health } = healthOf(observations, now); + return { + id: location.id, + name: location.name, + health, + createdAt: null, + brokenDeployPolicy: null, + failedDeploy: null, + lastLoadError: null, + instances: instancesOf(observations, now), + target: location.target, + snapshot: snapshotOf(published), + warehouseConnection: null, + connectionTest: connectionTestOf(observations) }; } diff --git a/ui/src/lib/api/http-problems.test.ts b/ui/src/lib/api/http-problems.test.ts index 9074446a06..553fa62ed0 100644 --- a/ui/src/lib/api/http-problems.test.ts +++ b/ui/src/lib/api/http-problems.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { toPlatformEvents } from './http-problems'; +import { toPlatformEvents, toPlatformHealth } from './http-problems'; const NOW = Date.parse('2026-11-08T12:00:00Z'); const tick = (name: string, location: string, scheduledFor: string) => ({ @@ -46,3 +46,15 @@ describe('toPlatformEvents', () => { ]); }); }); + +describe('toPlatformHealth', () => { + it('reports a lease only while a server leads, with the replica and heartbeat left unknown', () => { + const leading = toPlatformHealth({ scheduler_leader: true, scheduler_lease_expires_at: '2026-11-08T12:00:10Z', broken_locations: [], overdue_ticks: [] }); + expect(leading).toEqual({ scheduler: { lease: { holder: null, acquiredAt: null, renewedAt: null, expiresAt: '2026-11-08T12:00:10Z' }, instances: null }, heartbeat: null }); + expect('sink' in leading).toBe(false); + }); + + it('has no lease once the last one ran out, even though its expiry is still reported', () => { + expect(toPlatformHealth({ scheduler_leader: false, scheduler_lease_expires_at: '2026-11-08T11:50:00Z', broken_locations: [], overdue_ticks: [] }).scheduler.lease).toBeNull(); + }); +}); diff --git a/ui/src/lib/api/http-problems.ts b/ui/src/lib/api/http-problems.ts index 9b1e44dfb2..53c466a61d 100644 --- a/ui/src/lib/api/http-problems.ts +++ b/ui/src/lib/api/http-problems.ts @@ -1,6 +1,7 @@ import type { components } from '$control/schema.d.ts'; import { locationKey as keyOf } from './http-client'; import type { PlatformEvent } from './types'; +import type { PlatformHealth } from './types/platform'; type Wire = components['schemas']; @@ -54,3 +55,12 @@ export function toPlatformEvents(problems: Wire['PlatformProblems'], now: number } return events; } + +/** The scheduler from the problems read: whether a lease is live and when it runs out. Replicas, heartbeat and forwarding aren't reported. */ +export function toPlatformHealth(problems: Wire['PlatformProblems']): PlatformHealth { + const expiresAt = problems.scheduler_lease_expires_at; + return { + scheduler: { lease: problems.scheduler_leader && expiresAt ? { holder: null, acquiredAt: null, renewedAt: null, expiresAt } : null, instances: null }, + heartbeat: null + }; +} diff --git a/ui/src/lib/api/types/platform.ts b/ui/src/lib/api/types/platform.ts index caf3aad99c..823fbe6630 100644 --- a/ui/src/lib/api/types/platform.ts +++ b/ui/src/lib/api/types/platform.ts @@ -27,16 +27,18 @@ export type SchedulerInstance = { }; export type SchedulerLease = { - holder: string; - acquiredAt: Iso; - renewedAt: Iso; + /** Null when the server doesn't say which replica holds it, nor since when. */ + holder: string | null; + acquiredAt: Iso | null; + renewedAt: Iso | null; expiresAt: Iso; }; export type SchedulerStatus = { /** Null when no replica holds the leader lease. */ lease: SchedulerLease | null; - instances: SchedulerInstance[]; + /** Null when the server doesn't list its replicas. */ + instances: SchedulerInstance[] | null; }; export type HeartbeatStatus = { @@ -62,9 +64,10 @@ export type SinkStatus = { export type PlatformHealth = { scheduler: SchedulerStatus; - heartbeat: HeartbeatStatus; - /** Null when no server-side sink is configured. */ - sink: SinkStatus | null; + /** Null when the server doesn't report its heartbeat. */ + heartbeat: HeartbeatStatus | null; + /** Null when no server-side sink is configured; absent when the server doesn't report forwarding. */ + sink?: SinkStatus | null; }; export type PlatformEventLink = @@ -137,9 +140,10 @@ export type ConnectionTest = { export type LocationInstance = { id: string; - imageId: string; - gitRevision: string; - startedAt: Iso; + /** Null when the server doesn't report what the process runs or when it started. */ + imageId: string | null; + gitRevision: string | null; + startedAt: Iso | null; lastSeenAt: Iso; /** `stale` once heartbeats stop; `gone` after deregistering. */ state: 'live' | 'stale' | 'gone'; @@ -149,17 +153,20 @@ export type LocationDetail = { id: string; name: string; health: LocationHealth; - createdAt: Iso; - brokenDeployPolicy: BrokenDeployPolicy; + /** Null when the server doesn't report it. */ + createdAt: Iso | null; + /** Null when the server doesn't report it. */ + brokenDeployPolicy: BrokenDeployPolicy | null; /** The latest deploy that never published; null when the latest deploy is current. */ failedDeploy: FailedDeploy | null; lastLoadError: string | null; instances: LocationInstance[]; target: LocationTarget; snapshot: Snapshot | null; - /** Image the current snapshot was published from; runs start from it. Null for local processes. */ - imageId: string | null; - warehouseConnection: string; + /** Image the current snapshot was published from; runs start from it. Null for local processes; absent when the server doesn't report it. */ + imageId?: string | null; + /** Null when the server doesn't name it. */ + warehouseConnection: string | null; connectionTest: ConnectionTest | null; }; diff --git a/ui/src/routes/locations/+page.svelte b/ui/src/routes/locations/+page.svelte index 369f177c7d..c49798c4c9 100644 --- a/ui/src/routes/locations/+page.svelte +++ b/ui/src/routes/locations/+page.svelte @@ -24,6 +24,7 @@ const opensDetail = $derived(app.client.supports('getLocation')); const showsServer = $derived(app.client.supports('platformHealth')); const knowsPolicy = $derived(all.some((l) => l.brokenDeployPolicy)); + const partialTitle = 'You need view on every node of this location to see its health, deploys and processes'; // Connection tests run per location, so each row checks that location's own permission. const mayTest = (id: string) => @@ -74,12 +75,13 @@ {#each filtered as l (l.id)} {@const test = tested[l.id] ?? l.connectionTest} - opensDetail && goto(`/locations/${l.id}`)}> + {@const opens = opensDetail && l.health !== null} + opens && goto(`/locations/${l.id}`)}>
    - {#if opensDetail} e.stopPropagation()}>{l.name}{:else}{l.name}{/if} + {#if opens} e.stopPropagation()}>{l.name}{:else}{l.name}{/if}
    {targetWords(l.target)}
    @@ -88,14 +90,16 @@ {#if l.health} {:else} - + {/if} {#if l.activeProblems} {/if} - {#if l.snapshot} + {#if l.health === null} + — + {:else if l.snapshot}
    Version {l.snapshot.generation}
    deployed {formatAgo(l.snapshot.publishedAt, app.now)}
    {:else} @@ -110,6 +114,8 @@
    {test.trigger === 'manual' ? `tested ${formatAgo(test.at, Math.max(app.now, Date.parse(test.at)))} by you` : `tested ${formatAgo(test.at, app.now)} after deploying`}
    + {:else if l.health === null} + — {:else} {/if} @@ -119,8 +125,8 @@ {/if} -
    {l.liveInstances === 1 ? 'Running' : l.liveInstances === 0 ? 'Not running' : `${l.liveInstances} running`}
    - {#if l.lastSeenAt}{/if} + {#if l.health === null}—{:else}
    {l.liveInstances === 1 ? 'Running' : l.liveInstances === 0 ? 'Not running' : `${l.liveInstances} running`}
    + {#if l.lastSeenAt}{/if}{/if} {#if knowsPolicy}{l.brokenDeployPolicy === 'fallback' ? 'Keep running the previous version' : l.brokenDeployPolicy ? 'Hold scheduled runs until a deploy works' : '—'}{/if} diff --git a/ui/src/routes/locations/[id]/+page.svelte b/ui/src/routes/locations/[id]/+page.svelte index 30e4004923..31c8bf7fa2 100644 --- a/ui/src/routes/locations/[id]/+page.svelte +++ b/ui/src/routes/locations/[id]/+page.svelte @@ -16,11 +16,20 @@ import { healthLabel, healthTone, shortImage, targetWords } from '$lib/platform/labels'; import { app } from '$lib/state/app.svelte'; import { createResource } from '$lib/state/resource.svelte'; + import { openRecord } from '../utils'; import { firstLine, summariseProcesses } from './utils'; const id = $derived(page.params.id ?? ''); const location = createResource((client) => client.getLocation(id), ['snapshot', 'warehouse_sync']); - const problems = createResource((client) => client.listPlatformEventHistory({ locationId: id }), ['platform_events']); + // Without recorded history, the page lists the open problems the server derives now. + const history = $derived(app.client.supports('listPlatformEventHistory')); + const problems = createResource( + (client) => + client.supports('listPlatformEventHistory') + ? client.listPlatformEventHistory({ locationId: id }) + : client.listPlatformEvents({ activeOnly: true }).then((events) => events.filter((e) => e.locationId === id).map(openRecord)), + ['platform_events'] + ); const detail = $derived(location.data ?? null); let problemView = $state<'open' | 'all'>('open'); const problemRows = $derived((problems.data ?? []).filter((e) => problemView === 'all' || e.active)); @@ -74,8 +83,8 @@ {#if processes.live.length === 1 && running} Running · - {#if detail.imageId}image {shortImage(running.imageId)}{:else}local process{/if} - · last check-in {formatAgo(running.lastSeenAt, app.now)} + {#if running.imageId}image {shortImage(running.imageId)} ·{:else if detail.imageId === null}local process ·{/if} + last check-in {formatAgo(running.lastSeenAt, app.now)} {:else if processes.live.length > 1} {processes.live.length} processes running · usually a deploy in progress: the new one takes over once it loads {:else} @@ -83,7 +92,7 @@ {/if} - +
    @@ -93,8 +102,8 @@
  • {i.id} - image {shortImage(i.imageId)} · commit {i.gitRevision} - started · last check-in + {#if i.imageId && i.gitRevision}image {shortImage(i.imageId)} · commit {i.gitRevision}{/if} + {#if i.startedAt}started ·{/if} last check-in
  • {/each} @@ -125,6 +134,14 @@
    {detail.lastLoadError}
    + {:else if detail.health === 'falling_back'} +
    + + + The last reload failed — still running {detail.snapshot ? `version ${detail.snapshot.generation}` : 'the last version that loaded'}. The location keeps the error: see its output, or run + sqb compile in its project. + +
    {/if}
    @@ -133,11 +150,14 @@ !!row)} />
    @@ -145,21 +165,24 @@
    - + {#snippet actions()} - + />{/if} {/snippet}
    - {#if app.can('location.manage')} + {#if app.can('location.manage') && app.client.supports('listLocationTokens')} @@ -257,6 +280,20 @@ .alert[open] .more { visibility: hidden; } + .note { + display: flex; + align-items: baseline; + gap: 8px; + padding: 7px 10px; + } + .note > :global(svg) { + flex: 0 0 auto; + align-self: center; + color: var(--sb-warning); + } + .note .summary-text { + white-space: normal; + } .alert-body { padding: 0 10px 10px 32px; color: var(--muted-foreground); diff --git a/ui/src/routes/locations/[id]/utils.ts b/ui/src/routes/locations/[id]/utils.ts index 22a297b028..76baab17d8 100644 --- a/ui/src/routes/locations/[id]/utils.ts +++ b/ui/src/routes/locations/[id]/utils.ts @@ -15,7 +15,7 @@ export type ProcessSummary = { * than one is (usually a deploy overlap while a new process loads). */ export function summariseProcesses(instances: readonly LocationInstance[]): ProcessSummary { - const byStart = [...instances].sort((a, b) => b.startedAt.localeCompare(a.startedAt)); + const byStart = [...instances].sort((a, b) => (b.startedAt ?? b.lastSeenAt).localeCompare(a.startedAt ?? a.lastSeenAt)); const live = byStart.filter((i) => i.state === 'live'); if (live.length === 1) return { live, listed: [] }; return { live, listed: live.length > 1 ? live : byStart }; diff --git a/ui/src/routes/locations/server-status.svelte b/ui/src/routes/locations/server-status.svelte index a0857eaf34..8950388bc2 100644 --- a/ui/src/routes/locations/server-status.svelte +++ b/ui/src/routes/locations/server-status.svelte @@ -3,10 +3,17 @@ import { formatAgo, formatDuration, formatIn, formatStamp } from '$lib/format/time'; import { app } from '$lib/state/app.svelte'; import { createResource } from '$lib/state/resource.svelte'; - import { heartbeatLate, replicaLive, schedulerWords, sinkWords } from './utils'; + import { heartbeatLate, openRecord, replicaLive, schedulerWords, sinkWords } from './utils'; const health = createResource((client) => client.platformHealth(), ['platform_events']); - const serverEvents = createResource((client) => client.listPlatformEventHistory({ locationId: null }), ['platform_events']); + // Without recorded history, the open server problems are the ones the problems read derives now. + const serverEvents = createResource( + (client) => + client.supports('listPlatformEventHistory') + ? client.listPlatformEventHistory({ locationId: null }) + : client.listPlatformEvents({ activeOnly: true }).then((events) => events.filter((e) => e.locationId === null).map(openRecord)), + ['platform_events'] + ); // Scheduler, replicas and forwarding are liveness signals; poll them while the page is open. $effect(() => { @@ -16,8 +23,8 @@ const h = $derived(health.data ?? null); const scheduler = $derived(h ? schedulerWords(h.scheduler, app.now) : null); - const sink = $derived(h ? sinkWords(h.sink, app.now) : null); - const late = $derived(!!h && heartbeatLate(h.heartbeat, app.now)); + const sink = $derived(h && h.sink !== undefined ? sinkWords(h.sink, app.now) : null); + const late = $derived(!!h?.heartbeat && heartbeatLate(h.heartbeat, app.now)); const openServerProblems = $derived((serverEvents.data ?? []).filter((e) => e.active).length); let open = $state(false); @@ -30,65 +37,78 @@
    {#if health.error} Couldn't load the server status: {health.error} - {:else if !h || !scheduler || !sink} + {:else if !h || !scheduler} Loading server status… {:else} Scheduler {scheduler.text} {#if scheduler.checkIn}· last check-in {scheduler.checkIn}{/if} - · - Event forwarding {sink.text} + {#if sink} + · + Event forwarding {sink.text} + {/if} {#if openServerProblems}·{openServerProblems} open server problem{openServerProblems === 1 ? '' : 's'}{/if} {/if}
    {#if open && h} + {@const lease = h.scheduler.lease}
    -
    -
    -

    Servers

    -
      - {#each h.scheduler.instances as i (i.instanceId)} -
    • - - {i.instanceId} - - {!i.schedulerEnabled ? 'web and API only' : h.scheduler.lease?.holder === i.instanceId ? 'running the scheduler' : 'standing by to take over'} - - seen {formatAgo(i.lastSeenAt, app.now)} -
    • - {/each} -
    - {#if h.scheduler.lease} -

    - Leader lease held by {h.scheduler.lease.holder} for {formatDuration(h.scheduler.lease.acquiredAt, null, app.now)}; renewed {formatAgo(h.scheduler.lease.renewedAt, app.now)}, expires {formatIn(h.scheduler.lease.expiresAt, app.now)}. -

    + {#if h.scheduler.instances || h.heartbeat || h.sink !== undefined} +
    + {#if h.scheduler.instances} +
    +

    Servers

    +
      + {#each h.scheduler.instances as i (i.instanceId)} +
    • + + {i.instanceId} + + {!i.schedulerEnabled ? 'web and API only' : lease?.holder === i.instanceId ? 'running the scheduler' : 'standing by to take over'} + + seen {formatAgo(i.lastSeenAt, app.now)} +
    • + {/each} +
    + {#if lease?.holder && lease.acquiredAt && lease.renewedAt} +

    + Leader lease held by {lease.holder} for {formatDuration(lease.acquiredAt, null, app.now)}; renewed {formatAgo(lease.renewedAt, app.now)}, expires {formatIn(lease.expiresAt, app.now)}. +

    + {/if} +
    {/if} -
    -
    -

    Heartbeat

    -

    - {h.heartbeat.lastEmittedAt ? `Sent ${formatAgo(h.heartbeat.lastEmittedAt, app.now)}` : 'Never sent'}{late ? ' — late' : ''} -

    -

    The server sends a heartbeat event every {h.heartbeat.intervalSeconds}s. If it stops arriving where you forward events, the server is down.

    -
    -
    -

    Event forwarding

    - {#if h.sink} -

    Sends problems as they happen to a {h.sink.kind} set in the server config ({h.sink.destinationSource}).

    -

    - {h.sink.eventKinds.length ? h.sink.eventKinds.join(', ') : 'Every kind of event'} · {h.sink.pendingEvents} waiting · last tried {formatAgo(h.sink.lastAttemptAt, app.now)} -

    - {#if h.sink.lastError}

    {h.sink.lastError}

    {/if} - {:else} -

    Not set up. Sensors still see every problem; forwarding pushes them out without code.

    + {#if h.heartbeat} +
    +

    Heartbeat

    +

    + {h.heartbeat.lastEmittedAt ? `Sent ${formatAgo(h.heartbeat.lastEmittedAt, app.now)}` : 'Never sent'}{late ? ' — late' : ''} +

    +

    The server sends a heartbeat event every {h.heartbeat.intervalSeconds}s. If it stops arriving where you forward events, the server is down.

    +
    + {/if} + {#if h.sink !== undefined} +
    +

    Event forwarding

    + {#if h.sink} +

    Sends problems as they happen to a {h.sink.kind} set in the server config ({h.sink.destinationSource}).

    +

    + {h.sink.eventKinds.length ? h.sink.eventKinds.join(', ') : 'Every kind of event'} · {h.sink.pendingEvents} waiting · last tried {formatAgo(h.sink.lastAttemptAt, app.now)} +

    + {#if h.sink.lastError}

    {h.sink.lastError}

    {/if} + {:else} +

    Not set up. Sensors still see every problem; forwarding pushes them out without code.

    + {/if} +
    {/if}
    -
    -

    Server problems

    - +

    Server problems

    + {:else} +

    Server problems

    + {/if} +
    {/if} diff --git a/ui/src/routes/locations/utils.ts b/ui/src/routes/locations/utils.ts index a921a88ed1..41070749c8 100644 --- a/ui/src/routes/locations/utils.ts +++ b/ui/src/routes/locations/utils.ts @@ -1,6 +1,6 @@ // Server status in plain words for the Locations page. Pure. -import type { HeartbeatStatus, SchedulerStatus, SinkStatus } from '$lib/api'; +import type { HeartbeatStatus, PlatformEvent, PlatformEventRecord, SchedulerStatus, SinkStatus } from '$lib/api'; import { formatAgo, formatIn, formatStamp } from '$lib/format/time'; export type StatusWords = { text: string; tone: string; title: string; checkIn?: string }; @@ -15,6 +15,9 @@ export function schedulerWords(scheduler: SchedulerStatus, now: number): StatusW title: 'No server holds the scheduler lease, so schedules and sensors aren’t starting runs. Another server takes over automatically when one is available.' }; } + if (!lease.holder || !lease.acquiredAt || !lease.renewedAt) { + return { text: 'running', tone: 'var(--sb-success)', title: `A server holds the scheduler lease and renews it before it runs out ${formatIn(lease.expiresAt, now)}` }; + } return { text: `running (${lease.holder})`, tone: 'var(--sb-success)', @@ -45,3 +48,8 @@ export function heartbeatLate(heartbeat: HeartbeatStatus, now: number): boolean export function replicaLive(lastSeenAt: string, now: number): boolean { return now - Date.parse(lastSeenAt) < 60_000; } + +/** A problem the server derives now, as a record without history, links or sensor reactions. */ +export function openRecord(event: PlatformEvent): PlatformEventRecord { + return { ...event, resolvedAt: null, links: [], reactions: [], payload: {} }; +} From 9b1b6c8facec573083f3af41062a986f4ebb711a Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Mon, 9 Nov 2026 01:59:37 +0000 Subject: [PATCH 095/106] fix(server): count copied grants in the maintenance read instead of returning them The maintenance read returned each old-name view's copied GRANT statements, which name warehouse roles, to anyone who may view the target. It now returns only how many grants were copied, read the way local sqb reads the stored list; the stored rows are unchanged. --- .../orchestration/maintenance_history.json | 6 +- clients/typescript/src/schema.d.ts | 60 ++++++++++--------- .../warehouse_history/_helpers/maintenance.rs | 38 ++++++++++-- .../src/warehouse_history/models.rs | 32 +++++++++- .../tests/api/test_maintenance.rs | 26 ++++++++ ui/src/lib/api/http-maintenance.test.ts | 6 +- ui/src/lib/api/http-maintenance.ts | 11 ++-- 7 files changed, 133 insertions(+), 46 deletions(-) diff --git a/benchmarks/duckshop/edge_cases/orchestration/maintenance_history.json b/benchmarks/duckshop/edge_cases/orchestration/maintenance_history.json index 860b31bec6..af7c6e61e1 100644 --- a/benchmarks/duckshop/edge_cases/orchestration/maintenance_history.json +++ b/benchmarks/duckshop/edge_cases/orchestration/maintenance_history.json @@ -40,14 +40,16 @@ "destination_model": "orders", "event_type": "view_created", "created_at": "2026-02-03T04:05:07Z", - "column_aliases": "{\"order_id\":\"legacy_order_id\"}" + "column_aliases": "{\"order_id\":\"legacy_order_id\"}", + "grants_copied": "[\"GRANT SELECT ON main.orders_legacy TO fulfillment_readers\", \"GRANT SELECT ON main.orders_legacy TO support_analysts\"]" }, { "event_id": "view-customers-1", "migration_event_id": "move-customers-1", "destination_model": "customers", "event_type": "required", - "created_at": "2026-02-02T04:05:06Z" + "created_at": "2026-02-02T04:05:06Z", + "grants_copied": "GRANT SELECT ON main.customers_legacy TO support_analysts" }, { "event_id": "view-orders-old-1", diff --git a/clients/typescript/src/schema.d.ts b/clients/typescript/src/schema.d.ts index 3efe664d23..32140c3e3f 100644 --- a/clients/typescript/src/schema.d.ts +++ b/clients/typescript/src/schema.d.ts @@ -1641,7 +1641,38 @@ export interface components { janitor_events: components["schemas"]["WarehouseJanitorRecord"][]; migrations: components["schemas"]["WarehouseMigrationRecord"][]; /** @description Every step recorded for the listed moves, oldest first; same-time steps are not in step order. */ - old_name_views: components["schemas"]["WarehouseOldNameRecord"][]; + old_name_views: components["schemas"]["MaintenanceOldNameView"][]; + }; + /** @description One old-name view step as people read it: copied grants are counted, never returned as statements. */ + MaintenanceOldNameView: { + archive_name?: string | null; + column_aliases?: string | null; + /** Format: date-time */ + created_at: string; + destination_model: string; + drop_reason?: string | null; + event_id: string; + event_type: string; + /** Format: date-time */ + expires_at?: string | null; + /** + * Format: int64 + * @description How many grants were copied onto the view; null until it exists or when the stored list is unreadable. + */ + grants_copied?: number | null; + migration_event_id: string; + new_database?: string | null; + new_name: string; + new_schema: string; + old_database?: string | null; + old_name: string; + old_schema?: string | null; + run_id: string; + state_database?: string | null; + state_schema: string; + target_name?: string | null; + view_retention?: string | null; + view_sql?: string | null; }; /** @description One bounded page of one state table from the registered reader of a published generation. */ MetadataBatch: { @@ -2560,33 +2591,6 @@ export interface components { /** Format: date-time */ observed_at: string; }; - /** @description Preserve nullable compatibility-view facts without projecting live warehouse existence. */ - WarehouseOldNameRecord: { - archive_name?: string | null; - column_aliases?: string | null; - /** Format: date-time */ - created_at: string; - destination_model: string; - drop_reason?: string | null; - event_id: string; - event_type: string; - /** Format: date-time */ - expires_at?: string | null; - grants_copied?: string | null; - migration_event_id: string; - new_database?: string | null; - new_name: string; - new_schema: string; - old_database?: string | null; - old_name: string; - old_schema?: string | null; - run_id: string; - state_database?: string | null; - state_schema: string; - target_name?: string | null; - view_retention?: string | null; - view_sql?: string | null; - }; /** @description Physical namespace of SQLBuild's source-freshness state table, never a source query. */ WarehouseStateLocation: { database?: string | null; diff --git a/crates/sqlbuild-orchestration/src/warehouse_history/_helpers/maintenance.rs b/crates/sqlbuild-orchestration/src/warehouse_history/_helpers/maintenance.rs index 6c52ab9094..8e2cfaa832 100644 --- a/crates/sqlbuild-orchestration/src/warehouse_history/_helpers/maintenance.rs +++ b/crates/sqlbuild-orchestration/src/warehouse_history/_helpers/maintenance.rs @@ -2,12 +2,13 @@ use serde::Deserialize; use serde::de::DeserializeOwned; +use sqlbuild_state::events::main::decoded_grants::decoded_grants; use uuid::Uuid; use crate::store::errors::ControlError; use crate::store::main::query_records::query_records; use crate::store::models::{ControlTransaction, Parameter}; -use crate::warehouse_history::models::MaintenanceHistory; +use crate::warehouse_history::models::{MaintenanceHistory, MaintenanceOldNameView}; use crate::warehouse_metadata::models::{ WarehouseJanitorRecord, WarehouseMigrationRecord, WarehouseOldNameRecord, }; @@ -55,7 +56,7 @@ pub(crate) async fn maintenance_history( } }; let mut transaction = transaction; - let mut old_name_views: Vec = Vec::new(); + let mut stored_views: Vec = Vec::new(); if !migrations.is_empty() { let mut parameters = scope.clone(); parameters.extend( @@ -64,7 +65,7 @@ pub(crate) async fn maintenance_history( .map(|migration| Parameter::Text(migration.event_id.clone())), ); let placeholders = Parameter::placeholders(3..=parameters.len()); - (transaction, old_name_views) = records(transaction, + (transaction, stored_views) = records(transaction, &format!("SELECT record_json AS record FROM sqb_warehouse_old_name_views WHERE location_id=$1 AND target=$2 \ AND migration_event_id IN ({placeholders}) ORDER BY observed_at,event_id"), parameters).await?; @@ -80,8 +81,37 @@ pub(crate) async fn maintenance_history( transaction, MaintenanceHistory { migrations, - old_name_views, + old_name_views: stored_views.into_iter().map(old_name_view).collect(), janitor_events, }, )) } + +/// The stored step with its grant statements reduced to their number, read the way local `sqb` reads them. +fn old_name_view(record: WarehouseOldNameRecord) -> MaintenanceOldNameView { + MaintenanceOldNameView { + grants_copied: decoded_grants(record.grants_copied.as_deref()) + .map(|statements| statements.len() as u64), + state_database: record.state_database, + state_schema: record.state_schema, + event_id: record.event_id, + target_name: record.target_name, + event_type: record.event_type, + migration_event_id: record.migration_event_id, + destination_model: record.destination_model, + old_database: record.old_database, + old_schema: record.old_schema, + old_name: record.old_name, + new_database: record.new_database, + new_schema: record.new_schema, + new_name: record.new_name, + view_retention: record.view_retention, + archive_name: record.archive_name, + column_aliases: record.column_aliases, + view_sql: record.view_sql, + expires_at: record.expires_at, + drop_reason: record.drop_reason, + run_id: record.run_id, + created_at: record.created_at, + } +} diff --git a/crates/sqlbuild-orchestration/src/warehouse_history/models.rs b/crates/sqlbuild-orchestration/src/warehouse_history/models.rs index 57c26430cb..70aee07613 100644 --- a/crates/sqlbuild-orchestration/src/warehouse_history/models.rs +++ b/crates/sqlbuild-orchestration/src/warehouse_history/models.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::warehouse_metadata::models::{ - WarehouseAuditRecord, WarehouseJanitorRecord, WarehouseMigrationRecord, WarehouseOldNameRecord, + WarehouseAuditRecord, WarehouseJanitorRecord, WarehouseMigrationRecord, }; /// One stored check result. Its SQL, error texts and evidence rows need their own permissions. @@ -96,12 +96,40 @@ pub struct CheckEvidence { pub truncated: Option, } +/// One old-name view step as people read it: copied grants are counted, never returned as statements. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] +pub struct MaintenanceOldNameView { + pub state_database: Option, + pub state_schema: String, + pub event_id: String, + pub target_name: Option, + pub event_type: String, + pub migration_event_id: String, + pub destination_model: String, + pub old_database: Option, + pub old_schema: Option, + pub old_name: String, + pub new_database: Option, + pub new_schema: String, + pub new_name: String, + pub view_retention: Option, + pub archive_name: Option, + pub column_aliases: Option, + /// How many grants were copied onto the view; null until it exists or when the stored list is unreadable. + pub grants_copied: Option, + pub view_sql: Option, + pub expires_at: Option>, + pub drop_reason: Option, + pub run_id: String, + pub created_at: DateTime, +} + /// Recorded moves, their old-name view steps and janitor cleanups, newest moves first. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] pub struct MaintenanceHistory { pub migrations: Vec, /// Every step recorded for the listed moves, oldest first; same-time steps are not in step order. - pub old_name_views: Vec, + pub old_name_views: Vec, /// Empty when the history is for one model; cleanups are not tied to models. pub janitor_events: Vec, } diff --git a/crates/sqlbuild-orchestration/tests/api/test_maintenance.rs b/crates/sqlbuild-orchestration/tests/api/test_maintenance.rs index c19db6b760..f395d47821 100644 --- a/crates/sqlbuild-orchestration/tests/api/test_maintenance.rs +++ b/crates/sqlbuild-orchestration/tests/api/test_maintenance.rs @@ -110,6 +110,32 @@ fn given_moves_views_and_cleanups_when_people_read_maintenance_then_each_model_s "another target's steps never appear" ); assert_eq!(ids(&all, "janitor_events"), ["cleanup-2", "cleanup-1"]); + let grants: Vec<(&str, &Value)> = all["old_name_views"] + .as_array() + .expect("views") + .iter() + .map(|view| { + ( + view["event_id"].as_str().expect("id"), + &view["grants_copied"], + ) + }) + .collect(); + assert_eq!( + grants, + [ + ("view-orders-old-1", &Value::Null), + ("view-customers-1", &Value::Null), + ("view-orders-1", &Value::Null), + ("view-orders-2", &json!(2)) + ], + "copied grants are counted, and unreadable stored text reads as unknown" + ); + let body = all.to_string(); + assert!( + !body.contains("GRANT") && !body.contains("support_analysts"), + "no grant statement or grantee reaches people: {body}" + ); assert_eq!(all["migrations"][0]["origin_model"], Value::Null); assert_eq!(all["migrations"][0]["discovery"], "automatic"); diff --git a/ui/src/lib/api/http-maintenance.test.ts b/ui/src/lib/api/http-maintenance.test.ts index c94783fd59..01cb4533c4 100644 --- a/ui/src/lib/api/http-maintenance.test.ts +++ b/ui/src/lib/api/http-maintenance.test.ts @@ -25,7 +25,7 @@ function migration(eventId: string, originSchema: string | null, createdAt: stri }; } -function step(migrationEventId: string, eventType: string, createdAt: string, facts: Partial = {}): W['WarehouseOldNameRecord'] { +function step(migrationEventId: string, eventType: string, createdAt: string, facts: Partial = {}): W['MaintenanceOldNameView'] { return { state_schema: 'sqlbuild_state', event_id: `${migrationEventId}-${eventType}`, @@ -49,7 +49,7 @@ describe('maintenance history mapping', () => { old_name_views: [ step('orders', 'required', '2026-11-07T10:00:00Z', { view_retention: '30d' }), step('customers', 'required', '2026-11-08T10:00:00Z', { view_retention: 'false' }), - step('orders', 'view_created', '2026-11-07T10:01:00Z', { expires_at: '2026-12-07T10:01:00Z', column_aliases: '{"amount":"total","id":"order_id"}', grants_copied: '["GRANT SELECT"]' }), + step('orders', 'view_created', '2026-11-07T10:01:00Z', { expires_at: '2026-12-07T10:01:00Z', column_aliases: '{"amount":"total","id":"order_id"}', grants_copied: 3 }), step('orders', 'view_dropped', '2026-11-08T11:00:00Z', { drop_reason: 'Removed by hand' }) ], janitor_events: [] @@ -64,7 +64,7 @@ describe('maintenance history mapping', () => { it('takes each fact from the step that recorded it', () => { const orders = history.oldNameViews[1]; - expect([orders.retention, orders.expiresAt, orders.dropReason, orders.grantsCopied]).toEqual(['30d', '2026-12-07T10:01:00Z', 'Removed by hand', 1]); + expect([orders.retention, orders.expiresAt, orders.dropReason, orders.grantsCopied]).toEqual(['30d', '2026-12-07T10:01:00Z', 'Removed by hand', 3]); expect(orders.columnAliases).toEqual([ { old: 'amount', new: 'total' }, { old: 'id', new: 'order_id' } diff --git a/ui/src/lib/api/http-maintenance.ts b/ui/src/lib/api/http-maintenance.ts index 8a82971247..300ab4835c 100644 --- a/ui/src/lib/api/http-maintenance.ts +++ b/ui/src/lib/api/http-maintenance.ts @@ -25,19 +25,16 @@ function columnAliases(text: string | null | undefined): OldNameView['columnAlia return text ? Object.entries(JSON.parse(text) as Record).map(([old, renamed]) => ({ old, new: renamed })) : []; } -/** Stored as a JSON list of the grant statements copied to the view. */ -const grantCount = (text: string | null | undefined): number | null => (text ? (JSON.parse(text) as string[]).length : null); - /** One view per move, from its append-only steps (oldest first); each fact comes from the newest step that records it. */ -function toOldNameViews(rows: Wire['WarehouseOldNameRecord'][], migrationOrder: string[]): OldNameView[] { - const steps = new Map(); +function toOldNameViews(rows: Wire['MaintenanceOldNameView'][], migrationOrder: string[]): OldNameView[] { + const steps = new Map(); for (const row of rows) steps.set(row.migration_event_id, [...(steps.get(row.migration_event_id) ?? []), row]); const rank = (id: string) => (migrationOrder.includes(id) ? migrationOrder.indexOf(id) : migrationOrder.length); return [...steps.entries()] .sort(([a], [b]) => rank(a) - rank(b)) .map(([migrationEventId, recorded]) => { const first = recorded[0]; - const newest = (key: K) => [...recorded].reverse().find((r) => r[key] != null)?.[key] ?? null; + const newest = (key: K) => [...recorded].reverse().find((r) => r[key] != null)?.[key] ?? null; return { migrationEventId, destinationModel: first.destination_model, @@ -46,7 +43,7 @@ function toOldNameViews(rows: Wire['WarehouseOldNameRecord'][], migrationOrder: retention: (newest('view_retention') as string | null) ?? '', archiveName: newest('archive_name') as string | null, columnAliases: columnAliases(newest('column_aliases') as string | null), - grantsCopied: grantCount(newest('grants_copied') as string | null), + grantsCopied: newest('grants_copied') as number | null, expiresAt: newest('expires_at') as string | null, dropReason: newest('drop_reason') as string | null, steps: recorded.map((r) => ({ eventType: r.event_type as OldNameEventType, at: r.created_at, runId: r.run_id })) From 9534b3c1b582fc633770e38a039b003215f71ffc Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Mon, 9 Nov 2026 02:14:15 +0000 Subject: [PATCH 096/106] feat(ui): backfill a job over a cursor window from its launch form People who may rewrite history can give a start and an end when they run a job or group. Whole numbers on both ends send an integer window; anything else goes as typed for the server to validate. Server error bodies that aren't JSON, such as a request the route couldn't read, are now shown as the server wrote them instead of as the bare status. --- ui/src/lib/api/http-client.test.ts | 44 ++++++++++++++++++++ ui/src/lib/api/http-client.ts | 12 ++++-- ui/src/lib/api/http-jobs.test.ts | 19 ++++++++- ui/src/lib/api/http-jobs.ts | 16 ++++++- ui/src/routes/jobs/[name]/launch-form.svelte | 35 +++++++--------- 5 files changed, 101 insertions(+), 25 deletions(-) diff --git a/ui/src/lib/api/http-client.test.ts b/ui/src/lib/api/http-client.test.ts index b26c692767..946eeabc67 100644 --- a/ui/src/lib/api/http-client.test.ts +++ b/ui/src/lib/api/http-client.test.ts @@ -413,6 +413,50 @@ describe('locations', () => { }); }); +describe('launchJob', () => { + function server(answer: Response) { + const posts: unknown[] = []; + vi.stubGlobal('document', { cookie: 'sqb-csrf=token-1' }); + vi.stubGlobal( + 'fetch', + vi.fn(async (request: Request) => { + if (request.method === 'POST') { + posts.push(await request.json()); + return answer.clone(); + } + return Response.json([]); + }) + ); + return { posts, client: createHttpClient({ baseUrl: 'http://ui.test', onUnauthorized: () => {} }) }; + } + + it('sends a backfill window with the job run request', async () => { + const { posts, client } = server(Response.json({ error: 'stop here' }, { status: 409 })); + await expect(client.launchJob(ORDERS, 'daily', { window: { type: 'timestamp', start: '1200', end: '1500' } })).rejects.toThrow(); + expect(posts[0]).toMatchObject({ job_key: 'daily', target: 'development', window: { kind: 'integer', start: 1200, end: 1500 } }); + }); + + it('leaves the window out of an ordinary run', async () => { + const { posts, client } = server(Response.json({ error: 'stop here' }, { status: 409 })); + await expect(client.launchJob(ORDERS, 'daily')).rejects.toThrow(); + expect(posts[0]).not.toHaveProperty('window'); + }); + + it("shows a plain-text refusal of a body the server couldn't read", async () => { + const text = 'Failed to deserialize the JSON body into the target type: window: input contains invalid characters'; + const { client } = server(new Response(text, { status: 422, headers: { 'content-type': 'text/plain; charset=utf-8' } })); + await expect(client.launchJob(ORDERS, 'daily', { window: { type: 'timestamp', start: '2026-10-01', end: '2026-10-31T00:00:00Z' } })).rejects.toMatchObject({ status: 422, message: text }); + }); + + it("shows the server's refusal of a window as it comes", async () => { + const { client } = server(Response.json({ error: 'backfill window start must not follow end' }, { status: 400 })); + await expect(client.launchJob(ORDERS, 'daily', { window: { type: 'timestamp', start: '20', end: '10' } })).rejects.toMatchObject({ + status: 400, + message: expect.stringContaining('backfill window start must not follow end') + }); + }); +}); + describe('administration writes', () => { const ada = { principal_id: 'p-ada', name: 'ada', created_at: '2026-11-01T00:00:00Z', disabled_at: null, has_password: true, groups: [], active_sessions: 2, last_signed_in_at: null }; diff --git a/ui/src/lib/api/http-client.ts b/ui/src/lib/api/http-client.ts index 606cd2c49d..1e664402d9 100644 --- a/ui/src/lib/api/http-client.ts +++ b/ui/src/lib/api/http-client.ts @@ -11,13 +11,13 @@ import { toMaintenanceHistory } from './http-maintenance'; import { overviewOf } from './http-overview'; import { toPlatformEvents, toPlatformHealth } from './http-problems'; import { roleDetailOf, toRoleDefinition } from './http-roles'; -import { jobDetailOf, toJob, toTick, toTrigger, triggerDetailOf } from './http-jobs'; +import { jobDetailOf, runWindowOf, toJob, toTick, toTrigger, triggerDetailOf } from './http-jobs'; import { RUN_WINDOW, toAdhocCheck, runDetailOf, runRowPage, runsMatching, runTabCountsOf, toRunNode, toRuns, toRunEvent, toRunOutput, toRunStatements } from './http-runs'; import type { ChangeSubscription, ChangedChannel, ChangedSignal, ConnectionState } from './signals'; import type { AuthMode, Bootstrap, Capability, Location, PlatformEvent, Scope, WarehouseSync } from './types'; import type { Account, AuditEntry, AuditQuery, ConnectionTest, LocationDetail, LocationSummary, PlatformHealth, MaintenanceHistory, RoleAssignment, RoleAssignmentRequest, RoleDefinition, RoleDetail } from './types/platform'; import type { CatalogNode, CatalogNodeDetail, Check, CheckDetail, CheckEvidence, CheckSql, Overview, SnapshotChange, ColumnLineage } from './types/build'; -import type { AdhocRunCheck, AdhocRunRequest, Job, JobDetail, RerunMode, SnapshotNode, Run, RunDetail, RunListQuery, RunRowPage, RunRowQuery, RunTabCounts, Trigger, TriggerDetail, TriggerState, RunEventPage, RunOutputLine, RunStatement, StatementSql } from './types/operate'; +import type { AdhocRunCheck, AdhocRunRequest, Job, JobDetail, JobLaunchOptions, RerunMode, SnapshotNode, Run, RunDetail, RunListQuery, RunRowPage, RunRowQuery, RunTabCounts, Trigger, TriggerDetail, TriggerState, RunEventPage, RunOutputLine, RunStatement, StatementSql } from './types/operate'; type Wire = components['schemas']; @@ -321,9 +321,11 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt const wire = jobs.find((job) => job.job_key === key); return wire ? jobDetailOf(toJob(wire, triggers, runs), wire) : null; }, - async launchJob(scope: Scope, key: string): Promise { + /** A window makes it a backfill, which the server also checks against `run.rewrite_history`. */ + async launchJob(scope: Scope, key: string, options?: JobLaunchOptions): Promise { const { locationId, target } = parseLocationKey(scope.locationId); - const body = { run_id: crypto.randomUUID(), location_id: locationId, target, job_key: key, priority: 0, reason: 'Started from the web UI' }; + const window = options?.window ? runWindowOf(options.window) : undefined; + const body = { run_id: crypto.randomUUID(), location_id: locationId, target, job_key: key, priority: 0, reason: window ? 'Backfill started from the web UI' : 'Started from the web UI', window }; const { error, response } = await http.POST('/api/control/runs', { body, params: { header: { 'x-sqb-csrf': csrfToken(document.cookie) } } }); inFlight.clear(); if (response.status === 401) onUnauthorized(); @@ -778,7 +780,9 @@ export function createHttpClient({ baseUrl = '', onUnauthorized }: HttpClientOpt }) as unknown as ApiClient; } +/** The server's message: a JSON error body's text, or a plain-text body such as a request the route couldn't read. */ function errorText(error: unknown, response: Response): string { + if (typeof error === 'string' && error.trim()) return error.trim(); const message = (error as { message?: unknown; error?: unknown } | undefined)?.message ?? (error as { error?: unknown } | undefined)?.error; return typeof message === 'string' ? message : `${response.status} ${response.statusText}`; } diff --git a/ui/src/lib/api/http-jobs.test.ts b/ui/src/lib/api/http-jobs.test.ts index de992dfd7d..2b26d965ae 100644 --- a/ui/src/lib/api/http-jobs.test.ts +++ b/ui/src/lib/api/http-jobs.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import type { components } from '$control/schema.d.ts'; -import { cadenceOf, jobDetailOf, toJob, toTick, toTrigger, triggerDetailOf, triggerKindOf } from './http-jobs'; +import { cadenceOf, jobDetailOf, runWindowOf, toJob, toTick, toTrigger, triggerDetailOf, triggerKindOf } from './http-jobs'; import type { Run } from './types/operate'; type W = components['schemas']; @@ -69,3 +69,20 @@ describe('job mapping', () => { expect(toJob({ ...wire, job_key: 'nightly', kind: 'group' }, triggers, []).kind).toBe('group'); }); }); + +describe('runWindowOf', () => { + const typed = (start: string, end: string) => runWindowOf({ type: 'timestamp', start, end }); + + it('sends whole numbers on both ends as an integer window, negatives included', () => { + expect(typed(' -3 ', '11')).toEqual({ kind: 'integer', start: -3, end: 11 }); + }); + + it('sends anything else as typed for the server to validate, without reading zones or order', () => { + expect(typed('2026-10-31T00:00:00+02:00', '2026-10-01')).toEqual({ kind: 'timestamp', start: '2026-10-31T00:00:00+02:00', end: '2026-10-01' }); + expect(typed('1200', 'yesterday')).toEqual({ kind: 'timestamp', start: '1200', end: 'yesterday' }); + }); + + it('refuses a whole number JSON would round instead of sending another value', () => { + expect(() => typed('9007199254740993', '9007199254740995')).toThrow('exactly'); + }); +}); diff --git a/ui/src/lib/api/http-jobs.ts b/ui/src/lib/api/http-jobs.ts index af8de57e62..91a8cd10a5 100644 --- a/ui/src/lib/api/http-jobs.ts +++ b/ui/src/lib/api/http-jobs.ts @@ -1,5 +1,6 @@ import type { components } from '$control/schema.d.ts'; -import type { Job, JobDetail, Run, RunRef, Tick, Trigger, TriggerDetail, TriggerKind } from './types/operate'; +import { ApiError } from './client'; +import type { CursorWindow, Job, JobDetail, Run, RunRef, Tick, Trigger, TriggerDetail, TriggerKind } from './types/operate'; type Wire = components['schemas']; @@ -143,3 +144,16 @@ export function jobDetailOf(job: Job, wire: Wire['JobRecord']): JobDetail { cursorType: null }; } + +/** + * A backfill's bounds as typed: whole numbers on both ends make an integer window, anything else + * goes as timestamps for the server to validate. Only integers JSON would round are refused here. + */ +export function runWindowOf(window: CursorWindow): Wire['RunWindow'] { + const start = (window.start ?? '').trim(); + const end = (window.end ?? '').trim(); + if (![start, end].every((v) => /^-?\d+$/.test(v))) return { kind: 'timestamp', start, end }; + const [from, to] = [start, end].map(Number); + if (![from, to].every(Number.isSafeInteger)) throw new ApiError(`Whole numbers beyond ${Number.MAX_SAFE_INTEGER} can't be sent exactly`, 400); + return { kind: 'integer', start: from, end: to }; +} diff --git a/ui/src/routes/jobs/[name]/launch-form.svelte b/ui/src/routes/jobs/[name]/launch-form.svelte index 8da2536351..fc202ae288 100644 --- a/ui/src/routes/jobs/[name]/launch-form.svelte +++ b/ui/src/routes/jobs/[name]/launch-form.svelte @@ -16,15 +16,8 @@ const canRewrite = $derived(app.can('run.rewrite_history')); const canFullRefresh = $derived(app.client.supports('launchJobFromScratch')); const rewrites = $derived(fullRefresh || useWindow); - const placeholder = $derived(job.cursorType === 'integer' ? 'e.g. 1200' : 'e.g. 2026-10-01T00:00:00Z'); - const windowInvalid = $derived( - useWindow && - (!start && !end - ? 'Give a start or an end' - : job.cursorType === 'integer' - ? [start, end].some((v) => v && !/^\d+$/.test(v)) && 'Use whole numbers' - : [start, end].some((v) => v && Number.isNaN(Date.parse(v))) && 'Use a date and time like 2026-10-01T00:00:00Z') - ); + // The server checks the bounds against the job's cursors and reports anything it refuses. + const windowInvalid = $derived(useWindow && (!start.trim() || !end.trim()) && 'Give a start and an end'); async function launch() { const scope = app.scope; @@ -32,7 +25,7 @@ busy = true; error = null; try { - const options = { window: useWindow && job.cursorType ? { type: job.cursorType, start: start || null, end: end || null } : undefined }; + const options = { window: useWindow ? { type: job.cursorType ?? 'timestamp', start, end } : undefined }; const run = fullRefresh ? await app.client.launchJobFromScratch(scope, job.key, options) : await app.client.launchJob(scope, job.key, options); onclose(); await goto(`/runs/${run.id}`); @@ -49,7 +42,7 @@

    {job.kind === 'group' ? `Starts a group run, with one run per job in it${job.members ? ` (${job.members.length})` : ''}.` : `Builds ${job.nodeCount} nodes with the job's options.`} If another run is updating the same data, it waits for it to finish.

    - {#if canFullRefresh || job.cursorType} + {#if canFullRefresh || canRewrite}
    {#if canFullRefresh} {/if} - {#if job.cursorType} -