Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion .github/workflows/migrations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,18 @@ jobs:

if [ "${ENVIRONMENT}" = "dev" ]; then
echo "Dev environment — pushing schema directly (db:push)"
bun run db:push --force
# `--force` only suppresses the data-loss confirm, not drizzle's
# rename-vs-drop prompt, which fires (and crashes, no TTY) when a
# diff both adds and drops tables/columns at once. Turn that opaque
# crash into an actionable failure instead of a bare stack trace.
push_output="$(bun run db:push --force 2>&1)" && push_status=0 || push_status=$?
echo "$push_output"
if [ "$push_status" -ne 0 ]; then
if printf '%s' "$push_output" | grep -q 'Interactive prompts require a TTY'; then
echo "::error title=Dev schema push needs manual reconciliation::drizzle-kit push hit an interactive rename/drop prompt that CI cannot answer. The dev DB has drifted from schema.ts: it still holds table(s)/column(s) the schema no longer declares while the schema also adds new ones, so drizzle cannot tell a rename from a drop+create. Fix: drop the stale objects on the dev DB to match schema.ts — the same DROPs the latest versioned migration already applied to staging/prod (grep packages/db/migrations for the most recent DROP TABLE / DROP COLUMN) — then re-run this workflow. --force cannot bypass this prompt."
fi
exit "$push_status"
fi
else
echo "Applying versioned migrations (db:migrate)"
bun run ./scripts/migrate.ts
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
'use client'

import { useCallback, useMemo } from 'react'
import { generateId } from '@sim/utils/id'
import type { ComboboxOption } from '@/components/emcn'
import { Plus } from 'lucide-react'
import { Button } from '@/components/emcn'
import { useTableColumns } from '@/lib/table/hooks'
import type { FilterRule } from '@/lib/table/query-builder/constants'
import { useFilterBuilder } from '@/lib/table/query-builder/use-query-builder'
Expand All @@ -22,15 +22,6 @@ interface FilterBuilderProps {
tableIdSubBlockId?: string
}

const createDefaultRule = (columns: ComboboxOption[]): FilterRule => ({
id: generateId(),
logicalOperator: 'and',
column: columns[0]?.value || '',
operator: 'eq',
value: '',
collapsed: false,
})

