diff --git a/lib/sql_generator.js b/lib/sql_generator.js index 2b9040b..bdb79db 100644 --- a/lib/sql_generator.js +++ b/lib/sql_generator.js @@ -26,11 +26,10 @@ function generateSQL(source, colMeta) { if (dims.length === 0) throw new Error('No dimension columns defined in col_meta'); const srcTable = `${source.schema}.${source.tname}`; - // exclude 'id' — forecast table has its own bigserial id primary key - const dataCols = [...dims, dateCol, valueCol, unitsCol].filter(c => c !== 'id'); + const dataCols = [...dims, dateCol, valueCol, unitsCol].filter(Boolean); const effectiveValue = dataCols.includes(valueCol) ? valueCol : null; const effectiveUnits = dataCols.includes(unitsCol) ? unitsCol : null; - const insertCols = [...dataCols.map(q), 'iter', 'logid', 'pf_user', 'created_at'].join(', '); + const insertCols = [...dataCols.map(q), 'pf_iter', 'pf_logid', 'pf_user', 'pf_created_at'].join(', '); const selectData = dataCols.map(q).join(', '); const dimsJoined = dims.map(q).join(', '); @@ -189,7 +188,7 @@ SELECT * FROM ins`.trim(); // This entry is a placeholder — the undo route uses it as a template reference. return ` -- step 1 (run first): -DELETE FROM {{fc_table}} WHERE logid = {{logid}}; +DELETE FROM {{fc_table}} WHERE pf_logid = {{logid}}; -- step 2 (run after step 1): DELETE FROM pf.log WHERE id = {{logid}};`.trim(); } @@ -231,7 +230,7 @@ function buildWhere(slice, dimCols) { function buildExcludeClause(excludeIters) { if (!excludeIters || excludeIters.length === 0) return ''; const list = excludeIters.map(i => `'${esc(i)}'`).join(', '); - return `AND iter NOT IN (${list})`; + return `AND pf_iter NOT IN (${list})`; } // build the dimension columns portion of a SELECT for recode/clone @@ -253,7 +252,7 @@ function buildFilterClause(filters, colMeta) { err.status = 400; throw err; } const allowed = new Set( - colMeta.filter(c => c.role === 'date' || c.role === 'filter').map(c => c.cname) + colMeta.filter(c => c.role !== 'ignore').map(c => c.cname) ); const parts = filters.map(({ col, op, values = [] }) => { if (!allowed.has(col)) { diff --git a/public/app.js b/public/app.js index 497c0c9..3852c7a 100644 --- a/public/app.js +++ b/public/app.js @@ -9,7 +9,6 @@ const state = { colMeta: [], // col_meta for selected source slice: {}, // current pivot selection loadDataOp: null, // 'baseline' | 'reference' - baselineFilterCols: [], previewSchema: null, previewTname: null, grids: { @@ -117,15 +116,20 @@ function renderTablesGrid(tables) { state.grids.tables = agGrid.createGrid(el, { columnDefs: [ - { field: 'schema', headerName: 'Schema', width: 90 }, + { field: 'schema', headerName: 'Schema', flex: 1 }, { field: 'tname', headerName: 'Table', flex: 1 }, - { field: 'row_estimate', headerName: 'Rows', width: 80, + { field: 'row_estimate', headerName: 'Rows', flex: 1, valueFormatter: p => p.value != null ? Number(p.value).toLocaleString() : '—' } ], rowData: tables, rowSelection: 'single', onRowClicked: onTableRowClicked, onRowDoubleClicked: e => showTablePreview(e.data.schema, e.data.tname), + allowContextMenuWithControlKey: true, + getContextMenuItems: e => [ + { name: 'Preview', action: () => showTablePreview(e.node.data.schema, e.node.data.tname) }, + { name: 'Register', action: () => registerTable(e.node.data.schema, e.node.data.tname) } + ], defaultColDef: { resizable: true, sortable: true }, headerHeight: 32, rowHeight: 28 }); @@ -135,6 +139,7 @@ function onTableRowClicked(e) { const { schema, tname } = e.data; state.previewSchema = schema; state.previewTname = tname; + document.getElementById('btn-preview').classList.remove('hidden'); document.getElementById('btn-register').classList.remove('hidden'); } @@ -213,13 +218,11 @@ function renderSourcesGrid(sources) { async function selectSource(source) { setSource(source); + document.getElementById('btn-delete-source').classList.remove('hidden'); try { state.colMeta = await api('GET', `/sources/${source.id}/cols`); renderColMetaGrid(state.colMeta); - document.getElementById('sources-list-grid').classList.add('hidden'); - document.getElementById('col-meta-grid').classList.remove('hidden'); document.getElementById('right-panel-title').textContent = `${source.schema}.${source.tname} — Columns`; - document.getElementById('btn-back-sources').classList.remove('hidden'); document.getElementById('btn-save-cols').classList.remove('hidden'); document.getElementById('btn-generate-sql').classList.remove('hidden'); } catch (err) { @@ -227,13 +230,24 @@ async function selectSource(source) { } } -function backToSources() { - document.getElementById('sources-list-grid').classList.remove('hidden'); - document.getElementById('col-meta-grid').classList.add('hidden'); - document.getElementById('right-panel-title').textContent = 'Registered Sources'; - document.getElementById('btn-back-sources').classList.add('hidden'); - document.getElementById('btn-save-cols').classList.add('hidden'); - document.getElementById('btn-generate-sql').classList.add('hidden'); +async function deleteSource() { + if (!state.source) return; + const { id, schema, tname } = state.source; + if (!confirm(`Delete source ${schema}.${tname}? This does not drop existing forecast tables.`)) return; + try { + await api('DELETE', `/sources/${id}`); + showStatus(`Source ${tname} deleted`, 'success'); + setSource(null); + document.getElementById('btn-delete-source').classList.add('hidden'); + document.getElementById('btn-save-cols').classList.add('hidden'); + document.getElementById('btn-generate-sql').classList.add('hidden'); + document.getElementById('right-panel-title').textContent = 'Select a source to map columns'; + state.grids.colMeta?.setGridOption('rowData', []); + const sources = await api('GET', '/sources'); + state.grids.sources?.setGridOption('rowData', sources); + } catch (err) { + showStatus(err.message, 'error'); + } } function renderColMetaGrid(colMeta) { @@ -247,9 +261,9 @@ function renderColMetaGrid(colMeta) { { field: 'role', headerName: 'Role', width: 110, editable: true, cellEditor: 'agSelectCellEditor', - cellEditorParams: { values: ['dimension', 'value', 'units', 'date', 'filter', 'ignore'] }, + cellEditorParams: { values: ['dimension', 'value', 'units', 'date', 'ignore'] }, cellStyle: p => { - const colors = { dimension: '#eaf4fb', value: '#eafaf1', units: '#fef9e7', date: '#fdf2f8', filter: '#fef5e4', ignore: '#f9f9f9' }; + const colors = { dimension: '#eaf4fb', value: '#eafaf1', units: '#fef9e7', date: '#fdf2f8', ignore: '#f9f9f9' }; return { background: colors[p.value] || '' }; } }, @@ -553,19 +567,36 @@ async function openBaselineWorkbench() { if (!state.colMeta.length && state.source) { state.colMeta = await api('GET', `/sources/${state.source.id}/cols`); } - state.baselineFilterCols = state.colMeta.filter(c => c.role === 'date' || c.role === 'filter'); + + // populate column chips + const chips = document.getElementById('seg-col-chips'); + chips.innerHTML = state.colMeta + .filter(c => c.role !== 'ignore') + .map(c => `${c.cname}`) + .join(''); + chips.querySelectorAll('.col-chip').forEach(chip => { + chip.addEventListener('click', () => insertAtCursor(document.getElementById('seg-where'), chip.dataset.col)); + }); // reset form - document.getElementById('seg-description').value = ''; + document.getElementById('seg-description').value = ''; document.getElementById('seg-offset-type').value = ''; document.getElementById('seg-offset-value').value = '0'; - document.getElementById('seg-filter-rows').innerHTML = ''; + document.getElementById('seg-where').value = ''; document.getElementById('seg-timeline').classList.add('hidden'); - addFilterRow(); // start with one empty filter row await loadBaselineSegments(); } +function insertAtCursor(textarea, text) { + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const val = textarea.value; + textarea.value = val.slice(0, start) + text + val.slice(end); + textarea.selectionStart = textarea.selectionEnd = start + text.length; + textarea.focus(); +} + async function loadBaselineSegments() { if (!state.version) return; try { @@ -584,16 +615,11 @@ function renderBaselineSegments(segments) { return; } el.innerHTML = segments.map(s => { - const params = s.params || {}; - const filters = (params.filters || []) - .map(f => { - if (f.op === 'IS NULL' || f.op === 'IS NOT NULL') return `${f.col} ${f.op}`; - if (f.op === 'BETWEEN') return `${f.col} BETWEEN ${(f.values||[]).join(' AND ')}`; - if (f.op === 'IN' || f.op === 'NOT IN') return `${f.col} ${f.op} (${(f.values||[]).join(', ')})`; - return `${f.col} ${f.op} ${(f.values||[])[0] || ''}`; - }).join('\n'); - const offset = params.date_offset && params.date_offset !== '0 days' ? ` · offset: ${params.date_offset}` : ''; - const stamp = s.stamp ? new Date(s.stamp).toLocaleString() : ''; + const params = s.params || {}; + const whereClause = params.where_clause && params.where_clause !== 'TRUE' ? params.where_clause : ''; + const offset = params.date_offset && params.date_offset !== '0 days' ? `offset: ${params.date_offset}` : ''; + const paramsText = [whereClause, offset].filter(Boolean).join(' · '); + const stamp = s.stamp ? new Date(s.stamp).toLocaleString() : ''; return `
@@ -601,187 +627,25 @@ function renderBaselineSegments(segments) { ${s.pf_user} — ${stamp}
-
${filters}${offset}
+ ${paramsText ? `
${paramsText}
` : ''}
`; }).join(''); } -function addFilterRow() { - const container = document.getElementById('seg-filter-rows'); - const idx = container.children.length; - const colOptions = state.baselineFilterCols - .map(c => ``) - .join(''); - - const div = document.createElement('div'); - div.className = 'filter-row'; - div.dataset.index = idx; - div.innerHTML = ` - - -
- - `; - - div.querySelector('.filter-op-select').addEventListener('change', () => { - updateFilterValueInputs(div); - updateTimelinePreview(); - }); - div.querySelector('.filter-col-select').addEventListener('change', () => { - updateTimelinePreview(); - }); - div.querySelector('.filter-remove-btn').addEventListener('click', () => { - div.remove(); - updateTimelinePreview(); - }); - - updateFilterValueInputs(div); - container.appendChild(div); -} - -function updateFilterValueInputs(row) { - const op = row.querySelector('.filter-op-select').value; - const container = row.querySelector('.filter-value-container'); - - if (op === 'IS NULL' || op === 'IS NOT NULL') { - container.innerHTML = ''; - return; - } - if (op === 'BETWEEN') { - container.innerHTML = ` - - and - - `; - container.querySelectorAll('input').forEach(i => i.addEventListener('input', updateTimelinePreview)); - return; - } - if (op === 'IN' || op === 'NOT IN') { - container.innerHTML = ``; - return; - } - // = or != - container.innerHTML = ``; -} - -function getFilterRows() { - const rows = []; - document.querySelectorAll('#seg-filter-rows .filter-row').forEach(row => { - const col = row.querySelector('.filter-col-select').value; - const op = row.querySelector('.filter-op-select').value; - if (!col) return; - - const filter = { col, op, values: [] }; - - if (op === 'BETWEEN') { - const from = row.querySelector('.filter-val-from')?.value.trim(); - const to = row.querySelector('.filter-val-to')?.value.trim(); - if (from) filter.values.push(from); - if (to) filter.values.push(to); - } else if (op === 'IN' || op === 'NOT IN') { - const raw = row.querySelector('.filter-val-list')?.value.trim(); - if (raw) filter.values = raw.split(',').map(s => s.trim()).filter(Boolean); - } else if (op !== 'IS NULL' && op !== 'IS NOT NULL') { - const val = row.querySelector('.filter-val-single')?.value.trim(); - if (val) filter.values.push(val); - } - - rows.push(filter); - }); - return rows; -} function updateTimelinePreview() { - const el = document.getElementById('seg-timeline'); - - // find first BETWEEN filter on a date-role column - let dateFrom = null, dateTo = null; - document.querySelectorAll('#seg-filter-rows .filter-row').forEach(row => { - if (dateFrom) return; - const col = row.querySelector('.filter-col-select').value; - const op = row.querySelector('.filter-op-select').value; - if (op !== 'BETWEEN') return; - const meta = state.baselineFilterCols.find(c => c.cname === col); - if (!meta || meta.role !== 'date') return; - const from = row.querySelector('.filter-val-from')?.value.trim(); - const to = row.querySelector('.filter-val-to')?.value.trim(); - if (from && to) { dateFrom = from; dateTo = to; } - }); - - if (!dateFrom || !dateTo) { el.classList.add('hidden'); return; } - - const from = new Date(dateFrom + 'T00:00:00'); - const to = new Date(dateTo + 'T00:00:00'); - if (isNaN(from) || isNaN(to) || from > to) { el.classList.add('hidden'); return; } - - const months = (to.getFullYear() - from.getFullYear()) * 12 + (to.getMonth() - from.getMonth()) + 1; - const fmt = new Intl.DateTimeFormat('en-US', { month: 'short', year: 'numeric' }); + const el = document.getElementById('seg-timeline'); const offsetType = document.getElementById('seg-offset-type').value; const offsetValue = parseInt(document.getElementById('seg-offset-value').value) || 0; - function applyOffset(d) { - const r = new Date(d); - if (!offsetType || !offsetValue) return r; - switch (offsetType) { - case 'year': r.setFullYear(r.getFullYear() + offsetValue); break; - case 'month': r.setMonth(r.getMonth() + offsetValue); break; - case 'week': r.setDate(r.getDate() + offsetValue * 7); break; - case 'day': r.setDate(r.getDate() + offsetValue); break; - } - return r; - } + if (!offsetType || !offsetValue) { el.classList.add('hidden'); return; } - const projFrom = applyOffset(from); - const projTo = applyOffset(to); - - let html = ` -
-
Source
-
-
-
- ${fmt.format(from)} - ${months} month${months !== 1 ? 's' : ''} - ${fmt.format(to)} -
-
-
`; - - if (offsetType && offsetValue) { - html += ` -
+ ${offsetValue} ${offsetType}${offsetValue !== 1 ? 's' : ''} →
-
-
Projected
-
-
-
- ${fmt.format(projFrom)} - ${months} month${months !== 1 ? 's' : ''} - ${fmt.format(projTo)} -
-
-
`; - } - - el.innerHTML = html; + el.innerHTML = `
+ ${offsetValue} ${offsetType}${offsetValue !== 1 ? 's' : ''} applied to date column
`; el.classList.remove('hidden'); } async function submitBaselineSegment() { if (!state.version) return; - const filters = getFilterRows(); - if (filters.length === 0) { showStatus('Add at least one filter', 'error'); return; } const offsetType = document.getElementById('seg-offset-type').value; const offsetValue = parseInt(document.getElementById('seg-offset-value').value) || 0; @@ -790,7 +654,7 @@ async function submitBaselineSegment() { : '0 days'; const body = { - filters, + where_clause: document.getElementById('seg-where').value.trim() || undefined, date_offset, pf_user: getPfUser(), note: document.getElementById('seg-description').value.trim() || undefined @@ -800,10 +664,11 @@ async function submitBaselineSegment() { showStatus('Loading segment...', 'info'); const result = await api('POST', `/versions/${state.version.id}/baseline`, body); showStatus(`Segment loaded — ${result.rows_affected} rows`, 'success'); - document.getElementById('seg-description').value = ''; - document.getElementById('seg-filter-rows').innerHTML = ''; + document.getElementById('seg-description').value = ''; + document.getElementById('seg-where').value = ''; + document.getElementById('seg-offset-type').value = ''; + document.getElementById('seg-offset-value').value = '0'; document.getElementById('seg-timeline').classList.add('hidden'); - addFilterRow(); await loadBaselineSegments(); } catch (err) { showStatus(err.message, 'error'); @@ -901,10 +766,10 @@ function buildPivotColDefs() { } }); - // always include iter for grouping context - defs.push({ field: 'iter', headerName: 'Iter', enableRowGroup: true, enablePivot: true, width: 90 }); - defs.push({ field: 'pf_user', headerName: 'User', width: 90, hide: true }); - defs.push({ field: 'created_at', headerName: 'Created', width: 130, hide: true, + // always include pf_iter for grouping context + defs.push({ field: 'pf_iter', headerName: 'Iter', enableRowGroup: true, enablePivot: true, width: 90 }); + defs.push({ field: 'pf_user', headerName: 'User', width: 90, hide: true }); + defs.push({ field: 'pf_created_at', headerName: 'Created', width: 130, hide: true, valueFormatter: p => p.value ? new Date(p.value).toLocaleDateString() : '' }); return defs; @@ -1221,12 +1086,17 @@ document.addEventListener('DOMContentLoaded', () => { }); // sources view buttons + document.getElementById('btn-preview').addEventListener('click', () => { + if (state.previewSchema && state.previewTname) { + showTablePreview(state.previewSchema, state.previewTname); + } + }); document.getElementById('btn-register').addEventListener('click', () => { if (state.previewSchema && state.previewTname) { registerTable(state.previewSchema, state.previewTname); } }); - document.getElementById('btn-back-sources').addEventListener('click', backToSources); + document.getElementById('btn-delete-source').addEventListener('click', deleteSource); document.getElementById('btn-save-cols').addEventListener('click', saveColMeta); document.getElementById('btn-generate-sql').addEventListener('click', generateSQL); @@ -1283,7 +1153,6 @@ document.addEventListener('DOMContentLoaded', () => { }); // baseline workbench - document.getElementById('btn-add-filter-row').addEventListener('click', addFilterRow); document.getElementById('btn-load-segment').addEventListener('click', submitBaselineSegment); document.getElementById('btn-clear-baseline').addEventListener('click', clearBaseline); document.getElementById('seg-offset-type').addEventListener('change', updateTimelinePreview); @@ -1299,6 +1168,16 @@ document.addEventListener('DOMContentLoaded', () => { await loadBaselineSegments(); }); + // tables search + document.getElementById('tables-search').addEventListener('input', e => { + state.grids.tables?.setGridOption('quickFilterText', e.target.value); + }); + + // columns search + document.getElementById('cols-search').addEventListener('input', e => { + state.grids.colMeta?.setGridOption('quickFilterText', e.target.value); + }); + // init sources view initSourcesView().catch(err => showStatus(err.message, 'error')); }); diff --git a/public/index.html b/public/index.html index ee5f0e9..763833e 100644 --- a/public/index.html +++ b/public/index.html @@ -44,27 +44,43 @@
-
-
-
- Database Tables -
- +
+ +
+
+
+ Database Tables +
+ + +
+
+ +
+
+
+
+
+ Registered Sources + +
+
-
-
+ +
- Registered Sources + Select a source to map columns
- - - + +
-
- +
+ +
+
@@ -124,10 +140,10 @@
-
- -
- +
+ +
+
diff --git a/public/styles.css b/public/styles.css index 00fb86a..0e7b577 100644 --- a/public/styles.css +++ b/public/styles.css @@ -83,9 +83,18 @@ body { .view.hidden { display: none !important; } /* ============================================================ - SOURCES VIEW — two-column + SOURCES VIEW ============================================================ */ -.two-col-layout { display: flex; gap: 10px; flex: 1; overflow: hidden; } +.two-col-layout { display: flex; gap: 10px; flex: 1; overflow: hidden; } +.sources-layout { display: flex; gap: 10px; flex: 1; overflow: hidden; } +.sources-left-col { width: 420px; flex-shrink: 0; display: flex; flex-direction: column; gap: 10px; overflow: hidden; } +.sources-tables-panel { flex-shrink: 0; } +.sources-list-panel { flex: 1; min-height: 0; } +.sources-mapping-panel { flex: 1; min-width: 0; } +.tables-search-wrap { padding: 6px 8px; border-bottom: 1px solid #e8ecf0; flex-shrink: 0; } +.tables-search-wrap input { width: 100%; padding: 4px 7px; border: 1px solid #d0d7de; border-radius: 4px; font-size: 12px; outline: none; } +.tables-search-wrap input:focus { border-color: #3498db; } +.tables-grid { height: 172px; flex-shrink: 0; } .panel { flex: 1; background: white; @@ -491,13 +500,34 @@ body { .offset-row { display: flex; gap: 10px; } .offset-row label { flex: 1; } -.filter-section { display: flex; flex-direction: column; gap: 6px; } -.filter-section-label { +/* WHERE clause builder */ +.where-section { display: flex; flex-direction: column; gap: 6px; } +.filter-section-label { font-size: 11px; color: #555; font-weight: 600; } +.col-chips { display: flex; flex-wrap: wrap; gap: 4px; } +.col-chip { + display: inline-block; + padding: 2px 8px; + background: #eaf4fb; + border: 1px solid #aed6f1; + border-radius: 10px; font-size: 11px; - color: #555; - font-weight: 600; + color: #2471a3; + cursor: pointer; + user-select: none; } -.required-star { color: #e74c3c; } +.col-chip:hover { background: #d6eaf8; border-color: #2471a3; } +.where-textarea { + width: 100%; + font-family: 'SFMono-Regular', Consolas, monospace; + font-size: 12px; + padding: 8px; + border: 1px solid #d0d7de; + border-radius: 4px; + resize: vertical; + color: #333; + line-height: 1.5; +} +.where-textarea:focus { outline: none; border-color: #3498db; } .filter-row { display: flex; diff --git a/routes/log.js b/routes/log.js index 3639b60..3102f1d 100644 --- a/routes/log.js +++ b/routes/log.js @@ -43,7 +43,7 @@ module.exports = function(pool) { await client.query('BEGIN'); // delete forecast rows first (logid has no FK constraint — managed by app) const del = await client.query( - `DELETE FROM ${table} WHERE logid = $1 RETURNING id`, + `DELETE FROM ${table} WHERE pf_logid = $1 RETURNING pf_id`, [logid] ); await client.query(`DELETE FROM pf.log WHERE id = $1`, [logid]); diff --git a/routes/operations.js b/routes/operations.js index 5ddc15d..beddf19 100644 --- a/routes/operations.js +++ b/routes/operations.js @@ -1,5 +1,5 @@ const express = require('express'); -const { applyTokens, buildWhere, buildExcludeClause, buildSetClause, buildFilterClause, esc } = require('../lib/sql_generator'); +const { applyTokens, buildWhere, buildExcludeClause, buildSetClause, esc } = require('../lib/sql_generator'); const { fcTable } = require('../lib/utils'); module.exports = function(pool) { @@ -76,26 +76,19 @@ module.exports = function(pool) { // load baseline rows from source table — additive, no delete router.post('/versions/:id/baseline', async (req, res) => { - const { filters, date_offset, pf_user, note, replay } = req.body; - if (!filters || filters.length === 0) { - return res.status(400).json({ error: 'filters are required' }); - } - if (replay) { - return res.status(501).json({ error: 'replay is not yet implemented' }); - } - const dateOffset = date_offset || '0 days'; + const { where_clause, date_offset, pf_user, note } = req.body; + const dateOffset = date_offset || '0 days'; + const filterClause = (where_clause || '').trim() || 'TRUE'; try { const ctx = await getContext(parseInt(req.params.id), 'baseline'); if (!guardOpen(ctx.version, res)) return; - const filterClause = buildFilterClause(filters, ctx.colMeta); - const sql = applyTokens(ctx.sql, { fc_table: ctx.table, version_id: ctx.version.id, pf_user: esc(pf_user || ''), note: esc(note || ''), - params: esc(JSON.stringify({ filters, date_offset: dateOffset })), + params: esc(JSON.stringify({ where_clause: filterClause, date_offset: dateOffset })), filter_clause: filterClause, date_offset: esc(dateOffset) }); @@ -118,7 +111,7 @@ module.exports = function(pool) { try { await client.query('BEGIN'); const delRows = await client.query( - `DELETE FROM ${ctx.table} WHERE iter = 'baseline' RETURNING id` + `DELETE FROM ${ctx.table} WHERE pf_iter = 'baseline' RETURNING pf_id` ); const delLog = await client.query( `DELETE FROM pf.log WHERE version_id = $1 AND operation = 'baseline'`, diff --git a/routes/versions.js b/routes/versions.js index 746523e..ad02ba9 100644 --- a/routes/versions.js +++ b/routes/versions.js @@ -79,8 +79,9 @@ module.exports = function(pool) { // build CREATE TABLE DDL using col_meta + mapped data types const table = fcTable(source.tname, version.id); + const systemCols = new Set(['pf_id', 'pf_iter', 'pf_logid', 'pf_user', 'pf_created_at']); const colDefs = colResult.rows - .filter(c => c.cname !== 'id') + .filter(c => !systemCols.has(c.cname)) .map(c => { const pgType = mapType(c.data_type, c.numeric_precision, c.numeric_scale); const quoted = `"${c.cname}"`; @@ -89,12 +90,12 @@ module.exports = function(pool) { const ddl = ` CREATE TABLE ${table} ( - id bigserial PRIMARY KEY, + pf_id bigserial PRIMARY KEY, ${colDefs}, - iter text NOT NULL, - logid bigint NOT NULL, + pf_iter text NOT NULL, + pf_logid bigint NOT NULL, pf_user text, - created_at timestamptz NOT NULL DEFAULT now() + pf_created_at timestamptz NOT NULL DEFAULT now() ) `; await client.query(ddl);