Skip to content

Commit 91f1689

Browse files
ImTotemclaude
andcommitted
refactor(workflow): remove loop, dump all tables in one query
- Remove splitInBatches loop (broken in n8n 2.11) - Single UNION ALL query dumps all changed tables at once - Single batchUpdate pastes all tables in one API call - Remove Has Data? node (redundant with Has Changes?) Flow: List → Filter Changed → Has Changes? → Get Sheet IDs → Dump All (1 PG query) → Build Payload → Paste (1 API call) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5bee70e commit 91f1689

1 file changed

Lines changed: 24 additions & 60 deletions

File tree

workflows/pg_sheets_sync.json

Lines changed: 24 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -22,32 +22,32 @@
2222
"options": {}
2323
},
2424
"id": "pg-list-tables",
25-
"name": "List Tables + Mod Counts",
25+
"name": "List Tables",
2626
"type": "n8n-nodes-base.postgres",
2727
"typeVersion": 2.6,
2828
"position": [220, 0]
2929
},
3030
{
3131
"parameters": {
32-
"jsCode": "const staticData = $getWorkflowStaticData('global');\nif (!staticData.mods) staticData.mods = {};\n\nconst changed = [];\nfor (const item of $input.all()) {\n const name = item.json.table_name;\n const mods = item.json.mod_count;\n if (staticData.mods[name] !== mods) {\n staticData.mods[name] = mods;\n changed.push({ json: { table_name: name } });\n }\n}\n\nif (changed.length === 0) return [{ json: { _skip: true } }];\nreturn changed;"
32+
"jsCode": "const staticData = $getWorkflowStaticData('global');\nif (!staticData.mods) staticData.mods = {};\n\nconst changed = [];\nfor (const item of $input.all()) {\n const name = item.json.table_name;\n const mods = item.json.mod_count;\n if (staticData.mods[name] !== mods) {\n staticData.mods[name] = mods;\n changed.push(name);\n }\n}\n\nif (changed.length === 0) return [{ json: { _skip: true } }];\nreturn [{ json: { _skip: false, tables: changed } }];"
3333
},
3434
"id": "filter-changed",
35-
"name": "Filter Changed Tables",
35+
"name": "Filter Changed",
3636
"type": "n8n-nodes-base.code",
3737
"typeVersion": 2,
3838
"position": [440, 0]
3939
},
4040
{
4141
"parameters": {
4242
"conditions": {
43-
"options": {"caseSensitive": true, "leftValue": "", "typeValidation": "strict"},
44-
"conditions": [{"id": "c1", "leftValue": "={{ $json._skip }}", "rightValue": true, "operator": {"type": "boolean", "operation": "notEquals"}}],
43+
"options": {"caseSensitive": true, "leftValue": "", "typeValidation": "loose"},
44+
"conditions": [{"id": "c1", "leftValue": "={{ $json._skip }}", "rightValue": false, "operator": {"type": "boolean", "operation": "equals"}}],
4545
"combinator": "and"
4646
},
4747
"options": {}
4848
},
4949
"id": "if-changed",
50-
"name": "Any Changes?",
50+
"name": "Has Changes?",
5151
"type": "n8n-nodes-base.if",
5252
"typeVersion": 2.3,
5353
"position": [660, 0]
@@ -66,60 +66,27 @@
6666
"typeVersion": 4.2,
6767
"position": [880, -100]
6868
},
69-
{
70-
"parameters": {
71-
"jsCode": "const sheetsResponse = $('Get Sheet IDs').first().json;\nconst sheetMap = {};\nfor (const s of sheetsResponse.sheets) {\n sheetMap[s.properties.title] = s.properties.sheetId;\n}\n\nconst tables = $('Any Changes?').all().map(i => i.json.table_name);\nconst result = tables.map(t => ({\n json: { table_name: t, sheet_id: sheetMap[t] ?? null }\n}));\nreturn result;"
72-
},
73-
"id": "map-sheet-ids",
74-
"name": "Map Sheet IDs",
75-
"type": "n8n-nodes-base.code",
76-
"typeVersion": 2,
77-
"position": [1100, -100]
78-
},
79-
{
80-
"parameters": {"options": {}},
81-
"id": "split-batches",
82-
"name": "Loop Over Tables",
83-
"type": "n8n-nodes-base.splitInBatches",
84-
"typeVersion": 3,
85-
"position": [1320, -100]
86-
},
8769
{
8870
"parameters": {
8971
"operation": "executeQuery",
90-
"query": "=SELECT * FROM {{ $json.table_name }}",
72+
"query": "={{ $('Filter Changed').first().json.tables.map(t => \"SELECT '\" + t + \"' as _tbl, to_jsonb(sub) as _row FROM \\\"\" + t + \"\\\" sub\").join(' UNION ALL ') }}",
9173
"options": {}
9274
},
93-
"id": "pg-dump-table",
94-
"name": "Dump Table",
75+
"id": "pg-dump-all",
76+
"name": "Dump All Changed",
9577
"type": "n8n-nodes-base.postgres",
9678
"typeVersion": 2.6,
97-
"position": [1540, -100]
79+
"position": [1100, -100]
9880
},
9981
{
10082
"parameters": {
101-
"jsCode": "const rows = $input.all().map(i => i.json);\nconst tableName = $('Loop Over Tables').item.json.table_name;\nlet sheetId = $('Loop Over Tables').item.json.sheet_id;\n\nif (rows.length === 0) {\n return [{ json: { _empty: true } }];\n}\n\nfunction hashCode(s) {\n let h = 0;\n for (let i = 0; i < s.length; i++) h = ((h << 5) - h) + s.charCodeAt(i) | 0;\n return Math.abs(h);\n}\n\nconst headers = Object.keys(rows[0]);\nconst csvLines = [headers.join(',')];\nfor (const row of rows) {\n csvLines.push(headers.map(h => {\n const v = String(row[h] ?? '');\n return (v.includes(',') || v.includes('\"') || v.includes('\\n')) ? '\"' + v.replace(/\"/g, '\"\"') + '\"' : v;\n }).join(','));\n}\n\nconst requests = [];\n\nif (sheetId == null) {\n sheetId = hashCode(tableName);\n requests.push({ addSheet: { properties: { sheetId, title: tableName } } });\n}\nrequests.push({ updateCells: { range: { sheetId }, fields: 'userEnteredValue' } });\nrequests.push({ pasteData: { coordinate: { sheetId, rowIndex: 0, columnIndex: 0 }, data: csvLines.join('\\n'), type: 'PASTE_NORMAL', delimiter: ',' } });\n\nreturn [{ json: { _empty: false, body: { requests } } }];"
83+
"jsCode": "const sheetsData = $('Get Sheet IDs').first().json;\nconst dumpRows = $('Dump All Changed').all().map(i => i.json);\n\nconst sheetMap = {};\nfor (const s of sheetsData.sheets) {\n sheetMap[s.properties.title] = s.properties.sheetId;\n}\n\nfunction hashCode(s) {\n let h = 0;\n for (let i = 0; i < s.length; i++) h = ((h << 5) - h) + s.charCodeAt(i) | 0;\n return Math.abs(h);\n}\n\nconst grouped = {};\nfor (const r of dumpRows) {\n if (!grouped[r._tbl]) grouped[r._tbl] = [];\n grouped[r._tbl].push(r._row);\n}\n\nconst requests = [];\n\nfor (const [tableName, rows] of Object.entries(grouped)) {\n if (rows.length === 0) continue;\n\n let sheetId = sheetMap[tableName] ?? null;\n if (sheetId == null) {\n sheetId = hashCode(tableName);\n requests.push({ addSheet: { properties: { sheetId, title: tableName } } });\n }\n\n requests.push({ updateCells: { range: { sheetId }, fields: 'userEnteredValue' } });\n\n const headers = Object.keys(rows[0]);\n const csvLines = [headers.join(',')];\n for (const row of rows) {\n csvLines.push(headers.map(h => {\n const v = String(row[h] ?? '');\n return (v.includes(',') || v.includes('\"') || v.includes('\\n')) ? '\"' + v.replace(/\"/g, '\"\"') + '\"' : v;\n }).join(','));\n }\n\n requests.push({ pasteData: { coordinate: { sheetId, rowIndex: 0, columnIndex: 0 }, data: csvLines.join('\\n'), type: 'PASTE_NORMAL', delimiter: ',' } });\n}\n\nreturn [{ json: { body: { requests } } }];"
10284
},
103-
"id": "to-csv",
104-
"name": "Build CSV + Payload",
85+
"id": "build-payload",
86+
"name": "Build Batch Payload",
10587
"type": "n8n-nodes-base.code",
10688
"typeVersion": 2,
107-
"position": [1760, -100]
108-
},
109-
{
110-
"parameters": {
111-
"conditions": {
112-
"options": {"caseSensitive": true, "leftValue": "", "typeValidation": "strict"},
113-
"conditions": [{"id": "c1", "leftValue": "={{ $json._empty }}", "rightValue": true, "operator": {"type": "boolean", "operation": "notEquals"}}],
114-
"combinator": "and"
115-
},
116-
"options": {}
117-
},
118-
"id": "if-not-empty",
119-
"name": "Has Rows?",
120-
"type": "n8n-nodes-base.if",
121-
"typeVersion": 2.3,
122-
"position": [1980, -100]
89+
"position": [1320, -100]
12390
},
12491
{
12592
"parameters": {
@@ -133,24 +100,21 @@
133100
"options": {}
134101
},
135102
"id": "paste-data",
136-
"name": "Paste to Sheet",
103+
"name": "Paste to Sheets",
137104
"type": "n8n-nodes-base.httpRequest",
138105
"typeVersion": 4.2,
139-
"position": [2200, -200]
106+
"position": [1540, -100]
140107
}
141108
],
142109
"connections": {
143-
"Every 5 Minutes": {"main": [[{"node": "List Tables + Mod Counts", "type": "main", "index": 0}]]},
144-
"List Tables + Mod Counts": {"main": [[{"node": "Filter Changed Tables", "type": "main", "index": 0}]]},
145-
"Filter Changed Tables": {"main": [[{"node": "Any Changes?", "type": "main", "index": 0}]]},
146-
"Any Changes?": {"main": [[{"node": "Get Sheet IDs", "type": "main", "index": 0}], []]},
147-
"Get Sheet IDs": {"main": [[{"node": "Map Sheet IDs", "type": "main", "index": 0}]]},
148-
"Map Sheet IDs": {"main": [[{"node": "Loop Over Tables", "type": "main", "index": 0}]]},
149-
"Loop Over Tables": {"main": [[], [{"node": "Dump Table", "type": "main", "index": 0}]]},
150-
"Dump Table": {"main": [[{"node": "Build CSV + Payload", "type": "main", "index": 0}]]},
151-
"Build CSV + Payload": {"main": [[{"node": "Has Rows?", "type": "main", "index": 0}]]},
152-
"Has Rows?": {"main": [[{"node": "Paste to Sheet", "type": "main", "index": 0}], [{"node": "Loop Over Tables", "type": "main", "index": 0}]]},
153-
"Paste to Sheet": {"main": [[{"node": "Loop Over Tables", "type": "main", "index": 0}]]}
110+
"Every 5 Minutes": {"main": [[{"node": "List Tables", "type": "main", "index": 0}]]},
111+
"List Tables": {"main": [[{"node": "Filter Changed", "type": "main", "index": 0}]]},
112+
"Filter Changed": {"main": [[{"node": "Has Changes?", "type": "main", "index": 0}]]},
113+
"Has Changes?": {"main": [[{"node": "Get Sheet IDs", "type": "main", "index": 0}], []]},
114+
"Get Sheet IDs": {"main": [[{"node": "Dump All Changed", "type": "main", "index": 0}]]},
115+
"Dump All Changed": {"main": [[{"node": "Build Batch Payload", "type": "main", "index": 0}]]},
116+
"Build Batch Payload": {"main": [[{"node": "Paste to Sheets", "type": "main", "index": 0}]]},
117+
"Paste to Sheets": {"main": [[]]}
154118
},
155119
"settings": {"executionOrder": "v1"}
156120
}

0 commit comments

Comments
 (0)