Repository: briefercloud/briefer (language: python)

Issue:
minimizing SQL  makes buttons on the right display weirdly
Hide all buttons except "play" and "hide"


Discussion / hints:

Fix patch:
diff --git a/apps/api/src/python/query/athena.ts b/apps/api/src/python/query/athena.ts
index a542f186..47cd47be 100644
--- a/apps/api/src/python/query/athena.ts
+++ b/apps/api/src/python/query/athena.ts
@@ -1,6 +1,10 @@
 import { v4 as uuidv4 } from 'uuid'
 import { AthenaDataSource } from '@briefer/database/types/datasources/athena'
-import { RunQueryResult, SuccessRunQueryResult } from '@briefer/types'
+import {
+  RunQueryResult,
+  SQLQueryConfiguration,
+  SuccessRunQueryResult,
+} from '@briefer/types'
 import { getDatabaseURL } from '@briefer/database'
 import { makeQuery } from './index.js'
 import { renderJinja } from '../index.js'
@@ -13,7 +17,8 @@ export async function makeAthenaQuery(
   datasource: AthenaDataSource,
   encryptionKey: string,
   sql: string,
-  onProgress: (result: SuccessRunQueryResult) => void
+  onProgress: (result: SuccessRunQueryResult) => void,
+  configuration: SQLQueryConfiguration | null
 ): Promise<[Promise<RunQueryResult>, () => Promise<void>]> {
   const databaseURL = await getDatabaseURL(
     { type: 'athena', data: datasource },
@@ -57,6 +62,18 @@ export async function makeAthenaQuery(
     ]
   }
 
+  let resultReuseByAgeConfigurationMaxAgeInMinutes = configuration?.athena
+    ?.resultReuseConfiguration.resultReuseByAgeConfiguration.enabled
+    ? configuration?.athena?.resultReuseConfiguration
+        .resultReuseByAgeConfiguration.maxAgeInMinutes
+    : null
+  if (resultReuseByAgeConfigurationMaxAgeInMinutes !== null) {
+    resultReuseByAgeConfigurationMaxAgeInMinutes = Math.min(
+      1440,
+      Math.max(1, resultReuseByAgeConfigurationMaxAgeInMinutes)
+    )
+  }
+
   const code = `
 def briefer_make_athena_query():
     import boto3
@@ -185,12 +202,25 @@ def briefer_make_athena_query():
             region_name="${datasource.region}",
         )
 
+        result_reuse_configuration = {"ResultReuseByAgeConfiguration": {"Enabled": False}}
+        result_reuse_by_age_configuration_max_age_in_minutes = ${
+          resultReuseByAgeConfigurationMaxAgeInMinutes
+            ? resultReuseByAgeConfigurationMaxAgeInMinutes
+            : 'None'
+        }
+        if result_reuse_by_age_configuration_max_age_in_minutes:
+            result_reuse_configuration["ResultReuseByAgeConfiguration"] = {
+              "Enabled": True,
+              "MaxAgeInMinutes": result_reuse_by_age_configuration_max_age_in_minutes,
+            }
+
         query_response = athena_client.start_query_execution(
             QueryString=${JSON.stringify(renderedQuery)},
             QueryExecutionContext={"Database": "default"},
             ResultConfiguration={
                 "OutputLocation": s3_staging_dir,
             },
+            ResultReuseConfiguration=result_reuse_configuration,
         )
 
         query_id = query_response["QueryExecutionId"]
@@ -265,8 +295,14 @@ def briefer_make_athena_query():
             s3_bucket = s3_url.netloc
             s3_dir = s3_url.path[1:]
 
-            print(json.dumps({"type": "log", "message": f"Downloading s3://{s3_bucket}/{s3_dir}{query_id}.csv"}))
-            s3.download_file(s3_bucket, f"{s3_dir}{query_id}.csv", f"{tmpdir}/{query_id}.csv")
+            output_location = query_status.get("QueryExecution", {}).get("ResultConfiguration", {}).get("OutputLocation", None)
+            if output_location:
+                # remove s3://bucket_name/ prefix
+                output_location = urlparse(output_location).path[1:]
+            else:
+                output_location= f"{s3_dir}{query_id}.csv"
+            print(json.dumps({"type": "log", "message": f"Downloading s3://{s3_bucket}/{output_location}"}))
+            s3.download_file(s3_bucket, output_location, f"{tmpdir}/{query_id}.csv")
             if not os.path.exists(flag_file_path):
                 result = {
                     "type": "abort-error",
diff --git a/apps/api/src/python/query/index.ts b/apps/api/src/python/query/index.ts
index 110e2adc..aa5d53f3 100644
--- a/apps/api/src/python/query/index.ts
+++ b/apps/api/src/python/query/index.ts
@@ -5,6 +5,7 @@ import {
   DataFrameColumn,
   DataFrameStringColumn,
   RunQueryResult,
+  SQLQueryConfiguration,
   SuccessRunQueryResult,
   jsonString,
 } from '@briefer/types'
@@ -30,7 +31,8 @@ export async function makeSQLQuery(
   datasource: DataSource | 'duckdb',
   encryptionKey: string,
   sql: string,
-  onProgress: (result: SuccessRunQueryResult) => void
+  onProgress: (result: SuccessRunQueryResult) => void,
+  configuration: SQLQueryConfiguration | null
 ): Promise<[Promise<RunQueryResult>, () => Promise<void>]> {
   if (datasource === 'duckdb') {
     return makeDuckDBQuery(
@@ -80,7 +82,8 @@ export async function makeSQLQuery(
         datasource.data,
         encryptionKey,
         sql,
-        onProgress
+        onProgress,
+        configuration
       )
       break
     case 'oracle':
diff --git a/apps/api/src/yjs/v2/executors/blocks/sql.ts b/apps/api/src/yjs/v2/executors/blocks/sql.ts
index 3a5d25de..a1ee7c2a 100644
--- a/apps/api/src/yjs/v2/executors/blocks/sql.ts
+++ b/apps/api/src/yjs/v2/executors/blocks/sql.ts
@@ -211,9 +211,10 @@ export class SQLExecutor implements ISQLExecutor {
           block.removeAttribute('result')
 
           const {
+            id: blockId,
             aiSuggestions,
             source,
-            id: blockId,
+            configuration,
           } = getSQLAttributes(block, this.blocks)
 
           const actualSource =
@@ -231,7 +232,8 @@ export class SQLExecutor implements ISQLExecutor {
               actualSource,
               (result) => {
                 block.setAttribute('result', result)
-              }
+              },
+              configuration
             )
             runningQuery.abort = abort
             if (signal?.aborted) {
diff --git a/apps/web/src/components/v2Editor/customBlocks/sql/SQLQueryConfigurationButton.tsx b/apps/web/src/components/v2Editor/customBlocks/sql/SQLQueryConfigurationButton.tsx
new file mode 100644
index 00000000..ee8eefec
--- /dev/null
+++ b/apps/web/src/components/v2Editor/customBlocks/sql/SQLQueryConfigurationButton.tsx
@@ -0,0 +1,257 @@
+import ReactDOM from 'react-dom'
+import { APIDataSource } from '@briefer/database'
+import { Switch, Transition } from '@headlessui/react'
+import { Cog6ToothIcon } from '@heroicons/react/24/solid'
+import clsx from 'clsx'
+import { DatabaseZapIcon } from 'lucide-react'
+import { SQLQueryConfiguration } from '@briefer/types'
+import { useCallback, useEffect, useRef, useState } from 'react'
+import useDropdownPosition from '@/hooks/dropdownPosition'
+
+interface Props {
+  value: SQLQueryConfiguration | null
+  onChange: (value: SQLQueryConfiguration) => void
+  dataSource: APIDataSource
+  disabled: boolean
+}
+function SQLQueryConfigurationButton(props: Props) {
+  if (props.dataSource.config.type !== 'athena') {
+    return null
+  }
+
+  const onToggleResultReuseByAgeConfiguration = useCallback(
+    (checked: boolean) => {
+      props.onChange({
+        ...(props.value ?? { version: 1 }),
+        athena: {
+          ...(props.value?.athena ?? {}),
+          resultReuseConfiguration: {
+            ...(props.value?.athena?.resultReuseConfiguration ?? {}),
+            resultReuseByAgeConfiguration: {
+              enabled: checked,
+              maxAgeInMinutes:
+                props.value?.athena?.resultReuseConfiguration
+                  .resultReuseByAgeConfiguration.maxAgeInMinutes ?? 60,
+            },
+          },
+        },
+      })
+    },
+    [props.onChange, props.value]
+  )
+
+  const buttonRef = useRef<HTMLButtonElement>(null)
+  const { onOpen, dropdownPosition } = useDropdownPosition(buttonRef)
+  const [open, setOpen] = useState(false)
+  const onToggleSettings = useCallback(() => {
+    setOpen((prev) => !prev)
+    onOpen()
+  }, [])
+
+  const popoverRef = useRef<HTMLDivElement>(null)
+  useEffect(() => {
+    const handleClickOutside = (e: MouseEvent) => {
+      if (
+        !popoverRef.current?.contains(e.target as Node) &&
+        !buttonRef.current?.contains(e.target as Node)
+      ) {
+        setOpen(false)
+      }
+    }
+
+    document.addEventListener('mousedown', handleClickOutside)
+    return () => {
+      document.removeEventListener('mousedown', handleClickOutside)
+    }
+  }, [])
+
+  const [
+    athenaResultReuseConfigurationResultReuseByAgeConfigurationMaxAgeInMinutes,
+    setAthenaResultReuseConfigurationResultReuseByAgeConfigurationMaxAgeInMinutes,
+  ] = useState(
+    (
+      props.value?.athena?.resultReuseConfiguration
+        .resultReuseByAgeConfiguration.maxAgeInMinutes ?? 60
+    ).toString()
+  )
+  const onChangeAthenaResultReuseConfigurationResultReuseByAgeConfigurationMaxAgeInMinutes =
+    useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
+      setAthenaResultReuseConfigurationResultReuseByAgeConfigurationMaxAgeInMinutes(
+        e.target.value
+      )
+    }, [])
+  const onBlurAthenaResultReuseConfigurationResultReuseByAgeConfigurationMaxAgeInMinutes =
+    useCallback(() => {
+      const asNumber = parseInt(
+        athenaResultReuseConfigurationResultReuseByAgeConfigurationMaxAgeInMinutes
+      )
+      if (Number.isNaN(asNumber)) {
+        setAthenaResultReuseConfigurationResultReuseByAgeConfigurationMaxAgeInMinutes(
+          '60'
+        )
+        return
+      }
+
+      if (asNumber < 1) {
+        setAthenaResultReuseConfigurationResultReuseByAgeConfigurationMaxAgeInMinutes(
+          '1'
+        )
+      } else if (asNumber > 1440) {
+        setAthenaResultReuseConfigurationResultReuseByAgeConfigurationMaxAgeInMinutes(
+          '1440'
+        )
+      }
+    }, [
+      athenaResultReuseConfigurationResultReuseByAgeConfigurationMaxAgeInMinutes,
+    ])
+  const athenaResultReuseConfigurationResultReuseByAgeConfigurationEnabled =
+    props.value?.athena?.resultReuseConfiguration.resultReuseByAgeConfiguration
+      .enabled ?? false
+  useEffect(() => {
+    const asNumber = parseInt(
+      athenaResultReuseConfigurationResultReuseByAgeConfigurationMaxAgeInMinutes
+    )
+    const previousValue =
+      props.value?.athena?.resultReuseConfiguration
+        .resultReuseByAgeConfiguration.maxAgeInMinutes ?? 60
+
+    if (previousValue === asNumber) {
+      return
+    }
+
+    if (!Number.isNaN(asNumber)) {
+      props.onChange({
+        ...(props.value ?? { version: 1 }),
+        athena: {
+          ...(props.value?.athena ?? {}),
+          resultReuseConfiguration: {
+            ...(props.value?.athena?.resultReuseConfiguration ?? {}),
+            resultReuseByAgeConfiguration: {
+              enabled:
+                athenaResultReuseConfigurationResultReuseByAgeConfigurationEnabled,
+              maxAgeInMinutes: asNumber,
+            },
+          },
+        },
+      })
+    }
+  }, [
+    athenaResultReuseConfigurationResultReuseByAgeConfigurationMaxAgeInMinutes,
+    props.onChange,
+    props.value,
+    athenaResultReuseConfigurationResultReuseByAgeConfigurationEnabled,
+  ])
+
+  return (
+    <>
+      <div className="relative">
+        <button
+          ref={buttonRef}
+          className="rounded-sm h-6 min-w-6 flex items-center justify-center border border-gray-200 text-gray-400 hover:bg-gray-50 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:text-gray-300"
+          disabled={props.disabled}
+          onClick={onToggleSettings}
+        >
+          <Cog6ToothIcon className="w-3 h-3" />
+        </button>
+        {ReactDOM.createPortal(
+          <Transition
+            as="div"
+            show={open}
+            className="z-[2000]"
+            enter="transition-opacity duration-300"
+            enterFrom="opacity-0"
+            enterTo="opacity-100"
+            leave="transition-opacity duration-300"
+            leaveFrom="opacity-100"
+            leaveTo="opacity-0"
+            style={{
+              position: 'absolute',
+              top: dropdownPosition.top - 24,
+              right: dropdownPosition.right + 28,
+            }}
+          >
+            <div
+              ref={popoverRef}
+              className="w-72 bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none font-sans rounded-md"
+            >
+              <div className="px-4 py-3">
+                <Switch.Group
+                  as="div"
+                  className="flex items-center justify-between space-x-2 pb-3"
+                >
+                  <Switch.Label
+                    as="span"
+                    className="text-sm leading-6 text-gray-900 flex items-center gap-x-2"
+                    passive
+                  >
+                    <DatabaseZapIcon strokeWidth={2} className="w-4 h-4" />
+                    Reuse query results
+                  </Switch.Label>
+
+                  <Switch
+                    checked={
+                      athenaResultReuseConfigurationResultReuseByAgeConfigurationEnabled
+                    }
+                    onChange={onToggleResultReuseByAgeConfiguration}
+                    className={clsx(
+                      athenaResultReuseConfigurationResultReuseByAgeConfigurationEnabled
+                        ? 'bg-primary-500'
+                        : 'bg-gray-200',
+                      'relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none disabled:cursor-not-allowed'
+                    )}
+                  >
+                    <span
+                      aria-hidden="true"
+                      className={clsx(
+                        athenaResultReuseConfigurationResultReuseByAgeConfigurationEnabled
+                          ? 'translate-x-4'
+                          : 'translate-x-0',
+                        'pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out'
+                      )}
+                    />
+                  </Switch>
+                </Switch.Group>
+                <div>
+                  <label
+                    htmlFor="athenaResultReuseConfigurationResultReuseByAgeConfigurationMaxAgeInMinutes"
+                    className="block text-xs font-medium leading-6 text-gray-900 pb-1"
+                  >
+                    Maximum age (minutes)
+                  </label>
+                  <input
+                    name="athenaResultReuseConfigurationResultReuseByAgeConfigurationMaxAgeInMinutes"
+                    type="number"
+                    placeholder="60"
+                    className={clsx(
+                      athenaResultReuseConfigurationResultReuseByAgeConfigurationEnabled
+                        ? 'bg-white'
+                        : 'bg-gray-100 cursor-not-allowed',
+                      'w-full border-0 rounded-md ring-1 ring-inset ring-gray-200 focus:ring-1 focus:ring-inset focus:ring-gray-300 px-2.5 text-gray-800 text-xs placeholder:text-gray-400'
+                    )}
+                    value={
+                      athenaResultReuseConfigurationResultReuseByAgeConfigurationMaxAgeInMinutes
+                    }
+                    onChange={
+                      onChangeAthenaResultReuseConfigurationResultReuseByAgeConfigurationMaxAgeInMinutes
+                    }
+                    disabled={
+                      !athenaResultReuseConfigurationResultReuseByAgeConfigurationEnabled
+                    }
+                    onBlur={
+                      onBlurAthenaResultReuseConfigurationResultReuseByAgeConfigurationMaxAgeInMinutes
+                    }
+                    min="1"
+                    max="1440"
+                  />
+                </div>
+              </div>
+            </div>
+          </Transition>,
+          document.body
+        )}
+      </div>
+    </>
+  )
+}
+
+export default SQLQueryConfigurationButton
diff --git a/apps/web/src/components/v2Editor/customBlocks/sql/index.tsx b/apps/web/src/components/v2Editor/customBlocks/sql/index.tsx
index a9e02d7f..512d1f10 100644
--- a/apps/web/src/components/v2Editor/customBlocks/sql/index.tsx
+++ b/apps/web/src/components/v2Editor/customBlocks/sql/index.tsx
@@ -61,6 +61,8 @@ import useProperties from '@/hooks/useProperties'
 import { SaveReusableComponentButton } from '@/components/ReusableComponents'
 import { useReusableComponents } from '@/hooks/useReusableComponents'
 import { CodeEditor } from '../../CodeEditor'
+import SQLQueryConfigurationButton from './SQLQueryConfigurationButton'
+import { SQLQueryConfiguration } from '@briefer/types'
 
 const NO_DS_TEXT = `-- No data sources connected. Please add one using the "data sources" menu on the bottom left
 -- Alternatively, you can upload files using the file upload block and query them using DuckDB as a data source.`
@@ -155,6 +157,11 @@ function SQLBlock(props: Props) {
     componentId,
   } = getSQLAttributes(props.block, props.blocks)
 
+  const dataSource = useMemo(
+    () => props.dataSources.find((d) => d.config.data.id === dataSourceId),
+    [props.dataSources, dataSourceId]
+  )
+
   const [
     { data: components },
     { create: createReusableComponent, update: updateReusableComponent },
@@ -203,7 +210,7 @@ function SQLBlock(props: Props) {
     }
   }, [status, props.block, onRun])
 
-  const { source } = getSQLAttributes(props.block, props.blocks)
+  const { source, configuration } = getSQLAttributes(props.block, props.blocks)
   const lastQuery = props.block.getAttribute('lastQuery')
   const lastQueryTime = props.block.getAttribute('lastQueryTime')
   const queryStatusText = useMemo(() => {
@@ -346,6 +353,13 @@ function SQLBlock(props: Props) {
     props.document.title,
   ])
 
+  const onChangeConfiguration = useCallback(
+    (value: SQLQueryConfiguration) => {
+      props.block.setAttribute('configuration', value)
+    },
+    [props.block]
+  )
+
   if (props.dashboardMode !== 'none') {
     if (!result) {
       return (
@@ -641,35 +655,52 @@ function SQLBlock(props: Props) {
             <MissingDataSourceTooltip />
           )}
         </button>
-        <ToChartButton
-          layout={props.layout}
-          block={props.block}
-          blocks={props.blocks}
-        />
+        {((result && !isResultHidden) || !isCodeHidden) && (
+          <ToChartButton
+            layout={props.layout}
+            block={props.block}
+            blocks={props.blocks}
+          />
+        )}
 
         <HiddenInPublishedButton
           isBlockHiddenInPublished={props.isBlockHiddenInPublished}
           onToggleIsBlockHiddenInPublished={onToggleIsBlockHiddenInPublished}
           hasMultipleTabs={props.hasMultipleTabs}
         />
-        <button
-          className="rounded-sm border border-gray-200 h-6 min-w-6 flex items-center justify-center relative group hover:bg-gray-50"
-          onClick={onSchemaExplorer}
-        >
-          <BookOpenIcon className="w-3 h-3 text-gray-400 group-hover:text-gray-500" />
-          <div className="font-sans pointer-events-none absolute -top-1 left-1/2 -translate-y-full -translate-x-1/2 w-max opacity-0 transition-opacity group-hover:opacity-100 bg-hunter-950 text-white text-xs p-2 rounded-md flex flex-col gap-y-1 max-w-40">
-            <span className="inline-flex items-center text-gray-400">
-              Explore schema of selected data source.
-            </span>
-          </div>
-        </button>
 
-        <SaveReusableComponentButton
-          isComponent={blockId === component?.blockId}
-          onSave={onSaveReusableComponent}
-          disabled={!props.isEditable || isComponentInstance}
-          isComponentInstance={isComponentInstance}
-        />
+        {((result && !isResultHidden) || !isCodeHidden) && (
+          <button
+            className="rounded-sm border border-gray-200 h-6 min-w-6 flex items-center justify-center relative group hover:bg-gray-50"
+            onClick={onSchemaExplorer}
+          >
+            <BookOpenIcon className="w-3 h-3 text-gray-400 group-hover:text-gray-500" />
+            <div className="font-sans pointer-events-none absolute -top-1 left-1/2 -translate-y-full -translate-x-1/2 w-max opacity-0 transition-opacity group-hover:opacity-100 bg-hunter-950 text-white text-xs p-2 rounded-md flex flex-col gap-y-1 max-w-40">
+              <span className="inline-flex items-center text-gray-400">
+                Explore schema of selected data source.
+              </span>
+            </div>
+          </button>
+        )}
+
+        {((result && !isResultHidden) || !isCodeHidden) && (
+          <SaveReusableComponentButton
+            isComponent={blockId === component?.blockId}
+            onSave={onSaveReusableComponent}
+            disabled={!props.isEditable || isComponentInstance}
+            isComponentInstance={isComponentInstance}
+          />
+        )}
+
+        {((result && !isResultHidden) || !isCodeHidden) &&
+          dataSource?.config.type === 'athena' && (
+            <SQLQueryConfigurationButton
+              dataSource={dataSource}
+              value={configuration}
+              onChange={onChangeConfiguration}
+              disabled={!props.isEditable}
+            />
+          )}
       </div>
     </div>
   )
diff --git a/packages/editor/src/blocks/sql.ts b/packages/editor/src/blocks/sql.ts
index 1dd6d841..5197749e 100644
--- a/packages/editor/src/blocks/sql.ts
+++ b/packages/editor/src/blocks/sql.ts
@@ -1,5 +1,5 @@
 import * as Y from 'yjs'
-import { RunQueryResult } from '@briefer/types'
+import { RunQueryResult, SQLQueryConfiguration } from '@briefer/types'
 import {
   BlockType,
   BaseBlock,
@@ -47,6 +47,7 @@ export type SQLBlock = BaseBlock<BlockType.SQL> & {
   editWithAIPrompt: Y.Text
   isEditWithAIPromptOpen: boolean
   aiSuggestions: Y.Text | null
+  configuration: SQLQueryConfiguration | null
 
   // wether the block originated from a reusable component and the id of the component
   componentId: string | null
@@ -85,6 +86,7 @@ export const makeSQLBlock = (
     isEditWithAIPromptOpen: false,
     aiSuggestions: null,
     componentId: null,
+    configuration: null,
   }
 
   for (const [key, value] of Object.entries(attrs)) {
@@ -123,6 +125,7 @@ export function getSQLAttributes(
     ),
     aiSuggestions: getSQLAISuggestions(block),
     componentId: getAttributeOr(block, 'componentId', null),
+    configuration: getAttributeOr(block, 'configuration', null),
   }
 }
 
@@ -164,6 +167,7 @@ export function duplicateSQLBlock(
         ? duplicateYText(prevAttributes.aiSuggestions)
         : null,
     componentId: options?.componentId ?? prevAttributes.componentId,
+    configuration: clone(prevAttributes.configuration),
   }
 
   const yBlock = new Y.XmlElement<SQLBlock>('block')
diff --git a/packages/editor/src/component.ts b/packages/editor/src/component.ts
index 7e57523f..ddda2ed5 100644
--- a/packages/editor/src/component.ts
+++ b/packages/editor/src/component.ts
@@ -98,6 +98,7 @@ export function updateBlockFromComponent(
       dataframeName: clone(compAttrs.dataframeName),
       dataSourceId: compAttrs.dataSourceId,
       isFileDataSource: compAttrs.isFileDataSource,
+      configuration: clone(compAttrs.configuration),
     }
     for (const [key, value] of Object.entries(nextAttrs)) {
       block.setAttribute(
diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts
index d7caa8e4..ae7fc964 100644
--- a/packages/types/src/index.ts
+++ b/packages/types/src/index.ts
@@ -1167,3 +1167,15 @@ export type Comment = {
   createdAt: string
   updatedAt: string
 }
+
+export type SQLQueryConfiguration = {
+  version: 1
+  athena?: {
+    resultReuseConfiguration: {
+      resultReuseByAgeConfiguration: {
+        enabled: boolean
+        maxAgeInMinutes: number
+      }
+    }
+  }
+}


Test patch:
diff --git a/apps/api/src/yjs/v2/executors/blocks/sql.test.ts b/apps/api/src/yjs/v2/executors/blocks/sql.test.ts
index 7295c72d..436f66d0 100644
--- a/apps/api/src/yjs/v2/executors/blocks/sql.test.ts
+++ b/apps/api/src/yjs/v2/executors/blocks/sql.test.ts
@@ -12,6 +12,7 @@ import { DataSource } from '@briefer/database'
 import {
   DataFrame,
   RunQueryResult,
+  SQLQueryConfiguration,
   SuccessRunQueryResult,
 } from '@briefer/types'
 import { SQLEvents } from '../../../../events/index.js'
@@ -119,7 +120,8 @@ describe('SQLExecutor', () => {
           _datasource: DataSource | 'duckdb',
           _encryptionKey: string,
           _sql: string,
-          onProgress: (result: SuccessRunQueryResult) => void
+          onProgress: (result: SuccessRunQueryResult) => void,
+          _configuration: SQLQueryConfiguration | null
         ) => {
           onProgress(result)
           return Promise.resolve([Promise.resolve(result), jest.fn()])
@@ -140,7 +142,8 @@ describe('SQLExecutor', () => {
         datasource,
         'dataSourcesEncryptionKey',
         'print("3")',
-        expect.any(Function)
+        expect.any(Function),
+        null
       )
       expect(block.getAttribute('result')).toEqual(result)
       expect(Array.from(dataframes.entries())).toEqual([
@@ -179,7 +182,8 @@ describe('SQLExecutor', () => {
           _datasource: DataSource | 'duckdb',
           _encryptionKey: string,
           _sql: string,
-          onProgress: (result: SuccessRunQueryResult) => void
+          onProgress: (result: SuccessRunQueryResult) => void,
+          _configuration: SQLQueryConfiguration | null
         ) => {
           onProgress(result)
           return Promise.resolve([Promise.resolve(result), jest.fn()])
@@ -201,7 +205,8 @@ describe('SQLExecutor', () => {
         datasource,
         'dataSourcesEncryptionKey',
         'print("1337")',
-        expect.any(Function)
+        expect.any(Function),
+        null
       )
       expect(block.getAttribute('result')).toEqual(result)
       expect(Array.from(dataframes.entries())).toEqual([