/** Visual builder for table filter rules in workflow blocks. */
export function FilterBuilder({
blockId,
Expand All @@ -52,8 +43,7 @@ export function FilterBuilder({
}, [propColumns, dynamicColumns])

const value = isPreview ? previewValue : storeValue
const rules: FilterRule[] =
Array.isArray(value) && value.length > 0 ? value : [createDefaultRule(columns)]
const rules: FilterRule[] = Array.isArray(value) ? value : []
const isReadOnly = isPreview || disabled

const { comparisonOptions, logicalOptions, addRule, removeRule, updateRule } = useFilterBuilder({
Expand Down Expand Up @@ -86,15 +76,25 @@ export function FilterBuilder({
const handleRemoveRule = useCallback(
(id: string) => {
if (isReadOnly) return
if (rules.length === 1) {
setStoreValue([createDefaultRule(columns)])
} else {
removeRule(id)
}
removeRule(id)
},
[isReadOnly, rules, columns, setStoreValue, removeRule]
[isReadOnly, removeRule]
)

if (rules.length === 0) {
if (isReadOnly) return null
return (
<Button
variant='ghost'
onClick={addRule}
className='h-7 w-full justify-start gap-1.5 border border-[var(--border-1)] border-dashed text-[var(--text-muted)] text-small'
>
<Plus className='size-[14px]' />
Add filter condition
</Button>
)
}

return (
<div className='space-y-2'>
{rules.map((rule, index) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import { useCallback, useMemo } from 'react'
import { generateId } from '@sim/utils/id'
import type { ComboboxOption } from '@/components/emcn'
import { Plus } from 'lucide-react'
import { Button, type ComboboxOption } from '@/components/emcn'
import { useTableColumns } from '@/lib/table/hooks'
import { SORT_DIRECTIONS, type SortRule } from '@/lib/table/query-builder/constants'
import { useCanonicalSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-canonical-sub-block-value'
Expand Down Expand Up @@ -51,8 +52,7 @@ export function SortBuilder({
)

const value = isPreview ? previewValue : storeValue
const rules: SortRule[] =
Array.isArray(value) && value.length > 0 ? value : [createDefaultRule(columns)]
const rules: SortRule[] = Array.isArray(value) ? value : []
const isReadOnly = isPreview || disabled

const addRule = useCallback(() => {
Expand All @@ -63,13 +63,9 @@ export function SortBuilder({
const removeRule = useCallback(
(id: string) => {
if (isReadOnly) return
if (rules.length === 1) {
setStoreValue([createDefaultRule(columns)])
} else {
setStoreValue(rules.filter((r) => r.id !== id))
}
setStoreValue(rules.filter((r) => r.id !== id))
},
[isReadOnly, rules, columns, setStoreValue]
[isReadOnly, rules, setStoreValue]
)

const updateRule = useCallback(
Expand All @@ -88,6 +84,20 @@ export function SortBuilder({
[isReadOnly, rules, setStoreValue]
)

if (rules.length === 0) {
if (isReadOnly) return null
return (
<Button
variant='ghost'
onClick={addRule}
className='h-7 w-full justify-start gap-1.5 border border-[var(--border-1)] border-dashed text-[var(--text-muted)] text-small'
>
<Plus className='size-[14px]' />
Add sort
</Button>
)
}

return (
<div className='space-y-2'>
{rules.map((rule, index) => (
Expand Down
17 changes: 17 additions & 0 deletions apps/sim/blocks/blocks/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ interface TableBlockParams {
sortBuilder?: unknown
bulkFilterMode?: string
bulkFilterBuilder?: unknown
conflictColumn?: string
}

/** Normalized params after parsing, ready for tool request body */
Expand All @@ -70,6 +71,7 @@ interface ParsedParams {
sort?: unknown
limit?: number
offset?: number
conflictTarget?: string
}

/** Transforms raw block params into tool request params for each operation */
Expand All @@ -82,6 +84,7 @@ const paramTransformers: Record<string, (params: TableBlockParams) => ParsedPara
upsert_row: (params) => ({
tableId: params.tableId,
data: parseJSON(params.data, 'Row Data'),
conflictTarget: params.conflictColumn || undefined,
}),

batch_insert_rows: (params) => ({
Expand Down Expand Up @@ -275,6 +278,16 @@ Return ONLY the data JSON:`,
},
},

// Upsert - which unique column to match on (defaults to the first unique column)
{
id: 'conflictColumn',
title: 'Conflict Column',
type: 'short-input',
placeholder: 'Unique column to match on (defaults to the first unique column)',
dependsOn: ['tableId'],
condition: { field: 'operation', value: 'upsert_row' },
},

// Batch Insert - multiple rows
{
id: 'rows',
Expand Down Expand Up @@ -631,6 +644,10 @@ Return ONLY the sort JSON:`,
sortBuilder: { type: 'json', description: 'Visual sort builder conditions' },
sort: { type: 'json', description: 'Sort order (JSON)' },
offset: { type: 'number', description: 'Query result offset' },
conflictColumn: {
type: 'string',
description: 'Unique column to match on for upsert (defaults to the first unique column)',
},
},

outputs: {
Expand Down
4 changes: 4 additions & 0 deletions apps/sim/lib/table/query-builder/converters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export function filterRulesToFilter(rules: FilterRule[]): Filter | null {
let currentGroup: Filter = {}

for (const rule of rules) {
// Skip incomplete rows (no column selected) so a blank builder row never
// serializes to a `{ '': ... }` predicate.
if (!rule.column) continue
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

const isOr = rule.logicalOperator === 'or'
const ruleValue = toRuleValue(rule.operator, rule.value)

Expand Down
7 changes: 2 additions & 5 deletions apps/sim/lib/table/rows/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,12 +492,9 @@ export async function upsertRow(
)
}
targetColumnKey = getColumnId(col)
} else if (uniqueColumns.length === 1) {
targetColumnKey = getColumnId(uniqueColumns[0])
} else {
throw new Error(
`Table has multiple unique columns (${uniqueColumns.map((c) => c.name).join(', ')}). Specify conflictTarget to indicate which column to match on.`
)
// No conflict target specified — default to the first unique column.
targetColumnKey = getColumnId(uniqueColumns[0])
}
Comment thread
TheodoreSpeaks marked this conversation as resolved.
Outdated

// Validate row data
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/tools/table/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export interface TableListParams {
export interface TableRowInsertParams {
tableId: string
data: RowData
/** Unique column to match on for upsert; ignored by plain insert */
conflictTarget?: string
_context?: WorkflowToolExecutionContext
}

Expand Down
8 changes: 8 additions & 0 deletions apps/sim/tools/table/upsert_row.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ export const tableUpsertRowTool: ToolConfig<TableRowInsertParams, TableUpsertRes
description: 'Row data to insert or update',
visibility: 'user-or-llm',
},
conflictTarget: {
type: 'string',
required: false,
description:
'Unique column to match on. Required only when the table has more than one unique column.',
visibility: 'user-only',
},
},

request: {
Expand All @@ -45,6 +52,7 @@ export const tableUpsertRowTool: ToolConfig<TableRowInsertParams, TableUpsertRes
return {
data: params.data,
workspaceId,
...(params.conflictTarget ? { conflictTarget: params.conflictTarget } : {}),
}
},
},
Expand Down
Loading