diff --git a/definitions/materialized/atomicmarket_template_prices.sql b/definitions/materialized/atomicmarket_template_prices.sql deleted file mode 100644 index ed9bf4b2..00000000 --- a/definitions/materialized/atomicmarket_template_prices.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE MATERIALIZED VIEW IF NOT EXISTS atomicmarket_template_prices AS - SELECT * FROM atomicmarket_template_prices_master; - -CREATE UNIQUE INDEX atomicmarket_template_prices_pkey ON atomicmarket_template_prices (market_contract, assets_contract, collection_name, template_id, symbol); -CREATE INDEX atomicmarket_template_prices_fkey ON atomicmarket_template_prices (assets_contract, collection_name, template_id); - -CREATE INDEX atomicmarket_template_prices_collection_name ON atomicmarket_template_prices USING btree (collection_name); -CREATE INDEX atomicmarket_template_prices_template_id ON atomicmarket_template_prices USING btree (template_id); -CREATE INDEX atomicmarket_template_prices_median ON atomicmarket_template_prices USING btree (median); -CREATE INDEX atomicmarket_template_prices_average ON atomicmarket_template_prices USING btree (average); -CREATE INDEX atomicmarket_template_prices_suggested_median ON atomicmarket_template_prices USING btree (suggested_median); -CREATE INDEX atomicmarket_template_prices_suggested_average ON atomicmarket_template_prices USING btree (suggested_average); -CREATE INDEX atomicmarket_template_prices_min ON atomicmarket_template_prices USING btree ("min"); -CREATE INDEX atomicmarket_template_prices_max ON atomicmarket_template_prices USING btree ("max"); -CREATE INDEX atomicmarket_template_prices_sales ON atomicmarket_template_prices USING btree (sales); diff --git a/definitions/migrations/1.3.14/atomicmarket.sql b/definitions/migrations/1.3.14/atomicmarket.sql new file mode 100644 index 00000000..11cc056c --- /dev/null +++ b/definitions/migrations/1.3.14/atomicmarket.sql @@ -0,0 +1,131 @@ +DROP VIEW IF EXISTS atomicmarket_template_prices_master CASCADE; + +DROP TABLE IF EXISTS atomicmarket_template_prices CASCADE; +CREATE TABLE atomicmarket_template_prices ( + market_contract varchar(12) not null, + assets_contract varchar(12) not null, + collection_name varchar(12), + template_id bigint not null, + symbol varchar(12) not null, + median bigint, + average bigint, + suggested_median bigint, + suggested_average bigint, + "min" bigint, + "max" bigint, + sales bigint +); + + +ALTER TABLE atomicmarket_template_prices + ADD constraint atomicmarket_template_prices_pkey + primary key (market_contract, assets_contract, collection_name, template_id, symbol); + + +CREATE INDEX IF NOT EXISTS atomicmarket_stats_markets_template_id_time ON atomicmarket_stats_markets (template_id, time); +DROP INDEX IF EXISTS atomicmarket_stats_markets_template_id; + + +DROP FUNCTION IF EXISTS update_atomicmarket_template_prices; +CREATE OR REPLACE FUNCTION update_atomicmarket_template_prices() RETURNS INT +LANGUAGE plpgsql +AS $$ +DECLARE + result INT = 0; + temp INT; + rec RECORD; + current_block_time BIGINT = (SELECT MAX(block_time) FROM contract_readers); +BEGIN + FOR rec IN + WITH templates AS MATERIALIZED ( + SELECT DISTINCT template_id, assets_contract + FROM atomicmarket_stats_prices_master + WHERE template_id IS NOT NULL + ), sales AS MATERIALIZED ( + SELECT assets_contract, SUBSTRING(f FROM 2)::BIGINT template_id, MIN(price) min_price + FROM atomicmarket_sales_filters_listed + JOIN LATERAL UNNEST(filter) u(f) ON u.f LIKE 't%' + WHERE seller_contract IS DISTINCT FROM TRUE + AND asset_count = 1 + AND updated_at_time + 0 <= (current_block_time - 3600 * 24 * 3 * 1000) -- only include sales older than 3 days + GROUP BY template_id, assets_contract + ) + SELECT template_id, assets_contract, sug.suggested_median, sug.suggested_average + FROM templates + LEFT OUTER JOIN sales USING (template_id, assets_contract) + CROSS JOIN LATERAL ( + SELECT + LEAST(PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY price), sales.min_price) suggested_median, + LEAST(AVG(price)::BIGINT, sales.min_price) suggested_average + FROM ( + ( + SELECT listing_id /* not used, but required to prevent the same price being discarded in the union*/, price + FROM atomicmarket_stats_prices_master + WHERE template_id = templates.template_id AND assets_contract = templates.assets_contract + AND time >= ((extract(epoch from now() - '3 days'::INTERVAL)) * 1000)::BIGINT + ) + UNION + ( + SELECT listing_id, price + FROM atomicmarket_stats_prices_master + WHERE template_id = templates.template_id AND assets_contract = templates.assets_contract + ORDER BY time DESC + LIMIT 5 + ) + ) prices + ) sug + LOOP + INSERT INTO atomicmarket_template_prices AS tp (market_contract, assets_contract, collection_name, template_id, symbol, + median, average, suggested_median, suggested_average, "min", "max", sales) + SELECT + market_contract, assets_contract, collection_name, template_id, symbol, + PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY price) median, + AVG(price)::bigint average, + rec.suggested_median, + rec.suggested_average, + MIN(price) "min", MAX(price) "max", COUNT(*) sales + FROM atomicmarket_stats_prices_master + WHERE template_id = rec.template_id AND assets_contract = rec.assets_contract + GROUP BY market_contract, assets_contract, collection_name, template_id, symbol + ON CONFLICT (market_contract, assets_contract, collection_name, template_id, symbol) + DO UPDATE SET + median = EXCLUDED.median, + average = EXCLUDED.average, + suggested_median = EXCLUDED.suggested_median, + suggested_average = EXCLUDED.suggested_average, + "min" = EXCLUDED."min", + "max" = EXCLUDED."max", + sales = EXCLUDED.sales + WHERE tp.median IS DISTINCT FROM EXCLUDED.median + OR tp.average IS DISTINCT FROM EXCLUDED.average + OR tp.suggested_median IS DISTINCT FROM EXCLUDED.suggested_median + OR tp."min" IS DISTINCT FROM EXCLUDED."min" + OR tp."max" IS DISTINCT FROM EXCLUDED."max" + OR tp.sales IS DISTINCT FROM EXCLUDED.sales + ; + + GET DIAGNOSTICS temp = ROW_COUNT; + result = result + temp; + END LOOP; + + IF (random() <= 0.05) -- occasionally, remove deleted templates + THEN + DELETE FROM atomicmarket_template_prices + WHERE (template_id, assets_contract) NOT IN ( + SELECT DISTINCT template_id, assets_contract + FROM atomicmarket_stats_prices_master + WHERE template_id IS NOT NULL + ); + GET DIAGNOSTICS temp = ROW_COUNT; + result = result + temp; + END IF; + + RETURN result; +END +$$; + +SELECT update_atomicmarket_template_prices(); + +create index atomicmarket_template_prices_collection_name on atomicmarket_template_prices (collection_name); +create index atomicmarket_template_prices_template_id on atomicmarket_template_prices (template_id); + diff --git a/definitions/migrations/1.3.14/database.sql b/definitions/migrations/1.3.14/database.sql new file mode 100644 index 00000000..ca069fb6 --- /dev/null +++ b/definitions/migrations/1.3.14/database.sql @@ -0,0 +1 @@ +UPDATE dbinfo SET "value" = '1.3.14' WHERE name = 'version'; diff --git a/definitions/tables/atomicmarket_tables.sql b/definitions/tables/atomicmarket_tables.sql index 6aedd3f9..58f8420e 100644 --- a/definitions/tables/atomicmarket_tables.sql +++ b/definitions/tables/atomicmarket_tables.sql @@ -306,4 +306,4 @@ CREATE INDEX atomicmarket_stats_markets_price ON atomicmarket_stats_markets USIN CREATE INDEX atomicmarket_stats_markets_time ON atomicmarket_stats_markets USING btree ("time"); CREATE INDEX atomicmarket_stats_markets_asset_id ON atomicmarket_stats_markets USING btree ("asset_id"); CREATE INDEX atomicmarket_stats_markets_schema_name ON atomicmarket_stats_markets USING btree ("schema_name"); -CREATE INDEX atomicmarket_stats_markets_template_id ON atomicmarket_stats_markets USING btree ("template_id"); +CREATE INDEX atomicmarket_stats_markets_template_id_time ON atomicmarket_stats_markets (template_id, time); diff --git a/definitions/views/atomicmarket_template_prices_master.sql b/definitions/views/atomicmarket_template_prices_master.sql deleted file mode 100644 index 485ef216..00000000 --- a/definitions/views/atomicmarket_template_prices_master.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE OR REPLACE VIEW atomicmarket_template_prices_master AS - SELECT - t2.market_contract, t2.assets_contract::text, t2.collection_name, t2.template_id, t2.symbol, - PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY t2.price) median, - AVG(t2.price)::bigint average, - PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY t2.price) FILTER (WHERE t2.number <= 5 OR t2."time" / 1000 >= extract(epoch from now()) - 3600 * 24 * 3) suggested_median, - (AVG(t2.price) FILTER (WHERE t2.number <= 5 OR t2."time" / 1000 >= extract(epoch from now()) - 3600 * 24 * 3))::bigint suggested_average, - MIN(t2.price) "min", MAX(t2.price) "max", COUNT(*) sales - FROM ( - SELECT - t1.*, row_number() OVER (PARTITION BY t1.assets_contract, t1.collection_name, t1.template_id ORDER BY t1."time" DESC) "number" - FROM atomicmarket_stats_prices_master t1 - WHERE t1.template_id IS NOT NULL - ) t2 - GROUP BY t2.market_contract, t2.assets_contract, t2.collection_name, t2.template_id, t2.symbol diff --git a/package.json b/package.json index 5912fc19..5caa549c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eosio-contract-api", - "version": "1.3.13", + "version": "1.3.14", "description": "EOSIO Contract API", "author": "pink.gg", "license": "AGPL-3.0", diff --git a/src/api/namespaces/atomicmarket/handlers/assets.test.ts b/src/api/namespaces/atomicmarket/handlers/assets.test.ts index f7303286..3bc3d089 100644 --- a/src/api/namespaces/atomicmarket/handlers/assets.test.ts +++ b/src/api/namespaces/atomicmarket/handlers/assets.test.ts @@ -41,7 +41,7 @@ describe('AtomicMarket Assets API', () => { template_id: template_id2, }); - await client.query('REFRESH MATERIALIZED VIEW atomicmarket_template_prices'); + await client.refreshTemplatePrices(); expect(await getAssetIds({sort: 'suggested_median_price', asset_id: `${asset_id1},${asset_id2}`})) .to.deep.equal([asset_id1, asset_id2]); @@ -69,7 +69,7 @@ describe('AtomicMarket Assets API', () => { template_id: template_id2, }); - await client.query('REFRESH MATERIALIZED VIEW atomicmarket_template_prices'); + await client.refreshTemplatePrices(); expect(await getAssetIds({sort: 'suggested_average_price', asset_id: `${asset_id1},${asset_id2}`})) .to.deep.equal([asset_id1, asset_id2]); @@ -97,7 +97,7 @@ describe('AtomicMarket Assets API', () => { template_id: template_id2, }); - await client.query('REFRESH MATERIALIZED VIEW atomicmarket_template_prices'); + await client.refreshTemplatePrices(); expect(await getAssetIds({sort: 'median_price', asset_id: `${asset_id1},${asset_id2}`})) .to.deep.equal([asset_id1, asset_id2]); @@ -125,7 +125,7 @@ describe('AtomicMarket Assets API', () => { template_id: template_id2, }); - await client.query('REFRESH MATERIALIZED VIEW atomicmarket_template_prices'); + await client.refreshTemplatePrices(); expect(await getAssetIds({sort: 'average_price', asset_id: `${asset_id1},${asset_id2}`})) .to.deep.equal([asset_id1, asset_id2]); diff --git a/src/api/namespaces/atomicmarket/handlers/prices.test.ts b/src/api/namespaces/atomicmarket/handlers/prices.test.ts index 483ff827..ae3a05ce 100644 --- a/src/api/namespaces/atomicmarket/handlers/prices.test.ts +++ b/src/api/namespaces/atomicmarket/handlers/prices.test.ts @@ -26,6 +26,8 @@ describe('AtomicMarket Prices API', () => { }); txit('has data', async () => { + await client.createContractReader(); + const account = 'account1234'; const token = await client.createToken({token_symbol: 'SYM'}); const token2 = await client.createToken({token_symbol: 'WAX'}); @@ -145,7 +147,7 @@ describe('AtomicMarket Prices API', () => { buyoffer_id: buyOffer6.buyoffer_id, asset_id: asset6.asset_id }); - await client.refreshPrice(); + await client.refreshTemplatePrices(); const response = await getUsersInventoryPrices({}, getTestContext(client, {account})); diff --git a/src/api/namespaces/atomicmarket/handlers/sales2.get-sales-templates.test.ts b/src/api/namespaces/atomicmarket/handlers/sales2.get-sales-templates.test.ts index 2c7d4672..cfae382c 100644 --- a/src/api/namespaces/atomicmarket/handlers/sales2.get-sales-templates.test.ts +++ b/src/api/namespaces/atomicmarket/handlers/sales2.get-sales-templates.test.ts @@ -10,7 +10,7 @@ const {client, txit} = initAtomicMarketTest(); async function getSalesIds(values: RequestValues): Promise> { const testContext = getTestContext(client); - await client.query('SELECT update_atomicmarket_sales_filters()'); + await client.refreshSalesFilters(); const result = await getSalesTemplatesV2Action({ symbol: 'TEST', @@ -404,7 +404,7 @@ describe('AtomicMarket Sales API', () => { const {offer_id} = await client.createOfferAsset({}, {template_id}); const {sale_id} = await client.createSale({offer_id}); - await client.query('SELECT update_atomicmarket_sales_filters()'); + await client.refreshSalesFilters(); const testContext = getTestContext(client); diff --git a/src/api/namespaces/atomicmarket/handlers/sales2.test.ts b/src/api/namespaces/atomicmarket/handlers/sales2.test.ts index 2bfd6ab2..fea85a2e 100644 --- a/src/api/namespaces/atomicmarket/handlers/sales2.test.ts +++ b/src/api/namespaces/atomicmarket/handlers/sales2.test.ts @@ -15,7 +15,7 @@ async function getSalesIds(values: RequestValues, options = {refresh: true}): Pr const testContext = getTestContext(client); if (options.refresh) { - await client.query('SELECT update_atomicmarket_sales_filters()'); + await client.refreshSalesFilters(); } const result = await getSalesV2Action(values, testContext); @@ -69,7 +69,7 @@ describe('AtomicMarket Sales API', () => { state: SaleState.LISTED, }); - await client.query('SELECT update_atomicmarket_sales_filters()'); + await client.refreshSalesFilters(); await client.query(`UPDATE atomicmarket_sales SET state = ${SaleState.SOLD} WHERE sale_id = $1`, [sale_id2]); @@ -680,7 +680,7 @@ describe('AtomicMarket Sales API', () => { const testContext = getTestContext(client); - await client.query('SELECT update_atomicmarket_sales_filters()'); + await client.refreshSalesFilters(); const result = await getSalesV2Action({ids: `${sale_id}`, count: 'true'}, testContext); @@ -768,7 +768,7 @@ describe('AtomicMarket Sales API', () => { const testContext = getTestContext(client); - await client.query('SELECT update_atomicmarket_sales_filters()'); + await client.refreshSalesFilters(); const [result] = await getSalesV2Action({}, testContext); diff --git a/src/api/namespaces/atomicmarket/handlers/stats.test.ts b/src/api/namespaces/atomicmarket/handlers/stats.test.ts index 02cad25d..11d13a22 100644 --- a/src/api/namespaces/atomicmarket/handlers/stats.test.ts +++ b/src/api/namespaces/atomicmarket/handlers/stats.test.ts @@ -10,6 +10,8 @@ const {client, txit} = initAtomicMarketTest(); describe('AtomicMarket Stats API', () => { describe('getTemplateStatsAction', () => { txit('gets the templates sales and volume', async () => { + await client.createContractReader(); + await client.createToken({token_symbol: 'TOKEN1'}); const {template_id} = await client.createTemplate(); const {template_id: templateId2} = await client.createTemplate(); @@ -36,7 +38,6 @@ describe('AtomicMarket Stats API', () => { const response = await getTemplateStatsAction({symbol: 'TOKEN1'}, context); - expect(response.results.length).to.equal(2); expect(response.results.find((r: any) => r.template.template_id === template_id)).to.deep.contains({ volume: '2', @@ -51,6 +52,8 @@ describe('AtomicMarket Stats API', () => { context('with template_id filter', () => { txit('gets the templates sales and volume even if they dont have sales', async () => { + await client.createContractReader(); + await client.createToken({token_symbol: 'TOKEN1'}); const context = getTestContext(client); // Included @@ -73,6 +76,8 @@ describe('AtomicMarket Stats API', () => { context('with schema_name filter', () => { txit('gets the templates sales and volume even if they dont have sales', async () => { + await client.createContractReader(); + await client.createToken({token_symbol: 'TOKEN1'}); const context = getTestContext(client); // Included @@ -96,6 +101,8 @@ describe('AtomicMarket Stats API', () => { context('with collection_name filter', () => { txit('gets the templates sales and volume even if they dont have sales', async () => { + await client.createContractReader(); + await client.createToken({token_symbol: 'TOKEN1'}); const context = getTestContext(client); // Included @@ -119,6 +126,8 @@ describe('AtomicMarket Stats API', () => { context('with search filter', () => { txit('gets the templates sales and volume even if they dont have sales', async () => { + await client.createContractReader(); + await client.createToken({token_symbol: 'TOKEN1'}); const context = getTestContext(client); // Included @@ -141,6 +150,8 @@ describe('AtomicMarket Stats API', () => { context('with time after and before filter', () => { txit('gets the templates sales and volume even if they dont have sales in the period defined', async () => { + await client.createContractReader(); + await client.createToken({token_symbol: 'TOKEN1'}); // Included const {template_id} = await client.createTemplate(); diff --git a/src/api/namespaces/atomicmarket/test.ts b/src/api/namespaces/atomicmarket/test.ts index 390534e5..04da3837 100644 --- a/src/api/namespaces/atomicmarket/test.ts +++ b/src/api/namespaces/atomicmarket/test.ts @@ -65,16 +65,22 @@ export class AtomicMarketTestClient extends AtomicAssetsTestClient { }); } - async refreshPrice(): Promise { + async refreshTemplatePrices(): Promise { + await this.refreshSalesFilters(); + await this.refreshStatsMarket(); - await this.query('REFRESH MATERIALIZED VIEW atomicmarket_template_prices'); + await this.query('SELECT update_atomicmarket_template_prices()'); } async refreshStatsMarket(): Promise { await this.query('SELECT update_atomicmarket_stats_market()'); } + async refreshSalesFilters(): Promise { + await this.query('SELECT update_atomicmarket_sales_filters()'); + } + async createBuyOffer(values: Record = {}): Promise> { return this.insert('atomicmarket_buyoffers', { market_contract: 'amtest', diff --git a/src/filler/handlers/atomicmarket/index.ts b/src/filler/handlers/atomicmarket/index.ts index 099e117b..42a2a937 100644 --- a/src/filler/handlers/atomicmarket/index.ts +++ b/src/filler/handlers/atomicmarket/index.ts @@ -80,12 +80,7 @@ export default class AtomicMarketHandler extends ContractHandler { const views = [ 'atomicmarket_assets_master', 'atomicmarket_auctions_master', 'atomicmarket_sales_master', 'atomicmarket_sale_prices_master', - 'atomicmarket_stats_prices_master', - 'atomicmarket_template_prices_master', 'atomicmarket_buyoffers_master' - ]; - - const materializedViews = [ - 'atomicmarket_template_prices', + 'atomicmarket_stats_prices_master', 'atomicmarket_buyoffers_master' ]; const procedures = ['atomicmarket_auction_mints', 'atomicmarket_buyoffer_mints', 'atomicmarket_sale_mints']; @@ -101,10 +96,6 @@ export default class AtomicMarketHandler extends ContractHandler { await client.query(fs.readFileSync('./definitions/views/' + view + '.sql', {encoding: 'utf8'})); } - for (const view of materializedViews) { - await client.query(fs.readFileSync('./definitions/materialized/' + view + '.sql', {encoding: 'utf8'})); - } - for (const procedure of procedures) { await client.query(fs.readFileSync('./definitions/procedures/' + procedure + '.sql', {encoding: 'utf8'})); } @@ -139,7 +130,6 @@ export default class AtomicMarketHandler extends ContractHandler { if (version === '1.3.13') { await client.query(fs.readFileSync('./definitions/views/atomicmarket_stats_prices_master.sql', {encoding: 'utf8'})); - await client.query(fs.readFileSync('./definitions/views/atomicmarket_template_prices_master.sql', {encoding: 'utf8'})); } } @@ -240,7 +230,7 @@ export default class AtomicMarketHandler extends ContractHandler { 'atomicmarket_sales', 'atomicmarket_buyoffers', 'atomicmarket_buyoffers_assets', 'atomicmarket_config', 'atomicmarket_delphi_pairs', 'atomicmarket_marketplaces', 'atomicmarket_token_symbols', 'atomicmarket_bonusfees', 'atomicmarket_balances', - 'atomicmarket_stats_markets', + 'atomicmarket_stats_markets', 'atomicmarket_template_prices', ]; for (const table of tables) { @@ -249,14 +239,6 @@ export default class AtomicMarketHandler extends ContractHandler { [this.args.atomicmarket_account] ); } - - const materializedViews = [ - 'atomicmarket_template_prices', - ]; - - for (const view of materializedViews) { - await client.query('REFRESH MATERIALIZED VIEW ' + client.escapeIdentifier(view) + ''); - } } async register(processor: DataProcessor, notifier: ApiNotificationSender): Promise<() => any> { @@ -274,26 +256,6 @@ export default class AtomicMarketHandler extends ContractHandler { destructors.push(logProcessor(this, processor)); } - const materializedViews: Array<{name: string, priority: JobQueuePriority}> = [ - {name: 'atomicmarket_template_prices', priority: JobQueuePriority.LOW}, - ]; - - for (const view of materializedViews) { - let lastVacuum = Date.now(); - - this.filler.jobs.add(`Refresh MV ${view.name}`, 60_000, view.priority, async () => { - await this.connection.database.query(`REFRESH MATERIALIZED VIEW CONCURRENTLY ${view.name}`); - - if (lastVacuum + 3600 * 24 * 1000 < Date.now()) { - await this.connection.database.query(`VACUUM ANALYZE ${view.name}`); - - logger.info(`Successfully ran vacuum on ${view.name}`); - - lastVacuum = Date.now(); - } - }); - } - this.filler.jobs.add('update_atomicmarket_sale_mints', 30_000, JobQueuePriority.MEDIUM, async () => { await this.connection.database.query( 'CALL update_atomicmarket_sale_mints($1, $2)', @@ -331,12 +293,18 @@ export default class AtomicMarketHandler extends ContractHandler { ); }); - this.filler.jobs.add('refresh_atomicmarket_sales_filters_price', 60_000, JobQueuePriority.MEDIUM, async () => { + this.filler.jobs.add('update_atomicmarket_stats_market', 60_000, JobQueuePriority.MEDIUM, async () => { await this.connection.database.query( 'SELECT update_atomicmarket_stats_market()' ); }); + this.filler.jobs.add('update_atomicmarket_template_prices', 60_000 * 60, JobQueuePriority.MEDIUM, async () => { + await this.connection.database.query( + 'SELECT update_atomicmarket_template_prices()' + ); + }); + return (): any => destructors.map(fn => fn()); } } diff --git a/src/utils/test.ts b/src/utils/test.ts index 9817f1b6..f9131efd 100644 --- a/src/utils/test.ts +++ b/src/utils/test.ts @@ -37,6 +37,17 @@ export class TestClient extends Client { }); } + async createContractReader(values: Record = {}): Promise> { + return await this.insert('contract_readers', { + name: 'test-default', + block_num: this.getId(), + block_time: this.getId(), + live: false, + updated: this.getId(), + ...values, + }); + } + protected async insert(table: string, data: Record): Promise> { data = data || {};