Proof: Include the postgres loader exports required by the deployed operator-dashboard backend bootstrap path. Assumptions: The current postgres loader additions in the worktree are part of the operator-dashboard runtime surface and intended to ship with the dashboard backend. Still fake: External alert delivery remains unconfigured; this commit only resolves the live operator-dashboard startup blocker.
This commit is contained in:
parent
b8d731408e
commit
3fd0d8fe46
1 changed files with 422 additions and 1 deletions
|
|
@ -14,6 +14,8 @@ const TABLES = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const PORTFOLIO_METRICS_TABLE = 'portfolio_metrics_snapshots';
|
const PORTFOLIO_METRICS_TABLE = 'portfolio_metrics_snapshots';
|
||||||
|
const CREDITED_LIQUIDITY_STATUSES = ['CREDITED', 'COMPLETED', 'FINALIZED', 'SETTLED'];
|
||||||
|
const COMPLETED_WITHDRAWAL_STATUSES = ['COMPLETED', 'FINALIZED', 'SETTLED'];
|
||||||
|
|
||||||
export function createPostgresPool({ connectionString }) {
|
export function createPostgresPool({ connectionString }) {
|
||||||
return new Pool({
|
return new Pool({
|
||||||
|
|
@ -147,7 +149,10 @@ export async function insertHistoryEvent(pool, { table, topic, event, record })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadPortfolioMetricInputs(pool) {
|
export async function loadPortfolioMetricInputs(pool, {
|
||||||
|
btcAsset = null,
|
||||||
|
eureAsset = null,
|
||||||
|
} = {}) {
|
||||||
const [currentInventory, currentPrice, commandAggregate, resultAggregate] = await Promise.all([
|
const [currentInventory, currentPrice, commandAggregate, resultAggregate] = await Promise.all([
|
||||||
loadLatestEventPayload(pool, 'intent_inventory_snapshots'),
|
loadLatestEventPayload(pool, 'intent_inventory_snapshots'),
|
||||||
loadLatestEventPayload(pool, 'market_price_events'),
|
loadLatestEventPayload(pool, 'market_price_events'),
|
||||||
|
|
@ -184,6 +189,18 @@ export async function loadPortfolioMetricInputs(pool) {
|
||||||
[firstCommandAt],
|
[firstCommandAt],
|
||||||
);
|
);
|
||||||
const baselinePrice = await loadNearestPricePayload(pool, baselineInventory?.ingested_at || firstCommandAt);
|
const baselinePrice = await loadNearestPricePayload(pool, baselineInventory?.ingested_at || firstCommandAt);
|
||||||
|
const externalFlows = (
|
||||||
|
baselineInventory
|
||||||
|
&& baselinePrice
|
||||||
|
&& btcAsset?.assetId
|
||||||
|
&& eureAsset?.assetId
|
||||||
|
)
|
||||||
|
? await loadExternalAssetFlowsSince(pool, {
|
||||||
|
since: firstCommandAt,
|
||||||
|
btcAsset,
|
||||||
|
eureAsset,
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentInventory,
|
currentInventory,
|
||||||
|
|
@ -194,6 +211,7 @@ export async function loadPortfolioMetricInputs(pool) {
|
||||||
inventory: baselineInventory.payload,
|
inventory: baselineInventory.payload,
|
||||||
price: baselinePrice.payload,
|
price: baselinePrice.payload,
|
||||||
} : null,
|
} : null,
|
||||||
|
externalFlows,
|
||||||
commandCount,
|
commandCount,
|
||||||
resultCount,
|
resultCount,
|
||||||
};
|
};
|
||||||
|
|
@ -242,6 +260,196 @@ export async function loadLatestPortfolioMetric(pool) {
|
||||||
return normalizePortfolioMetricRow(result.rows[0]);
|
return normalizePortfolioMetricRow(result.rows[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function loadLatestInventorySnapshot(pool) {
|
||||||
|
const latest = await loadLatestEventPayload(pool, 'intent_inventory_snapshots');
|
||||||
|
if (!latest) return null;
|
||||||
|
return {
|
||||||
|
ingested_at: latest.ingested_at,
|
||||||
|
payload: latest.payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadLatestMarketPrice(pool) {
|
||||||
|
const latest = await loadLatestEventPayload(pool, 'market_price_events');
|
||||||
|
if (!latest) return null;
|
||||||
|
return {
|
||||||
|
ingested_at: latest.ingested_at,
|
||||||
|
payload: latest.payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadRecentQuotes(pool, { limit = 10 } = {}) {
|
||||||
|
const result = await pool.query(
|
||||||
|
`
|
||||||
|
SELECT observed_at, ingested_at, payload
|
||||||
|
FROM swap_demand_events
|
||||||
|
ORDER BY ingested_at DESC
|
||||||
|
LIMIT $1
|
||||||
|
`,
|
||||||
|
[limit],
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.rows.map(normalizeRecentQuoteRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadSuccessfulTradeSummary(pool) {
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
COUNT(*)::INT AS total,
|
||||||
|
MAX(COALESCE(observed_at, ingested_at)) AS last_successful_trade_at
|
||||||
|
FROM trade_execution_results
|
||||||
|
WHERE payload->>'status' = 'submitted'
|
||||||
|
`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
total: Number(result.rows[0]?.total || 0),
|
||||||
|
last_successful_trade_at: toIsoTimestamp(result.rows[0]?.last_successful_trade_at),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadSuccessfulTradesPage(pool, { page = 1, pageSize = 20 } = {}) {
|
||||||
|
const normalizedPage = Math.max(1, Number(page) || 1);
|
||||||
|
const normalizedPageSize = Math.max(1, Number(pageSize) || 20);
|
||||||
|
const offset = (normalizedPage - 1) * normalizedPageSize;
|
||||||
|
|
||||||
|
const [countResult, rowsResult] = await Promise.all([
|
||||||
|
pool.query(`
|
||||||
|
SELECT COUNT(*)::INT AS total
|
||||||
|
FROM trade_execution_results
|
||||||
|
WHERE payload->>'status' = 'submitted'
|
||||||
|
`),
|
||||||
|
pool.query(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
r.observed_at AS result_observed_at,
|
||||||
|
r.ingested_at AS result_ingested_at,
|
||||||
|
r.payload AS result_payload,
|
||||||
|
c.payload AS command_payload,
|
||||||
|
d.payload AS decision_payload
|
||||||
|
FROM trade_execution_results r
|
||||||
|
LEFT JOIN execute_trade_commands c
|
||||||
|
ON c.decision_key = r.decision_key
|
||||||
|
LEFT JOIN trade_decisions d
|
||||||
|
ON d.decision_key = COALESCE(c.payload->>'decision_id', r.payload->>'decision_id')
|
||||||
|
WHERE r.payload->>'status' = 'submitted'
|
||||||
|
ORDER BY COALESCE(r.observed_at, r.ingested_at) DESC
|
||||||
|
LIMIT $1
|
||||||
|
OFFSET $2
|
||||||
|
`,
|
||||||
|
[normalizedPageSize, offset],
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const total = Number(countResult.rows[0]?.total || 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
page: normalizedPage,
|
||||||
|
page_size: normalizedPageSize,
|
||||||
|
total,
|
||||||
|
total_pages: total > 0 ? Math.ceil(total / normalizedPageSize) : 1,
|
||||||
|
items: rowsResult.rows.map(normalizeSuccessfulTradeRow),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadCurrentFundingObservations(pool) {
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT DISTINCT ON (decision_key)
|
||||||
|
observed_at,
|
||||||
|
ingested_at,
|
||||||
|
payload
|
||||||
|
FROM funding_observations
|
||||||
|
WHERE decision_key IS NOT NULL
|
||||||
|
ORDER BY decision_key, ingested_at DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
return result.rows
|
||||||
|
.map((row) => ({
|
||||||
|
observed_at: toIsoTimestamp(row.observed_at),
|
||||||
|
ingested_at: toIsoTimestamp(row.ingested_at),
|
||||||
|
payload: row.payload,
|
||||||
|
}))
|
||||||
|
.sort((left, right) => (
|
||||||
|
Date.parse(
|
||||||
|
right.payload?.last_seen_at
|
||||||
|
|| right.observed_at
|
||||||
|
|| right.ingested_at
|
||||||
|
|| '',
|
||||||
|
) - Date.parse(
|
||||||
|
left.payload?.last_seen_at
|
||||||
|
|| left.observed_at
|
||||||
|
|| left.ingested_at
|
||||||
|
|| '',
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadRecentDepositStatuses(pool, { limit = 20 } = {}) {
|
||||||
|
const normalizedLimit = Math.max(1, Number(limit) || 20);
|
||||||
|
const fetchLimit = Math.max(normalizedLimit * 4, normalizedLimit);
|
||||||
|
const result = await pool.query(
|
||||||
|
`
|
||||||
|
SELECT observed_at, ingested_at, payload
|
||||||
|
FROM liquidity_actions
|
||||||
|
WHERE payload->>'action_type' = 'deposit_status_observed'
|
||||||
|
ORDER BY COALESCE(observed_at, ingested_at) DESC
|
||||||
|
LIMIT $1
|
||||||
|
`,
|
||||||
|
[fetchLimit],
|
||||||
|
);
|
||||||
|
|
||||||
|
const seen = new Set();
|
||||||
|
const uniqueRows = [];
|
||||||
|
for (const row of result.rows) {
|
||||||
|
const key = buildDepositStatusRowKey(row.payload);
|
||||||
|
if (seen.has(key)) continue;
|
||||||
|
seen.add(key);
|
||||||
|
uniqueRows.push(row);
|
||||||
|
if (uniqueRows.length >= normalizedLimit) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniqueRows.map((row) => ({
|
||||||
|
observed_at: toIsoTimestamp(row.observed_at),
|
||||||
|
ingested_at: toIsoTimestamp(row.ingested_at),
|
||||||
|
payload: row.payload,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadRecentAlertTransitions(pool, { limit = 20 } = {}) {
|
||||||
|
const result = await pool.query(
|
||||||
|
`
|
||||||
|
SELECT observed_at, ingested_at, payload
|
||||||
|
FROM ops_alerts
|
||||||
|
ORDER BY ingested_at DESC
|
||||||
|
LIMIT $1
|
||||||
|
`,
|
||||||
|
[limit],
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.rows.map((row) => ({
|
||||||
|
observed_at: toIsoTimestamp(row.observed_at),
|
||||||
|
ingested_at: toIsoTimestamp(row.ingested_at),
|
||||||
|
payload: row.payload,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadRecentTradeDecisions(pool, { limit = 20 } = {}) {
|
||||||
|
const result = await pool.query(
|
||||||
|
`
|
||||||
|
SELECT observed_at, ingested_at, payload
|
||||||
|
FROM trade_decisions
|
||||||
|
ORDER BY COALESCE(observed_at, ingested_at) DESC
|
||||||
|
LIMIT $1
|
||||||
|
`,
|
||||||
|
[limit],
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.rows.map((row) => ({
|
||||||
|
observed_at: toIsoTimestamp(row.observed_at),
|
||||||
|
ingested_at: toIsoTimestamp(row.ingested_at),
|
||||||
|
payload: row.payload,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
async function loadLatestEventPayload(pool, table, clause = 'ORDER BY ingested_at DESC LIMIT 1', params = []) {
|
async function loadLatestEventPayload(pool, table, clause = 'ORDER BY ingested_at DESC LIMIT 1', params = []) {
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
`
|
`
|
||||||
|
|
@ -274,6 +482,138 @@ async function loadNearestPricePayload(pool, anchorAt) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadExternalAssetFlowsSince(pool, {
|
||||||
|
since,
|
||||||
|
btcAsset,
|
||||||
|
eureAsset,
|
||||||
|
} = {}) {
|
||||||
|
const [depositRows, withdrawalRows] = await Promise.all([
|
||||||
|
loadCreditedDepositRowsSince(pool, since),
|
||||||
|
loadCompletedWithdrawalRowsSince(pool, since),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const flows = [];
|
||||||
|
|
||||||
|
for (const row of depositRows) {
|
||||||
|
flows.push(await normalizeExternalFlowRow(pool, {
|
||||||
|
row,
|
||||||
|
kind: 'deposit',
|
||||||
|
sign: 1n,
|
||||||
|
btcAsset,
|
||||||
|
eureAsset,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const row of withdrawalRows) {
|
||||||
|
flows.push(await normalizeExternalFlowRow(pool, {
|
||||||
|
row,
|
||||||
|
kind: 'withdrawal',
|
||||||
|
sign: -1n,
|
||||||
|
btcAsset,
|
||||||
|
eureAsset,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return flows
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort((left, right) => timestampValue(left.effective_at) - timestampValue(right.effective_at));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCreditedDepositRowsSince(pool, since) {
|
||||||
|
const result = await pool.query(
|
||||||
|
`
|
||||||
|
SELECT DISTINCT ON (
|
||||||
|
payload->'details'->>'tx_hash',
|
||||||
|
payload->>'chain',
|
||||||
|
payload->>'asset_id',
|
||||||
|
payload->'details'->>'amount'
|
||||||
|
)
|
||||||
|
observed_at,
|
||||||
|
ingested_at,
|
||||||
|
payload
|
||||||
|
FROM liquidity_actions
|
||||||
|
WHERE ingested_at > $1
|
||||||
|
AND payload->>'action_type' = 'deposit_status_observed'
|
||||||
|
AND UPPER(payload->>'status') = ANY($2)
|
||||||
|
AND COALESCE(payload->'details'->>'tx_hash', '') <> ''
|
||||||
|
ORDER BY
|
||||||
|
payload->'details'->>'tx_hash',
|
||||||
|
payload->>'chain',
|
||||||
|
payload->>'asset_id',
|
||||||
|
payload->'details'->>'amount',
|
||||||
|
ingested_at DESC
|
||||||
|
`,
|
||||||
|
[since, CREDITED_LIQUIDITY_STATUSES],
|
||||||
|
);
|
||||||
|
return result.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCompletedWithdrawalRowsSince(pool, since) {
|
||||||
|
const result = await pool.query(
|
||||||
|
`
|
||||||
|
SELECT DISTINCT ON (
|
||||||
|
payload->'details'->>'withdrawal_hash',
|
||||||
|
payload->>'chain',
|
||||||
|
payload->>'asset_id'
|
||||||
|
)
|
||||||
|
observed_at,
|
||||||
|
ingested_at,
|
||||||
|
payload
|
||||||
|
FROM liquidity_actions
|
||||||
|
WHERE ingested_at > $1
|
||||||
|
AND payload->>'action_type' = 'withdrawal_status_changed'
|
||||||
|
AND UPPER(payload->>'status') = ANY($2)
|
||||||
|
AND COALESCE(payload->'details'->>'withdrawal_hash', '') <> ''
|
||||||
|
ORDER BY
|
||||||
|
payload->'details'->>'withdrawal_hash',
|
||||||
|
payload->>'chain',
|
||||||
|
payload->>'asset_id',
|
||||||
|
ingested_at DESC
|
||||||
|
`,
|
||||||
|
[since, COMPLETED_WITHDRAWAL_STATUSES],
|
||||||
|
);
|
||||||
|
return result.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function normalizeExternalFlowRow(pool, {
|
||||||
|
row,
|
||||||
|
kind,
|
||||||
|
sign,
|
||||||
|
btcAsset,
|
||||||
|
eureAsset,
|
||||||
|
} = {}) {
|
||||||
|
const payload = row?.payload || {};
|
||||||
|
const details = payload.details || {};
|
||||||
|
const assetId = payload.asset_id || details.asset_id || null;
|
||||||
|
if (!assetId) return null;
|
||||||
|
|
||||||
|
const amount = String(details.amount || '0');
|
||||||
|
const effectiveAt = toIsoTimestamp(row.observed_at || row.ingested_at);
|
||||||
|
const signedUnits = (sign * BigInt(amount)).toString();
|
||||||
|
let referencePriceAtFlowTime = null;
|
||||||
|
|
||||||
|
if (assetId === btcAsset?.assetId) {
|
||||||
|
const nearestPrice = await loadNearestPricePayload(pool, effectiveAt);
|
||||||
|
referencePriceAtFlowTime = nearestPrice?.payload?.eure_per_btc || null;
|
||||||
|
} else if (assetId !== eureAsset?.assetId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
flow_id:
|
||||||
|
kind === 'deposit'
|
||||||
|
? `deposit:${details.tx_hash || effectiveAt || Math.random().toString(16).slice(2)}`
|
||||||
|
: `withdrawal:${details.withdrawal_hash || effectiveAt || Math.random().toString(16).slice(2)}`,
|
||||||
|
kind,
|
||||||
|
asset_id: assetId,
|
||||||
|
effective_at: effectiveAt,
|
||||||
|
signed_units: signedUnits,
|
||||||
|
tx_hash: details.tx_hash || null,
|
||||||
|
withdrawal_hash: details.withdrawal_hash || null,
|
||||||
|
reference_price_eure_per_btc_at_flow_time: referencePriceAtFlowTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function normalizePortfolioMetricRow(row) {
|
function normalizePortfolioMetricRow(row) {
|
||||||
return {
|
return {
|
||||||
metric_id: row.metric_id,
|
metric_id: row.metric_id,
|
||||||
|
|
@ -284,6 +624,87 @@ function normalizePortfolioMetricRow(row) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeRecentQuoteRow(row) {
|
||||||
|
const payload = row.payload || {};
|
||||||
|
return {
|
||||||
|
quote_id: payload.quote_id || null,
|
||||||
|
pair: payload.pair || buildPair(payload.asset_in, payload.asset_out),
|
||||||
|
asset_in: payload.asset_in || null,
|
||||||
|
asset_out: payload.asset_out || null,
|
||||||
|
request_kind: payload.request_kind || null,
|
||||||
|
amount_in: payload.amount_in ?? null,
|
||||||
|
amount_out: payload.amount_out ?? null,
|
||||||
|
min_deadline_ms: payload.min_deadline_ms ?? null,
|
||||||
|
observed_at: toIsoTimestamp(row.observed_at),
|
||||||
|
ingested_at: toIsoTimestamp(row.ingested_at),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSuccessfulTradeRow(row) {
|
||||||
|
const resultPayload = row.result_payload || {};
|
||||||
|
const commandPayload = row.command_payload || {};
|
||||||
|
const decisionPayload = row.decision_payload || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
command_id: resultPayload.command_id || commandPayload.command_id || null,
|
||||||
|
decision_id:
|
||||||
|
commandPayload.decision_id
|
||||||
|
|| resultPayload.decision_id
|
||||||
|
|| decisionPayload.decision_id
|
||||||
|
|| null,
|
||||||
|
execution_key: resultPayload.execution_key || commandPayload.execution_key || null,
|
||||||
|
quote_id: resultPayload.quote_id || commandPayload.quote_id || decisionPayload.quote_id || null,
|
||||||
|
pair: resultPayload.pair || commandPayload.pair || decisionPayload.pair || null,
|
||||||
|
observed_at: toIsoTimestamp(row.result_observed_at || row.result_ingested_at),
|
||||||
|
ingested_at: toIsoTimestamp(row.result_ingested_at),
|
||||||
|
status: resultPayload.status || null,
|
||||||
|
result_code: resultPayload.result_code || null,
|
||||||
|
request_kind: commandPayload.request_kind || decisionPayload.request_kind || null,
|
||||||
|
asset_in: commandPayload.asset_in || null,
|
||||||
|
asset_out: commandPayload.asset_out || null,
|
||||||
|
amount_in: resolveTradeAmount(commandPayload, 'amount_in'),
|
||||||
|
amount_out: resolveTradeAmount(commandPayload, 'amount_out'),
|
||||||
|
quoted_amount_in: commandPayload.amount_in || null,
|
||||||
|
quoted_amount_out: commandPayload.amount_out || null,
|
||||||
|
gross_edge_pct: decisionPayload.gross_edge_pct || null,
|
||||||
|
decision_reason: decisionPayload.decision_reason || null,
|
||||||
|
direction: decisionPayload.direction || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTradeAmount(commandPayload, field) {
|
||||||
|
const quoteOutputField = commandPayload?.quote_output?.[field];
|
||||||
|
const proposedField = commandPayload?.[`proposed_${field}`];
|
||||||
|
return quoteOutputField || proposedField || commandPayload?.[field] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPair(assetIn, assetOut) {
|
||||||
|
if (!assetIn || !assetOut) return null;
|
||||||
|
return `${assetIn}->${assetOut}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toIsoTimestamp(value) {
|
||||||
|
if (!value) return null;
|
||||||
|
const date = new Date(value);
|
||||||
|
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function timestampValue(value) {
|
||||||
|
const parsed = Date.parse(value || '');
|
||||||
|
return Number.isFinite(parsed) ? parsed : -Infinity;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDepositStatusRowKey(payload) {
|
||||||
|
const details = payload?.details || {};
|
||||||
|
return [
|
||||||
|
details.tx_hash || 'no-tx',
|
||||||
|
payload?.chain || details.chain || 'no-chain',
|
||||||
|
payload?.asset_id || details.asset_id || 'no-asset',
|
||||||
|
details.address || 'no-address',
|
||||||
|
details.amount || 'no-amount',
|
||||||
|
].join('|');
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureExpressionIndex(pool, { name, table, expression }) {
|
async function ensureExpressionIndex(pool, { name, table, expression }) {
|
||||||
await pool.query(`
|
await pool.query(`
|
||||||
CREATE INDEX IF NOT EXISTS ${name}
|
CREATE INDEX IF NOT EXISTS ${name}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue