bench
This commit is contained in:
130
BENCHMARK_REPORT.md
Normal file
130
BENCHMARK_REPORT.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# Database Drivers API Timing & Benchmark Report
|
||||
|
||||
- **Date**: 2026-09-22T06:09:22.346Z
|
||||
- **Scale**: small
|
||||
- **Read Iterations on Filled DB**: 2
|
||||
|
||||
## SQLite (BetterSqlite3Provider) Detailed Breakdown
|
||||
|
||||
| API Method | Calls | Avg (ms) | Min (ms) | Max (ms) | P50 (ms) | P95 (ms) | Errors |
|
||||
| :--- | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
|
||||
| `article.addImage` | 4 | 35.62 | 13.96 | 44.76 | 41.95 | 44.76 | 0 |
|
||||
| `article.createUnderParent` | 19 | 0.53 | 0.46 | 0.92 | 0.50 | 0.92 | 0 |
|
||||
| `article.delete` | 1 | 0.64 | 0.64 | 0.64 | 0.64 | 0.64 | 0 |
|
||||
| `article.deleteImage` | 1 | 0.85 | 0.85 | 0.85 | 0.85 | 0.85 | 0 |
|
||||
| `article.getBySku` | 2 | 0.68 | 0.59 | 0.76 | 0.76 | 0.76 | 0 |
|
||||
| `article.getHistoryBySku` | 2 | 0.32 | 0.29 | 0.35 | 0.35 | 0.35 | 0 |
|
||||
| `article.importRows` | 1 | 2.45 | 2.45 | 2.45 | 2.45 | 2.45 | 0 |
|
||||
| `article.listExport` | 4 | 0.85 | 0.64 | 1.27 | 0.80 | 1.27 | 0 |
|
||||
| `article.listSearchIndex` | 2 | 0.54 | 0.46 | 0.61 | 0.61 | 0.61 | 0 |
|
||||
| `article.orderImages` | 3 | 0.50 | 0.36 | 0.71 | 0.43 | 0.71 | 0 |
|
||||
| `article.setBrandIdBySku` | 18 | 0.58 | 0.53 | 0.78 | 0.56 | 0.78 | 0 |
|
||||
| `article.setCategoryIdsBySku` | 18 | 0.74 | 0.70 | 0.99 | 0.72 | 0.99 | 0 |
|
||||
| `article.setEansBySku` | 18 | 0.57 | 0.50 | 0.96 | 0.53 | 0.96 | 0 |
|
||||
| `article.setFatherBySku` | 13 | 0.66 | 0.59 | 1.00 | 0.63 | 1.00 | 0 |
|
||||
| `article.setPricesBySku` | 18 | 0.68 | 0.63 | 1.10 | 0.65 | 1.10 | 0 |
|
||||
| `article.setWeightBySku` | 18 | 0.53 | 0.49 | 0.68 | 0.51 | 0.68 | 0 |
|
||||
| `article.updateBySku` | 18 | 0.82 | 0.73 | 1.61 | 0.76 | 1.61 | 0 |
|
||||
| `articlesupplier.create` | 19 | 0.56 | 0.50 | 1.05 | 0.52 | 1.05 | 0 |
|
||||
| `articlesupplier.delete` | 1 | 0.40 | 0.40 | 0.40 | 0.40 | 0.40 | 0 |
|
||||
| `articlesupplier.listByArticleSku` | 2 | 0.25 | 0.23 | 0.26 | 0.26 | 0.26 | 0 |
|
||||
| `articlesupplier.listByEntityAndSkus` | 2 | 0.44 | 0.36 | 0.52 | 0.52 | 0.52 | 0 |
|
||||
| `articlesupplier.listBySupplierAndSkus` | 2 | 0.37 | 0.35 | 0.38 | 0.38 | 0.38 | 0 |
|
||||
| `articlesupplier.order` | 18 | 0.29 | 0.27 | 0.49 | 0.28 | 0.49 | 0 |
|
||||
| `articlesupplier.update` | 18 | 0.42 | 0.36 | 0.79 | 0.40 | 0.79 | 0 |
|
||||
| `brand.delete` | 1 | 0.25 | 0.25 | 0.25 | 0.25 | 0.25 | 0 |
|
||||
| `brand.list` | 1 | 0.26 | 0.26 | 0.26 | 0.26 | 0.26 | 0 |
|
||||
| `brand.upsert` | 3 | 0.26 | 0.22 | 0.30 | 0.25 | 0.30 | 0 |
|
||||
| `category.addImage` | 2 | 43.22 | 22.62 | 63.83 | 63.83 | 63.83 | 0 |
|
||||
| `category.createUnderParent` | 10 | 0.39 | 0.30 | 0.85 | 0.33 | 0.85 | 0 |
|
||||
| `category.delete` | 1 | 0.24 | 0.24 | 0.24 | 0.24 | 0.24 | 0 |
|
||||
| `category.deleteImage` | 1 | 0.36 | 0.36 | 0.36 | 0.36 | 0.36 | 0 |
|
||||
| `category.ensureByPath` | 1 | 0.59 | 0.59 | 0.59 | 0.59 | 0.59 | 0 |
|
||||
| `category.getCategoryArticles` | 2 | 0.39 | 0.37 | 0.42 | 0.42 | 0.42 | 0 |
|
||||
| `category.getTreeForCompany` | 2 | 0.70 | 0.65 | 0.76 | 0.76 | 0.76 | 0 |
|
||||
| `category.move` | 1 | 0.49 | 0.49 | 0.49 | 0.49 | 0.49 | 0 |
|
||||
| `category.order` | 1 | 0.40 | 0.40 | 0.40 | 0.40 | 0.40 | 0 |
|
||||
| `category.rename` | 1 | 0.25 | 0.25 | 0.25 | 0.25 | 0.25 | 0 |
|
||||
| `company.create` | 2 | 1.10 | 0.78 | 1.41 | 1.41 | 1.41 | 0 |
|
||||
| `company.delete` | 1 | 0.23 | 0.23 | 0.23 | 0.23 | 0.23 | 0 |
|
||||
| `company.list` | 1 | 0.26 | 0.26 | 0.26 | 0.26 | 0.26 | 0 |
|
||||
| `company.update` | 1 | 0.50 | 0.50 | 0.50 | 0.50 | 0.50 | 0 |
|
||||
| `companysetting.get` | 1 | 0.23 | 0.23 | 0.23 | 0.23 | 0.23 | 0 |
|
||||
| `companysetting.list` | 3 | 0.21 | 0.16 | 0.24 | 0.22 | 0.24 | 0 |
|
||||
| `companysetting.set` | 2 | 0.35 | 0.29 | 0.42 | 0.42 | 0.42 | 0 |
|
||||
| `companyuser.add` | 1 | 0.12 | 0.12 | 0.12 | 0.12 | 0.12 | 0 |
|
||||
| `companyuser.delete` | 1 | 0.10 | 0.10 | 0.10 | 0.10 | 0.10 | 0 |
|
||||
| `companyuser.getByEmail` | 1 | 0.11 | 0.11 | 0.11 | 0.11 | 0.11 | 0 |
|
||||
| `customergroup.create` | 3 | 0.30 | 0.24 | 0.38 | 0.29 | 0.38 | 0 |
|
||||
| `customergroup.delete` | 1 | 0.28 | 0.28 | 0.28 | 0.28 | 0.28 | 0 |
|
||||
| `customergroup.list` | 2 | 0.19 | 0.17 | 0.21 | 0.21 | 0.21 | 0 |
|
||||
| `customergroup.order` | 1 | 0.41 | 0.41 | 0.41 | 0.41 | 0.41 | 0 |
|
||||
| `customergroup.update` | 1 | 0.33 | 0.33 | 0.33 | 0.33 | 0.33 | 0 |
|
||||
| `delivery.create` | 7 | 9.48 | 0.67 | 62.06 | 0.74 | 62.06 | 0 |
|
||||
| `delivery.delete` | 1 | 0.26 | 0.26 | 0.26 | 0.26 | 0.26 | 0 |
|
||||
| `delivery.getById` | 2 | 0.50 | 0.29 | 0.72 | 0.72 | 0.72 | 0 |
|
||||
| `delivery.list` | 2 | 0.33 | 0.28 | 0.38 | 0.38 | 0.38 | 0 |
|
||||
| `delivery.update` | 6 | 0.52 | 0.36 | 1.13 | 0.39 | 1.13 | 0 |
|
||||
| `entity.create` | 7 | 0.48 | 0.39 | 0.89 | 0.42 | 0.89 | 0 |
|
||||
| `entity.delete` | 1 | 0.25 | 0.25 | 0.25 | 0.25 | 0.25 | 0 |
|
||||
| `entity.getById` | 2 | 0.18 | 0.15 | 0.21 | 0.21 | 0.21 | 0 |
|
||||
| `entity.isNumberFree` | 6 | 0.17 | 0.15 | 0.24 | 0.16 | 0.24 | 0 |
|
||||
| `entity.list` | 4 | 0.25 | 0.19 | 0.36 | 0.24 | 0.36 | 0 |
|
||||
| `entity.peekNextNumber` | 6 | 0.24 | 0.20 | 0.39 | 0.22 | 0.39 | 0 |
|
||||
| `entityaddress.create` | 7 | 0.33 | 0.27 | 0.59 | 0.29 | 0.59 | 0 |
|
||||
| `entityaddress.delete` | 1 | 0.28 | 0.28 | 0.28 | 0.28 | 0.28 | 0 |
|
||||
| `entityaddress.list` | 2 | 0.30 | 0.26 | 0.35 | 0.35 | 0.35 | 0 |
|
||||
| `entityaddress.update` | 6 | 0.38 | 0.33 | 0.56 | 0.35 | 0.56 | 0 |
|
||||
| `entitycontact.create` | 7 | 0.28 | 0.24 | 0.41 | 0.27 | 0.41 | 0 |
|
||||
| `entitycontact.delete` | 1 | 0.26 | 0.26 | 0.26 | 0.26 | 0.26 | 0 |
|
||||
| `entitycontact.list` | 2 | 0.25 | 0.20 | 0.30 | 0.30 | 0.30 | 0 |
|
||||
| `entitycontact.update` | 6 | 0.33 | 0.29 | 0.46 | 0.31 | 0.46 | 0 |
|
||||
| `google_auth.add` | 3 | 0.21 | 0.08 | 0.42 | 0.13 | 0.42 | 0 |
|
||||
| `google_auth.getByEmail` | 2 | 0.06 | 0.04 | 0.09 | 0.09 | 0.09 | 0 |
|
||||
| `google_auth.getById` | 2 | 0.09 | 0.07 | 0.10 | 0.10 | 0.10 | 0 |
|
||||
| `google_auth.listAllowedEmails` | 1 | 0.14 | 0.14 | 0.14 | 0.14 | 0.14 | 0 |
|
||||
| `google_auth.removeAllowedEmail` | 1 | 0.09 | 0.09 | 0.09 | 0.09 | 0.09 | 0 |
|
||||
| `google_auth.setProfile` | 1 | 0.16 | 0.16 | 0.16 | 0.16 | 0.16 | 0 |
|
||||
| `google_auth.setSelectedCompany` | 2 | 0.07 | 0.05 | 0.08 | 0.08 | 0.08 | 0 |
|
||||
| `lang.create` | 2 | 0.07 | 0.07 | 0.07 | 0.07 | 0.07 | 0 |
|
||||
| `lang.delete` | 1 | 0.12 | 0.12 | 0.12 | 0.12 | 0.12 | 0 |
|
||||
| `lang.ensure` | 2 | 0.10 | 0.09 | 0.11 | 0.11 | 0.11 | 0 |
|
||||
| `lang.getByIso639` | 1 | 0.05 | 0.05 | 0.05 | 0.05 | 0.05 | 0 |
|
||||
| `lang.list` | 1 | 0.06 | 0.06 | 0.06 | 0.06 | 0.06 | 0 |
|
||||
| `manifestattachment.add` | 3 | 35.65 | 1.22 | 55.34 | 50.41 | 55.34 | 0 |
|
||||
| `manifestattachment.getById` | 2 | 0.75 | 0.65 | 0.85 | 0.85 | 0.85 | 0 |
|
||||
| `manifestattachment.listByManifestId` | 3 | 0.61 | 0.51 | 0.70 | 0.61 | 0.70 | 0 |
|
||||
| `manifestattachment.order` | 1 | 0.54 | 0.54 | 0.54 | 0.54 | 0.54 | 0 |
|
||||
| `manifestattachment.remove` | 1 | 0.86 | 0.86 | 0.86 | 0.86 | 0.86 | 0 |
|
||||
| `numbers.list` | 1 | 0.27 | 0.27 | 0.27 | 0.27 | 0.27 | 0 |
|
||||
| `order.create` | 7 | 0.50 | 0.40 | 0.69 | 0.45 | 0.69 | 0 |
|
||||
| `order.delete` | 1 | 0.24 | 0.24 | 0.24 | 0.24 | 0.24 | 0 |
|
||||
| `order.getById` | 2 | 0.24 | 0.21 | 0.28 | 0.28 | 0.28 | 0 |
|
||||
| `order.list` | 2 | 0.21 | 0.17 | 0.25 | 0.25 | 0.25 | 0 |
|
||||
| `order.update` | 6 | 0.34 | 0.28 | 0.53 | 0.29 | 0.53 | 0 |
|
||||
| `pic.getImageByHash` | 2 | 0.66 | 0.59 | 0.74 | 0.74 | 0.74 | 0 |
|
||||
| `pic.getThumbByHash` | 2 | 0.71 | 0.66 | 0.76 | 0.76 | 0.76 | 0 |
|
||||
| `posterminal.delete` | 1 | 0.21 | 0.21 | 0.21 | 0.21 | 0.21 | 0 |
|
||||
| `posterminal.getCategoryPosterminalIds` | 1 | 0.11 | 0.11 | 0.11 | 0.11 | 0.11 | 0 |
|
||||
| `posterminal.list` | 3 | 0.18 | 0.15 | 0.19 | 0.19 | 0.19 | 0 |
|
||||
| `posterminal.setOff` | 1 | 0.38 | 0.38 | 0.38 | 0.38 | 0.38 | 0 |
|
||||
| `posterminal.setOffAll` | 1 | 0.30 | 0.30 | 0.30 | 0.30 | 0.30 | 0 |
|
||||
| `posterminal.setOn` | 1 | 0.41 | 0.41 | 0.41 | 0.41 | 0.41 | 0 |
|
||||
| `posterminal.setOnAll` | 1 | 0.54 | 0.54 | 0.54 | 0.54 | 0.54 | 0 |
|
||||
| `posterminal.upsert` | 3 | 0.27 | 0.19 | 0.35 | 0.28 | 0.35 | 0 |
|
||||
| `quote.create` | 7 | 0.61 | 0.39 | 1.21 | 0.53 | 1.21 | 0 |
|
||||
| `quote.delete` | 1 | 0.24 | 0.24 | 0.24 | 0.24 | 0.24 | 0 |
|
||||
| `quote.getById` | 2 | 0.23 | 0.23 | 0.23 | 0.23 | 0.23 | 0 |
|
||||
| `quote.list` | 2 | 0.20 | 0.17 | 0.23 | 0.23 | 0.23 | 0 |
|
||||
| `quote.update` | 6 | 0.36 | 0.29 | 0.66 | 0.31 | 0.66 | 0 |
|
||||
| `supplier.create` | 4 | 0.41 | 0.33 | 0.63 | 0.34 | 0.63 | 0 |
|
||||
| `supplier.delete` | 1 | 0.30 | 0.30 | 0.30 | 0.30 | 0.30 | 0 |
|
||||
| `supplier.list` | 3 | 0.20 | 0.16 | 0.26 | 0.16 | 0.26 | 0 |
|
||||
| `supplier.update` | 3 | 0.42 | 0.30 | 0.58 | 0.39 | 0.58 | 0 |
|
||||
| `tax.create` | 1 | 0.08 | 0.08 | 0.08 | 0.08 | 0.08 | 0 |
|
||||
| `tax.ensure` | 2 | 0.06 | 0.06 | 0.06 | 0.06 | 0.06 | 0 |
|
||||
| `tax.getByRate` | 1 | 0.05 | 0.05 | 0.05 | 0.05 | 0.05 | 0 |
|
||||
| `tax.list` | 1 | 0.07 | 0.07 | 0.07 | 0.07 | 0.07 | 0 |
|
||||
| `unit.delete` | 1 | 0.25 | 0.25 | 0.25 | 0.25 | 0.25 | 0 |
|
||||
| `unit.list` | 1 | 0.29 | 0.29 | 0.29 | 0.29 | 0.29 | 0 |
|
||||
| `unit.upsert` | 3 | 0.52 | 0.32 | 0.78 | 0.47 | 0.78 | 0 |
|
||||
@@ -20,7 +20,8 @@
|
||||
"init-schema": "node scripts/init-schema.js",
|
||||
"purge-db": "node scripts/purge-db.js",
|
||||
"reset-db": "node scripts/reset-db.js",
|
||||
"test:api": "node scripts/test-api.js"
|
||||
"test:api": "node scripts/test-api.js",
|
||||
"test:benchmark": "node scripts/benchmark.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^1.8.1",
|
||||
|
||||
197
scripts/benchmark.js
Normal file
197
scripts/benchmark.js
Normal file
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import 'dotenv/config';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { performance } from 'node:perf_hooks';
|
||||
import chalk from 'chalk';
|
||||
import LibDashStore from '../src/index.js';
|
||||
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
|
||||
import { MssqlProvider } from '../src/providers/mssql/index.js';
|
||||
import { MetricsCollector, instrumentStore } from './benchmark/instrument.js';
|
||||
import { fillDatabase, benchmarkFilledDatabase, testLifecycleDeletions } from './benchmark/workload.js';
|
||||
import { BenchmarkReporter } from './benchmark/reporter.js';
|
||||
|
||||
// CLI argument parsing
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
const options = {
|
||||
driver: 'both',
|
||||
scale: 'small',
|
||||
iterations: 5,
|
||||
reportFile: 'BENCHMARK_REPORT.md',
|
||||
keepDb: false,
|
||||
};
|
||||
|
||||
for (const arg of args) {
|
||||
if (arg.startsWith('--driver=')) {
|
||||
options.driver = arg.split('=')[1].toLowerCase();
|
||||
} else if (arg.startsWith('--scale=')) {
|
||||
options.scale = arg.split('=')[1].toLowerCase();
|
||||
} else if (arg.startsWith('--iterations=')) {
|
||||
options.iterations = parseInt(arg.split('=')[1], 10);
|
||||
} else if (arg.startsWith('--report-file=')) {
|
||||
options.reportFile = arg.split('=')[1];
|
||||
} else if (arg === '--keep-db') {
|
||||
options.keepDb = true;
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
async function runSqliteBenchmark(options) {
|
||||
console.log(chalk.blue(`\n▶ Starting SQLite benchmark (scale: ${options.scale}, iterations: ${options.iterations})...`));
|
||||
|
||||
const tempDir = mkdtempSync(join(tmpdir(), 'libdashstore-bench-sqlite-'));
|
||||
const dbFile = join(tempDir, 'benchmark.db');
|
||||
const storageDir = join(tempDir, 'storage');
|
||||
|
||||
const provider = new BetterSqlite3Provider({
|
||||
filename: dbFile,
|
||||
storageDir,
|
||||
});
|
||||
|
||||
await provider.initialize();
|
||||
const rawStore = await LibDashStore.create(provider);
|
||||
const metrics = new MetricsCollector();
|
||||
const { store } = instrumentStore(rawStore, metrics);
|
||||
|
||||
const startTotal = performance.now();
|
||||
|
||||
try {
|
||||
console.log(chalk.dim(' 1. Filling SQLite database using domain APIs...'));
|
||||
const fillStart = performance.now();
|
||||
const ctx = await fillDatabase(store, options);
|
||||
console.log(chalk.green(` ✓ Filled SQLite DB in ${((performance.now() - fillStart) / 1000).toFixed(2)}s`));
|
||||
|
||||
console.log(chalk.dim(' 2. Benchmarking queries and lookups on filled database...'));
|
||||
const benchStart = performance.now();
|
||||
await benchmarkFilledDatabase(store, ctx, options);
|
||||
console.log(chalk.green(` ✓ Read queries completed in ${((performance.now() - benchStart) / 1000).toFixed(2)}s`));
|
||||
|
||||
console.log(chalk.dim(' 3. Testing lifecycle deletions...'));
|
||||
const delStart = performance.now();
|
||||
await testLifecycleDeletions(store, ctx);
|
||||
console.log(chalk.green(` ✓ Deletions completed in ${((performance.now() - delStart) / 1000).toFixed(2)}s`));
|
||||
|
||||
const totalElapsedMs = performance.now() - startTotal;
|
||||
const stats = metrics.getAllStats();
|
||||
return { stats, totalElapsedMs };
|
||||
} finally {
|
||||
provider.close();
|
||||
if (!options.keepDb) {
|
||||
try {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.dim(` [i] SQLite test database kept at: ${tempDir}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function runMssqlBenchmark(options) {
|
||||
console.log(chalk.blue(`\n▶ Starting MSSQL benchmark (scale: ${options.scale}, iterations: ${options.iterations})...`));
|
||||
|
||||
const config = {
|
||||
server: process.env.MSSQL_SERVER || '127.0.0.1',
|
||||
database: process.env.MSSQL_DATABASE || 'dashback',
|
||||
user: process.env.MSSQL_USER || 'dashback',
|
||||
password: process.env.MSSQL_PASSWORD || 'dashback',
|
||||
options: {
|
||||
encrypt: false,
|
||||
trustServerCertificate: true,
|
||||
},
|
||||
};
|
||||
|
||||
const provider = new MssqlProvider(config);
|
||||
await provider.ensurePool();
|
||||
|
||||
console.log(chalk.dim(' Resetting MSSQL schema (purge & init)...'));
|
||||
await provider.purge();
|
||||
await provider.initialize();
|
||||
|
||||
const rawStore = await LibDashStore.create(provider);
|
||||
const metrics = new MetricsCollector();
|
||||
const { store } = instrumentStore(rawStore, metrics);
|
||||
|
||||
const startTotal = performance.now();
|
||||
|
||||
try {
|
||||
console.log(chalk.dim(' 1. Filling MSSQL database using domain APIs...'));
|
||||
const fillStart = performance.now();
|
||||
const ctx = await fillDatabase(store, options);
|
||||
console.log(chalk.green(` ✓ Filled MSSQL DB in ${((performance.now() - fillStart) / 1000).toFixed(2)}s`));
|
||||
|
||||
console.log(chalk.dim(' 2. Benchmarking queries and lookups on filled database...'));
|
||||
const benchStart = performance.now();
|
||||
await benchmarkFilledDatabase(store, ctx, options);
|
||||
console.log(chalk.green(` ✓ Read queries completed in ${((performance.now() - benchStart) / 1000).toFixed(2)}s`));
|
||||
|
||||
console.log(chalk.dim(' 3. Testing lifecycle deletions...'));
|
||||
const delStart = performance.now();
|
||||
await testLifecycleDeletions(store, ctx);
|
||||
console.log(chalk.green(` ✓ Deletions completed in ${((performance.now() - delStart) / 1000).toFixed(2)}s`));
|
||||
|
||||
const totalElapsedMs = performance.now() - startTotal;
|
||||
const stats = metrics.getAllStats();
|
||||
return { stats, totalElapsedMs };
|
||||
} finally {
|
||||
await provider.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArgs();
|
||||
console.log(chalk.bold.yellow(`========================================================================`));
|
||||
console.log(chalk.bold.yellow(` LIBDASHSTORE DATABASE DRIVERS TEST & BENCHMARK SYSTEM`));
|
||||
console.log(chalk.bold.yellow(`========================================================================`));
|
||||
console.log(`Driver mode : ${chalk.cyan(options.driver)}`);
|
||||
console.log(`Scale : ${chalk.cyan(options.scale)}`);
|
||||
console.log(`Iterations : ${chalk.cyan(options.iterations)}`);
|
||||
console.log(`Report File : ${chalk.cyan(options.reportFile)}`);
|
||||
|
||||
let sqliteResult = null;
|
||||
let mssqlResult = null;
|
||||
|
||||
if (options.driver === 'both' || options.driver === 'sqlite') {
|
||||
sqliteResult = await runSqliteBenchmark(options);
|
||||
BenchmarkReporter.printDriverReport('better-sqlite3', sqliteResult.stats, sqliteResult.totalElapsedMs);
|
||||
}
|
||||
|
||||
if (options.driver === 'both' || options.driver === 'mssql') {
|
||||
mssqlResult = await runMssqlBenchmark(options);
|
||||
BenchmarkReporter.printDriverReport('mssql', mssqlResult.stats, mssqlResult.totalElapsedMs);
|
||||
}
|
||||
|
||||
if (sqliteResult && mssqlResult) {
|
||||
BenchmarkReporter.printComparisonReport(
|
||||
mssqlResult.stats,
|
||||
sqliteResult.stats,
|
||||
mssqlResult.totalElapsedMs,
|
||||
sqliteResult.totalElapsedMs,
|
||||
);
|
||||
}
|
||||
|
||||
const markdown = BenchmarkReporter.generateMarkdownReport({
|
||||
mssqlStats: mssqlResult?.stats,
|
||||
sqliteStats: sqliteResult?.stats,
|
||||
mssqlElapsedMs: mssqlResult?.totalElapsedMs,
|
||||
sqliteElapsedMs: sqliteResult?.totalElapsedMs,
|
||||
options,
|
||||
});
|
||||
|
||||
if (options.reportFile) {
|
||||
BenchmarkReporter.saveReportToFile(options.reportFile, markdown);
|
||||
}
|
||||
|
||||
console.log(chalk.bold.green(`\n✓ All database driver tests & benchmarks finished successfully!`));
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(chalk.bold.red(`\nBenchmark failed with error:`), err);
|
||||
process.exit(1);
|
||||
});
|
||||
126
scripts/benchmark/instrument.js
Normal file
126
scripts/benchmark/instrument.js
Normal file
@@ -0,0 +1,126 @@
|
||||
import { performance } from 'node:perf_hooks';
|
||||
|
||||
export class MetricsCollector {
|
||||
constructor() {
|
||||
this.entries = new Map();
|
||||
}
|
||||
|
||||
record(name, durationMs, error = null) {
|
||||
let entry = this.entries.get(name);
|
||||
if (!entry) {
|
||||
entry = {
|
||||
name,
|
||||
durations: [],
|
||||
count: 0,
|
||||
errors: 0,
|
||||
totalMs: 0,
|
||||
};
|
||||
this.entries.set(name, entry);
|
||||
}
|
||||
entry.count += 1;
|
||||
entry.totalMs += durationMs;
|
||||
entry.durations.push(durationMs);
|
||||
if (error) {
|
||||
entry.errors += 1;
|
||||
}
|
||||
}
|
||||
|
||||
getStats(name) {
|
||||
const entry = this.entries.get(name);
|
||||
if (!entry || entry.count === 0) return null;
|
||||
|
||||
const sorted = [...entry.durations].sort((a, b) => a - b);
|
||||
const count = entry.count;
|
||||
const totalMs = entry.totalMs;
|
||||
const avgMs = totalMs / count;
|
||||
const minMs = sorted[0];
|
||||
const maxMs = sorted[count - 1];
|
||||
|
||||
const p50Index = Math.min(Math.floor(count * 0.5), count - 1);
|
||||
const p95Index = Math.min(Math.floor(count * 0.95), count - 1);
|
||||
const p99Index = Math.min(Math.floor(count * 0.99), count - 1);
|
||||
|
||||
const p50Ms = sorted[p50Index];
|
||||
const p95Ms = sorted[p95Index];
|
||||
const p99Ms = sorted[p99Index];
|
||||
|
||||
return {
|
||||
name,
|
||||
count,
|
||||
errors: entry.errors,
|
||||
totalMs,
|
||||
avgMs,
|
||||
minMs,
|
||||
maxMs,
|
||||
p50Ms,
|
||||
p95Ms,
|
||||
p99Ms,
|
||||
};
|
||||
}
|
||||
|
||||
getAllStats() {
|
||||
const results = [];
|
||||
for (const name of this.entries.keys()) {
|
||||
results.push(this.getStats(name));
|
||||
}
|
||||
return results.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a LibDashStore instance so all method calls across all namespaces
|
||||
* are intercepted and measured with high-resolution performance timers.
|
||||
*
|
||||
* @param {import('../src/index.js').default} store
|
||||
* @param {MetricsCollector} metrics
|
||||
* @returns {import('../src/index.js').default} Instrumented store
|
||||
*/
|
||||
export function instrumentStore(store, metrics = new MetricsCollector()) {
|
||||
const ignoredProperties = new Set(['provider', 'pool', 'constructor']);
|
||||
|
||||
for (const [namespaceKey, namespaceVal] of Object.entries(store)) {
|
||||
if (ignoredProperties.has(namespaceKey)) continue;
|
||||
|
||||
// Direct root function like store.purge or store.initialize
|
||||
if (typeof namespaceVal === 'function') {
|
||||
const originalFn = namespaceVal.bind(store);
|
||||
store[namespaceKey] = async (...args) => {
|
||||
const start = performance.now();
|
||||
let error = null;
|
||||
try {
|
||||
return await originalFn(...args);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
throw err;
|
||||
} finally {
|
||||
const duration = performance.now() - start;
|
||||
metrics.record(`store.${namespaceKey}`, duration, error);
|
||||
}
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof namespaceVal === 'object' && namespaceVal !== null) {
|
||||
for (const [methodKey, methodVal] of Object.entries(namespaceVal)) {
|
||||
if (typeof methodVal === 'function') {
|
||||
const originalMethod = methodVal.bind(namespaceVal);
|
||||
namespaceVal[methodKey] = async (...args) => {
|
||||
const start = performance.now();
|
||||
let error = null;
|
||||
try {
|
||||
return await originalMethod(...args);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
throw err;
|
||||
} finally {
|
||||
const duration = performance.now() - start;
|
||||
metrics.record(`${namespaceKey}.${methodKey}`, duration, error);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { store, metrics };
|
||||
}
|
||||
186
scripts/benchmark/reporter.js
Normal file
186
scripts/benchmark/reporter.js
Normal file
@@ -0,0 +1,186 @@
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import chalk from 'chalk';
|
||||
|
||||
function fmtMs(ms) {
|
||||
if (ms == null || Number.isNaN(ms)) return '-';
|
||||
if (ms < 1) return `${ms.toFixed(2)} ms`;
|
||||
if (ms < 10) return `${ms.toFixed(2)} ms`;
|
||||
return `${ms.toFixed(1)} ms`;
|
||||
}
|
||||
|
||||
function padRight(str, len) {
|
||||
const s = String(str);
|
||||
return s + ' '.repeat(Math.max(0, len - s.length));
|
||||
}
|
||||
|
||||
function padLeft(str, len) {
|
||||
const s = String(str);
|
||||
return ' '.repeat(Math.max(0, len - s.length)) + s;
|
||||
}
|
||||
|
||||
export class BenchmarkReporter {
|
||||
/**
|
||||
* Prints the single-driver timing summary to stdout.
|
||||
*/
|
||||
static printDriverReport(driverName, stats, totalElapsedMs) {
|
||||
console.log();
|
||||
console.log(chalk.bold.cyan(`========================================================================================`));
|
||||
console.log(chalk.bold.cyan(` DATABASE DRIVER TIMING REPORT: ${driverName.toUpperCase()}`));
|
||||
console.log(chalk.bold.cyan(`========================================================================================`));
|
||||
|
||||
const totalOps = stats.reduce((acc, s) => acc + s.count, 0);
|
||||
const totalErrors = stats.reduce((acc, s) => acc + s.errors, 0);
|
||||
|
||||
const header = `${padRight('API Method', 38)} | ${padLeft('Calls', 6)} | ${padLeft('Avg (ms)', 10)} | ${padLeft('Min (ms)', 10)} | ${padLeft('Max (ms)', 10)} | ${padLeft('P50 (ms)', 10)} | ${padLeft('P95 (ms)', 10)}`;
|
||||
console.log(chalk.gray('-'.repeat(header.length)));
|
||||
console.log(chalk.bold(header));
|
||||
console.log(chalk.gray('-'.repeat(header.length)));
|
||||
|
||||
for (const item of stats) {
|
||||
const avgStr = fmtMs(item.avgMs);
|
||||
const minStr = fmtMs(item.minMs);
|
||||
const maxStr = fmtMs(item.maxMs);
|
||||
const p50Str = fmtMs(item.p50Ms);
|
||||
const p95Str = fmtMs(item.p95Ms);
|
||||
|
||||
let line = `${padRight(item.name, 38)} | ${padLeft(item.count, 6)} | ${padLeft(avgStr, 10)} | ${padLeft(minStr, 10)} | ${padLeft(maxStr, 10)} | ${padLeft(p50Str, 10)} | ${padLeft(p95Str, 10)}`;
|
||||
if (item.errors > 0) {
|
||||
line = chalk.red(line + ` [${item.errors} ERR]`);
|
||||
}
|
||||
console.log(line);
|
||||
}
|
||||
|
||||
console.log(chalk.gray('-'.repeat(header.length)));
|
||||
console.log(
|
||||
chalk.green(
|
||||
`Total Operations: ${totalOps} | Total Time: ${(totalElapsedMs / 1000).toFixed(2)}s | APIs Covered: ${stats.length} | Errors: ${totalErrors}`,
|
||||
),
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the side-by-side comparison between MSSQL and SQLite.
|
||||
*/
|
||||
static printComparisonReport(mssqlStats, sqliteStats, mssqlElapsedMs, sqliteElapsedMs) {
|
||||
console.log();
|
||||
console.log(chalk.bold.magenta(`========================================================================================================`));
|
||||
console.log(chalk.bold.magenta(` COMPARATIVE BENCHMARK: MSSQL vs BETTER-SQLITE3 (Timings in ms)`));
|
||||
console.log(chalk.bold.magenta(`========================================================================================================`));
|
||||
|
||||
const mssqlMap = new Map(mssqlStats.map((s) => [s.name, s]));
|
||||
const sqliteMap = new Map(sqliteStats.map((s) => [s.name, s]));
|
||||
const allNames = Array.from(new Set([...mssqlMap.keys(), ...sqliteMap.keys()])).sort();
|
||||
|
||||
const header = `${padRight('API Method', 36)} | ${padLeft('Calls', 6)} | ${padLeft('MSSQL Avg', 11)} | ${padLeft('MSSQL P95', 11)} | ${padLeft('SQLite Avg', 11)} | ${padLeft('SQLite P95', 11)} | ${padLeft('Ratio (M/S)', 12)}`;
|
||||
console.log(chalk.gray('-'.repeat(header.length)));
|
||||
console.log(chalk.bold(header));
|
||||
console.log(chalk.gray('-'.repeat(header.length)));
|
||||
|
||||
for (const name of allNames) {
|
||||
const m = mssqlMap.get(name);
|
||||
const s = sqliteMap.get(name);
|
||||
|
||||
const calls = m?.count || s?.count || 0;
|
||||
const mAvg = m ? fmtMs(m.avgMs) : '-';
|
||||
const mP95 = m ? fmtMs(m.p95Ms) : '-';
|
||||
const sAvg = s ? fmtMs(s.avgMs) : '-';
|
||||
const sP95 = s ? fmtMs(s.p95Ms) : '-';
|
||||
|
||||
let ratioStr = '-';
|
||||
if (m && s && s.avgMs > 0) {
|
||||
const ratio = m.avgMs / s.avgMs;
|
||||
ratioStr = `${ratio.toFixed(1)}x`;
|
||||
if (ratio > 1.5) {
|
||||
ratioStr = chalk.yellow(ratioStr + ' (SQLite faster)');
|
||||
} else if (ratio < 0.8) {
|
||||
ratioStr = chalk.cyan(ratioStr + ' (MSSQL faster)');
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`${padRight(name, 36)} | ${padLeft(calls, 6)} | ${padLeft(mAvg, 11)} | ${padLeft(mP95, 11)} | ${padLeft(sAvg, 11)} | ${padLeft(sP95, 11)} | ${padLeft(ratioStr, 12)}`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(chalk.gray('-'.repeat(header.length)));
|
||||
console.log(
|
||||
chalk.bold(
|
||||
`Total Runtime: MSSQL = ${(mssqlElapsedMs / 1000).toFixed(2)}s vs SQLite = ${(sqliteElapsedMs / 1000).toFixed(2)}s`,
|
||||
),
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a GitHub-flavored Markdown report.
|
||||
*/
|
||||
static generateMarkdownReport({ mssqlStats, sqliteStats, mssqlElapsedMs, sqliteElapsedMs, options = {} }) {
|
||||
const lines = [];
|
||||
lines.push('# Database Drivers API Timing & Benchmark Report');
|
||||
lines.push('');
|
||||
lines.push(`- **Date**: ${new Date().toISOString()}`);
|
||||
lines.push(`- **Scale**: ${options.scale || 'small'}`);
|
||||
lines.push(`- **Read Iterations on Filled DB**: ${options.iterations || 5}`);
|
||||
lines.push('');
|
||||
|
||||
if (mssqlStats && sqliteStats) {
|
||||
lines.push('## Comparative Summary');
|
||||
lines.push('');
|
||||
lines.push(`| Metric | MSSQL | better-sqlite3 | Delta |`);
|
||||
lines.push(`| :--- | :---: | :---: | :---: |`);
|
||||
const mssqlTotalOps = mssqlStats.reduce((acc, s) => acc + s.count, 0);
|
||||
const sqliteTotalOps = sqliteStats.reduce((acc, s) => acc + s.count, 0);
|
||||
lines.push(`| Total API Calls | ${mssqlTotalOps} | ${sqliteTotalOps} | - |`);
|
||||
lines.push(`| Total Runtime | ${(mssqlElapsedMs / 1000).toFixed(2)}s | ${(sqliteElapsedMs / 1000).toFixed(2)}s | ${(mssqlElapsedMs / sqliteElapsedMs).toFixed(1)}x |`);
|
||||
lines.push('');
|
||||
|
||||
lines.push('## API Timings Comparison (in milliseconds)');
|
||||
lines.push('');
|
||||
lines.push('| API Method | Calls | MSSQL Avg (ms) | MSSQL P95 (ms) | SQLite Avg (ms) | SQLite P95 (ms) | Ratio (MSSQL / SQLite) |');
|
||||
lines.push('| :--- | :---: | :---: | :---: | :---: | :---: | :---: |');
|
||||
|
||||
const mssqlMap = new Map(mssqlStats.map((s) => [s.name, s]));
|
||||
const sqliteMap = new Map(sqliteStats.map((s) => [s.name, s]));
|
||||
const allNames = Array.from(new Set([...mssqlMap.keys(), ...sqliteMap.keys()])).sort();
|
||||
|
||||
for (const name of allNames) {
|
||||
const m = mssqlMap.get(name);
|
||||
const s = sqliteMap.get(name);
|
||||
const calls = m?.count || s?.count || 0;
|
||||
const mAvg = m ? m.avgMs.toFixed(2) : '-';
|
||||
const mP95 = m ? m.p95Ms.toFixed(2) : '-';
|
||||
const sAvg = s ? s.avgMs.toFixed(2) : '-';
|
||||
const sP95 = s ? s.p95Ms.toFixed(2) : '-';
|
||||
const ratio = m && s && s.avgMs > 0 ? (m.avgMs / s.avgMs).toFixed(2) + 'x' : '-';
|
||||
lines.push(`| \`${name}\` | ${calls} | ${mAvg} | ${mP95} | ${sAvg} | ${sP95} | ${ratio} |`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
const drivers = [];
|
||||
if (mssqlStats) drivers.push({ name: 'MSSQL (MssqlProvider)', stats: mssqlStats, elapsed: mssqlElapsedMs });
|
||||
if (sqliteStats) drivers.push({ name: 'SQLite (BetterSqlite3Provider)', stats: sqliteStats, elapsed: sqliteElapsedMs });
|
||||
|
||||
for (const driver of drivers) {
|
||||
lines.push(`## ${driver.name} Detailed Breakdown`);
|
||||
lines.push('');
|
||||
lines.push('| API Method | Calls | Avg (ms) | Min (ms) | Max (ms) | P50 (ms) | P95 (ms) | Errors |');
|
||||
lines.push('| :--- | :---: | :---: | :---: | :---: | :---: | :---: | :---: |');
|
||||
|
||||
for (const item of driver.stats) {
|
||||
lines.push(
|
||||
`| \`${item.name}\` | ${item.count} | ${item.avgMs.toFixed(2)} | ${item.minMs.toFixed(2)} | ${item.maxMs.toFixed(2)} | ${item.p50Ms.toFixed(2)} | ${item.p95Ms.toFixed(2)} | ${item.errors} |`,
|
||||
);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
static saveReportToFile(filePath, markdownContent) {
|
||||
writeFileSync(filePath, markdownContent, 'utf-8');
|
||||
console.log(chalk.green(`✓ Benchmark report written to: ${filePath}`));
|
||||
}
|
||||
}
|
||||
789
scripts/benchmark/workload.js
Normal file
789
scripts/benchmark/workload.js
Normal file
@@ -0,0 +1,789 @@
|
||||
import sharp from 'sharp';
|
||||
|
||||
/**
|
||||
* Creates sample PNG image buffer.
|
||||
*/
|
||||
async function makeSampleImage(width = 200, height = 200, color = { r: 50, g: 120, b: 220 }) {
|
||||
return sharp({
|
||||
create: {
|
||||
width,
|
||||
height,
|
||||
channels: 4,
|
||||
background: { ...color, alpha: 1 },
|
||||
},
|
||||
})
|
||||
.png()
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates sample AVIF thumbnail buffer.
|
||||
*/
|
||||
async function makeSampleThumb(width = 64, height = 64) {
|
||||
return sharp({
|
||||
create: {
|
||||
width,
|
||||
height,
|
||||
channels: 4,
|
||||
background: { r: 200, g: 60, b: 60, alpha: 1 },
|
||||
},
|
||||
})
|
||||
.avif()
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes Phase 1: Database Population / Fill.
|
||||
* Systematically calls creation and write APIs to populate the database
|
||||
* with realistic entities and relationships according to the configured scale.
|
||||
*/
|
||||
export async function fillDatabase(store, options = {}) {
|
||||
const scale = options.scale || 'small';
|
||||
const counts = {
|
||||
small: { categories: 6, articles: 18, entities: 6, manifests: 6 },
|
||||
medium: { categories: 15, articles: 60, entities: 20, manifests: 20 },
|
||||
large: { categories: 30, articles: 200, entities: 50, manifests: 60 },
|
||||
}[scale] || { categories: 6, articles: 18, entities: 6, manifests: 6 };
|
||||
|
||||
const ctx = {
|
||||
googleAuthId: null,
|
||||
company: null,
|
||||
secondaryGoogleAuthId: null,
|
||||
secondaryCompany: null,
|
||||
categories: [],
|
||||
articles: [],
|
||||
fatherArticles: [],
|
||||
childArticles: [],
|
||||
customergroups: [],
|
||||
entities: [],
|
||||
suppliers: [],
|
||||
deliveries: [],
|
||||
orders: [],
|
||||
quotes: [],
|
||||
terminals: [],
|
||||
sampleImageHash: null,
|
||||
sampleImageThumbHash: null,
|
||||
attachments: [],
|
||||
};
|
||||
|
||||
const sampleImage = await makeSampleImage();
|
||||
const sampleThumb = await makeSampleThumb();
|
||||
|
||||
// 1. Google Auth setup
|
||||
const primaryEmail = `benchmark.admin.${Date.now()}@example.com`;
|
||||
const secEmail = `benchmark.secondary.${Date.now()}@example.com`;
|
||||
|
||||
ctx.googleAuthId = await store.google_auth.add(primaryEmail);
|
||||
await store.google_auth.setProfile(ctx.googleAuthId, {
|
||||
sub: `sub-${ctx.googleAuthId}`,
|
||||
picture: 'https://example.com/pic.png',
|
||||
});
|
||||
await store.google_auth.listAllowedEmails();
|
||||
|
||||
ctx.secondaryGoogleAuthId = await store.google_auth.add(secEmail);
|
||||
|
||||
// 2. Company setup
|
||||
ctx.company = await store.company.create({
|
||||
name: `Bench Corp ${Date.now()}`,
|
||||
iso639: 'de',
|
||||
taxrate: 19,
|
||||
reducedtaxrate: 7,
|
||||
googleAuthId: ctx.googleAuthId,
|
||||
});
|
||||
await store.google_auth.setSelectedCompany(ctx.googleAuthId, ctx.company.id);
|
||||
|
||||
ctx.secondaryCompany = await store.company.create({
|
||||
name: `Secondary Corp ${Date.now()}`,
|
||||
iso639: 'en',
|
||||
taxrate: 20,
|
||||
reducedtaxrate: 10,
|
||||
googleAuthId: ctx.secondaryGoogleAuthId,
|
||||
});
|
||||
await store.google_auth.setSelectedCompany(ctx.secondaryGoogleAuthId, ctx.secondaryCompany.id);
|
||||
|
||||
await store.company.update(ctx.company.id, {
|
||||
name: `Bench Corp Updated ${Date.now()}`,
|
||||
iso639: 'de',
|
||||
taxrate: 19,
|
||||
reducedtaxrate: 7,
|
||||
});
|
||||
await store.company.list(ctx.googleAuthId);
|
||||
|
||||
// 3. Company Users
|
||||
const compUserId = await store.companyuser.add(ctx.company.id, ctx.secondaryGoogleAuthId);
|
||||
ctx.compUserId = compUserId;
|
||||
await store.companyuser.getByEmail(secEmail);
|
||||
|
||||
// 4. Lang APIs
|
||||
await store.lang.ensure('en');
|
||||
await store.lang.ensure('fr');
|
||||
await store.lang.create('es');
|
||||
await store.lang.getByIso639('en');
|
||||
await store.lang.list();
|
||||
|
||||
// 5. Tax APIs
|
||||
await store.tax.ensure(19);
|
||||
await store.tax.ensure(7);
|
||||
const taxIdSample = await store.tax.create(5.5);
|
||||
ctx.taxSampleId = taxIdSample;
|
||||
await store.tax.getByRate(19);
|
||||
await store.tax.list();
|
||||
|
||||
// 6. Unit APIs
|
||||
await store.unit.upsert(ctx.googleAuthId, {
|
||||
si: 'kg',
|
||||
basesi: 'g',
|
||||
basesifactor: 1000,
|
||||
symbol: 'kg',
|
||||
name: 'Kilogramm',
|
||||
});
|
||||
await store.unit.upsert(ctx.googleAuthId, {
|
||||
si: 'stk',
|
||||
basesi: 'stk',
|
||||
basesifactor: 1,
|
||||
symbol: 'Stk',
|
||||
name: 'Stück',
|
||||
});
|
||||
await store.unit.list(ctx.googleAuthId);
|
||||
|
||||
// 7. Company Settings
|
||||
await store.companysetting.set(ctx.googleAuthId, {
|
||||
key: 'ui.theme',
|
||||
value: 'dark',
|
||||
frontend: true,
|
||||
writeonly: false,
|
||||
});
|
||||
await store.companysetting.set(ctx.googleAuthId, {
|
||||
key: 'labelprinter.type',
|
||||
value: 'zebra',
|
||||
frontend: false,
|
||||
writeonly: false,
|
||||
});
|
||||
await store.companysetting.get(ctx.googleAuthId, 'ui.theme');
|
||||
await store.companysetting.list(ctx.googleAuthId);
|
||||
|
||||
// 8. Brand APIs
|
||||
const brand1 = await store.brand.upsert(ctx.googleAuthId, {
|
||||
name: 'BenchBrand Pro',
|
||||
gsprpinfo: 'Manufacturer Pro GmbH',
|
||||
});
|
||||
const brand2 = await store.brand.upsert(ctx.googleAuthId, {
|
||||
name: 'BenchBrand Ultra',
|
||||
gsprpinfo: 'Manufacturer Ultra AG',
|
||||
});
|
||||
ctx.brands = [brand1, brand2];
|
||||
await store.brand.list(ctx.googleAuthId);
|
||||
|
||||
// 9. Customer Groups
|
||||
const cgVip = await store.customergroup.create(ctx.googleAuthId, { name: 'VIP Members' });
|
||||
const cgRetail = await store.customergroup.create(ctx.googleAuthId, { name: 'Retail Customers' });
|
||||
await store.customergroup.update(ctx.googleAuthId, cgVip.id, { name: 'VIP Gold Members' });
|
||||
const allCgs = await store.customergroup.list(ctx.googleAuthId);
|
||||
await store.customergroup.order(ctx.googleAuthId, {
|
||||
customergroupIds: allCgs.map((g) => g.id).reverse(),
|
||||
});
|
||||
await store.customergroup.list(ctx.googleAuthId);
|
||||
ctx.customergroups.push(cgRetail, cgVip);
|
||||
|
||||
// 10. Categories Hierarchy
|
||||
const rootCatId = ctx.company.rootCategoryId;
|
||||
const numCats = counts.categories;
|
||||
|
||||
for (let i = 1; i <= numCats; i++) {
|
||||
const parentId = i > 3 && ctx.categories.length > 0 ? ctx.categories[(i % 3)].id : rootCatId;
|
||||
const cat = await store.category.createUnderParent({
|
||||
googleAuthId: ctx.googleAuthId,
|
||||
parentId,
|
||||
name: `Category ${i}`,
|
||||
});
|
||||
ctx.categories.push(cat);
|
||||
}
|
||||
|
||||
// Ensure by path
|
||||
const deepCatId = await store.category.ensureByPath({
|
||||
googleAuthId: ctx.googleAuthId,
|
||||
parentId: rootCatId,
|
||||
path: 'Hardware / Components / Processors',
|
||||
});
|
||||
ctx.deepCatId = deepCatId;
|
||||
|
||||
// Category manipulation APIs
|
||||
if (ctx.categories.length >= 2) {
|
||||
await store.category.rename({
|
||||
googleAuthId: ctx.googleAuthId,
|
||||
id: ctx.categories[0].id,
|
||||
name: 'Main Catalog Category',
|
||||
});
|
||||
const orderParent = await store.category.createUnderParent({
|
||||
googleAuthId: ctx.googleAuthId,
|
||||
parentId: rootCatId,
|
||||
name: 'Order Test Parent',
|
||||
});
|
||||
const oChild1 = await store.category.createUnderParent({
|
||||
googleAuthId: ctx.googleAuthId,
|
||||
parentId: orderParent.id,
|
||||
name: 'Order Child 1',
|
||||
});
|
||||
const oChild2 = await store.category.createUnderParent({
|
||||
googleAuthId: ctx.googleAuthId,
|
||||
parentId: orderParent.id,
|
||||
name: 'Order Child 2',
|
||||
});
|
||||
await store.category.move({
|
||||
googleAuthId: ctx.googleAuthId,
|
||||
id: oChild2.id,
|
||||
parentId: orderParent.id,
|
||||
});
|
||||
await store.category.order({
|
||||
googleAuthId: ctx.googleAuthId,
|
||||
parentId: orderParent.id,
|
||||
categoryIds: [oChild2.id, oChild1.id],
|
||||
});
|
||||
}
|
||||
|
||||
// Category image
|
||||
await store.category.addImage(ctx.googleAuthId, ctx.categories[0].id, sampleImage);
|
||||
|
||||
// 11. Numbers APIs
|
||||
const numList = await store.numbers.list(ctx.googleAuthId);
|
||||
if (numList.length > 0) {
|
||||
await store.numbers.update(ctx.googleAuthId, numList[0].id, {
|
||||
prefix: 'BN-',
|
||||
padding: 6,
|
||||
next: 1001,
|
||||
});
|
||||
}
|
||||
|
||||
// 12. POS Terminals
|
||||
const term1 = await store.posterminal.upsert(ctx.googleAuthId, {
|
||||
name: 'Counter Terminal 1',
|
||||
pairingpin: '1234',
|
||||
});
|
||||
const term2 = await store.posterminal.upsert(ctx.googleAuthId, {
|
||||
name: 'Mobile Terminal 2',
|
||||
pairingpin: '5678',
|
||||
});
|
||||
ctx.terminals.push(term1, term2);
|
||||
|
||||
await store.posterminal.setOn(ctx.googleAuthId, term1.id, ctx.categories[0].id);
|
||||
await store.posterminal.setOff(ctx.googleAuthId, term1.id, ctx.categories[0].id);
|
||||
await store.posterminal.setOnAll(ctx.googleAuthId, term1.id);
|
||||
await store.posterminal.setOffAll(ctx.googleAuthId, term1.id);
|
||||
await store.posterminal.getCategoryPosterminalIds(ctx.googleAuthId, ctx.categories[0].id);
|
||||
await store.posterminal.list(ctx.googleAuthId);
|
||||
|
||||
// 13. Entities (Customers)
|
||||
const numEntities = counts.entities;
|
||||
for (let i = 1; i <= numEntities; i++) {
|
||||
const nextNum = await store.entity.peekNextNumber(ctx.googleAuthId);
|
||||
await store.entity.isNumberFree(ctx.googleAuthId, nextNum);
|
||||
const ent = await store.entity.create(ctx.googleAuthId, {
|
||||
customergroupId: ctx.customergroups[i % ctx.customergroups.length].id,
|
||||
name: `Customer Company ${i}`,
|
||||
firstName: `Firstname ${i}`,
|
||||
});
|
||||
ctx.entities.push(ent);
|
||||
|
||||
// Entity Address
|
||||
const addr = await store.entityaddress.create(ctx.googleAuthId, ent.id, {
|
||||
addressType: 'shipping',
|
||||
name: `Branch ${i}`,
|
||||
street: `Main Street ${i}`,
|
||||
streetNumber: `${i}`,
|
||||
zip: '10115',
|
||||
city: 'Berlin',
|
||||
county: 'Berlin',
|
||||
sort: 1,
|
||||
});
|
||||
await store.entityaddress.update(ctx.googleAuthId, ent.id, addr.id, {
|
||||
street: `Updated Street ${i}`,
|
||||
});
|
||||
|
||||
// Entity Contact
|
||||
const contact = await store.entitycontact.create(ctx.googleAuthId, ent.id, {
|
||||
contactType: 'email',
|
||||
contact: `contact${i}@benchmark.test`,
|
||||
sort: 1,
|
||||
});
|
||||
await store.entitycontact.update(ctx.googleAuthId, ent.id, contact.id, {
|
||||
contact: `updated-contact${i}@benchmark.test`,
|
||||
});
|
||||
}
|
||||
|
||||
// 14. Suppliers
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const sup = await store.supplier.create(ctx.googleAuthId, {
|
||||
name: `Supplier Express ${i}`,
|
||||
});
|
||||
await store.supplier.update(ctx.googleAuthId, sup.id, {
|
||||
name: `Supplier Express ${i} GmbH`,
|
||||
});
|
||||
ctx.suppliers.push(sup);
|
||||
}
|
||||
await store.supplier.list(ctx.googleAuthId);
|
||||
|
||||
// 15. Articles
|
||||
const numArticles = counts.articles;
|
||||
for (let i = 1; i <= numArticles; i++) {
|
||||
const targetCat = ctx.categories[i % ctx.categories.length];
|
||||
const isFather = i % 4 === 1;
|
||||
const art = await store.article.createUnderParent({
|
||||
googleAuthId: ctx.googleAuthId,
|
||||
parentId: targetCat.id,
|
||||
name: `Bench Article ${i}`,
|
||||
});
|
||||
|
||||
const sku = art.sku;
|
||||
ctx.articles.push(art);
|
||||
|
||||
// Update article details
|
||||
await store.article.updateBySku(ctx.googleAuthId, sku, {
|
||||
name: `Bench Article ${i} Pro`,
|
||||
unitSi: 'stk',
|
||||
unitamount: 1,
|
||||
});
|
||||
|
||||
// Prices
|
||||
await store.article.setPricesBySku(ctx.googleAuthId, sku, [
|
||||
{ price: 10 + i * 2.5, tax: 19, minmultiplier: 1 },
|
||||
{ price: 8 + i * 2.5, tax: 19, minmultiplier: 5 },
|
||||
]);
|
||||
|
||||
// EANs
|
||||
const ean = `4012345${String(i).padStart(6, '0')}`;
|
||||
await store.article.setEansBySku(ctx.googleAuthId, sku, [ean]);
|
||||
|
||||
// Brand
|
||||
await store.article.setBrandIdBySku(ctx.googleAuthId, sku, ctx.brands[i % ctx.brands.length].id);
|
||||
|
||||
// Weight
|
||||
await store.article.setWeightBySku(ctx.googleAuthId, sku, 0.25 * i);
|
||||
|
||||
// Categories
|
||||
await store.article.setCategoryIdsBySku(ctx.googleAuthId, sku, [
|
||||
targetCat.id,
|
||||
ctx.categories[(i + 1) % ctx.categories.length].id,
|
||||
]);
|
||||
|
||||
// V/K Father-Child hierarchy
|
||||
if (isFather) {
|
||||
ctx.fatherArticles.push(art);
|
||||
} else if (ctx.fatherArticles.length > 0) {
|
||||
const parentFather = ctx.fatherArticles[ctx.fatherArticles.length - 1];
|
||||
await store.article.setFatherBySku(ctx.googleAuthId, sku, parentFather.sku);
|
||||
ctx.childArticles.push(art);
|
||||
}
|
||||
|
||||
// Add image on select articles
|
||||
if (i <= 3) {
|
||||
const imgRes = await store.article.addImage(ctx.googleAuthId, sku, sampleImage);
|
||||
ctx.sampleImageHash = imgRes.fullHash;
|
||||
ctx.sampleImageThumbHash = imgRes.thumbHash;
|
||||
await store.article.orderImages(ctx.googleAuthId, sku, {
|
||||
fullHashes: [imgRes.fullHash],
|
||||
});
|
||||
}
|
||||
|
||||
// Article Suppliers
|
||||
if (ctx.suppliers.length > 0) {
|
||||
const sup = ctx.suppliers[i % ctx.suppliers.length];
|
||||
const artSup = await store.articlesupplier.create(ctx.googleAuthId, sku, {
|
||||
entityId: sup.id,
|
||||
sku: `SUP-SKU-${i}`,
|
||||
pu: 1,
|
||||
stock: 100,
|
||||
sort: 1,
|
||||
netPrice: 7.5 + i,
|
||||
taxId: ctx.company.taxrateId,
|
||||
});
|
||||
await store.articlesupplier.update(ctx.googleAuthId, sku, artSup.id, {
|
||||
stock: 120,
|
||||
pu: 2,
|
||||
});
|
||||
await store.articlesupplier.order(ctx.googleAuthId, sku, {
|
||||
ids: [artSup.id],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 16. Article Import Rows API
|
||||
const importRows = [
|
||||
{
|
||||
sku: `IMP-${Date.now()}-1`,
|
||||
name: 'Imported Keyboard Model X',
|
||||
vk: 'V',
|
||||
parentCategory: 'Hardware / Components / Processors',
|
||||
brand: 'BenchBrand Pro',
|
||||
net: '45,50',
|
||||
tax: 19,
|
||||
eans: [`4262557${String(Date.now()).slice(-6)}`],
|
||||
},
|
||||
{
|
||||
sku: `IMP-${Date.now()}-2`,
|
||||
name: 'Imported Keyboard Variant Black',
|
||||
vk: 'K',
|
||||
parentCategory: 'Hardware / Components / Processors',
|
||||
brand: 'BenchBrand Pro',
|
||||
net: '45,50',
|
||||
tax: 19,
|
||||
fatherSku: `IMP-${Date.now()}-1`,
|
||||
},
|
||||
];
|
||||
await store.article.importRows({
|
||||
googleAuthId: ctx.googleAuthId,
|
||||
rows: importRows,
|
||||
});
|
||||
|
||||
// 17. Manifests: Quotes, Orders, Deliveries
|
||||
const numManifests = counts.manifests;
|
||||
for (let i = 1; i <= numManifests; i++) {
|
||||
const ent = ctx.entities[i % ctx.entities.length];
|
||||
const sup = ctx.suppliers[i % ctx.suppliers.length];
|
||||
const art = ctx.articles[i % ctx.articles.length];
|
||||
|
||||
// Quote
|
||||
const quote = await store.quote.create(ctx.googleAuthId, {
|
||||
number: `QUO-${Date.now()}-${i}`,
|
||||
date: new Date().toISOString(),
|
||||
entityId: ent.id,
|
||||
externalNumber: `EXT-Q-${i}`,
|
||||
lines: [
|
||||
{
|
||||
sku: art.sku,
|
||||
name: `Item ${art.sku}`,
|
||||
amount: 2,
|
||||
net: 25.0,
|
||||
pu: 1,
|
||||
rebate: 0,
|
||||
taxrate: 19,
|
||||
comment: 'Standard quote item',
|
||||
},
|
||||
],
|
||||
});
|
||||
await store.quote.update(ctx.googleAuthId, quote.id, {
|
||||
externalNumber: `EXT-Q-${i}-UPDATED`,
|
||||
});
|
||||
ctx.quotes.push(quote);
|
||||
|
||||
// Order
|
||||
const order = await store.order.create(ctx.googleAuthId, {
|
||||
number: `ORD-${Date.now()}-${i}`,
|
||||
date: new Date().toISOString(),
|
||||
entityId: ent.id,
|
||||
externalNumber: `EXT-O-${i}`,
|
||||
lines: [
|
||||
{
|
||||
sku: art.sku,
|
||||
name: `Item ${art.sku}`,
|
||||
amount: 5,
|
||||
net: 25.0,
|
||||
pu: 1,
|
||||
rebate: 5,
|
||||
taxrate: 19,
|
||||
comment: 'Expedited delivery order',
|
||||
},
|
||||
],
|
||||
});
|
||||
await store.order.update(ctx.googleAuthId, order.id, {
|
||||
externalNumber: `EXT-O-${i}-CONFIRMED`,
|
||||
});
|
||||
ctx.orders.push(order);
|
||||
|
||||
// Delivery with attached files
|
||||
const delivery = await store.delivery.create(ctx.googleAuthId, {
|
||||
supplierId: sup.id,
|
||||
externalNumber: `DEL-${Date.now()}-${i}`,
|
||||
date: new Date().toISOString(),
|
||||
lines: [
|
||||
{
|
||||
sku: art.sku,
|
||||
name: `Delivery Item ${art.sku}`,
|
||||
amount: 50,
|
||||
net: 15.0,
|
||||
pu: 1,
|
||||
rebate: 0,
|
||||
taxrate: 19,
|
||||
comment: 'Warehouse receipt',
|
||||
},
|
||||
],
|
||||
files: i === 1 ? [
|
||||
{
|
||||
name: 'packing-slip.txt',
|
||||
mimetype: 'text/plain',
|
||||
content: Buffer.from(`Packing slip details for delivery ${i}`),
|
||||
},
|
||||
] : [],
|
||||
});
|
||||
await store.delivery.update(ctx.googleAuthId, delivery.id, {
|
||||
externalNumber: `DEL-${Date.now()}-${i}-STOCKED`,
|
||||
});
|
||||
ctx.deliveries.push(delivery);
|
||||
}
|
||||
|
||||
// 18. Manifest Attachments (file storage API)
|
||||
if (ctx.deliveries.length > 0) {
|
||||
const primaryManifest = ctx.deliveries[0];
|
||||
const attach1 = await store.manifestattachment.add(ctx.googleAuthId, primaryManifest.id, {
|
||||
name: 'invoice.pdf',
|
||||
mimetype: 'application/pdf',
|
||||
content: Buffer.from('%PDF-1.4 ... benchmark dummy pdf content ...'),
|
||||
thumbavif: sampleThumb,
|
||||
thumbw: 64,
|
||||
thumbh: 64,
|
||||
});
|
||||
const attach2 = await store.manifestattachment.add(ctx.googleAuthId, primaryManifest.id, {
|
||||
name: 'quality-report.txt',
|
||||
mimetype: 'text/plain',
|
||||
content: Buffer.from('Quality inspection passed successfully.'),
|
||||
});
|
||||
const currentList = await store.manifestattachment.listByManifestId(
|
||||
ctx.googleAuthId,
|
||||
primaryManifest.id,
|
||||
);
|
||||
await store.manifestattachment.order(ctx.googleAuthId, primaryManifest.id, {
|
||||
ids: currentList.map((a) => a.id).reverse(),
|
||||
});
|
||||
ctx.attachments.push({ manifestId: primaryManifest.id, ...attach1 });
|
||||
}
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes Phase 2: Filled Database Performance Benchmark.
|
||||
* Runs read, query, search, and lookup APIs against the populated database
|
||||
* over multiple iterations to measure realistic data access speeds.
|
||||
*/
|
||||
export async function benchmarkFilledDatabase(store, ctx, options = {}) {
|
||||
const iterations = options.iterations || 5;
|
||||
|
||||
for (let iter = 0; iter < iterations; iter++) {
|
||||
// 1. Category Tree & Articles
|
||||
await store.category.getTreeForCompany(ctx.googleAuthId);
|
||||
if (ctx.categories.length > 0) {
|
||||
await store.category.getCategoryArticles(ctx.googleAuthId, ctx.categories[0].id);
|
||||
}
|
||||
|
||||
// 2. Article Catalog Read APIs
|
||||
await store.article.listSearchIndex(ctx.googleAuthId);
|
||||
await store.article.listExport(ctx.googleAuthId);
|
||||
if (ctx.categories.length > 0) {
|
||||
await store.article.listExport(ctx.googleAuthId, ctx.categories[0].id);
|
||||
}
|
||||
|
||||
// Article SKU lookups & history
|
||||
if (ctx.articles.length > 0) {
|
||||
const sampleSku = ctx.articles[iter % ctx.articles.length].sku;
|
||||
await store.article.getBySku(ctx.googleAuthId, sampleSku);
|
||||
await store.article.getHistoryBySku(ctx.googleAuthId, sampleSku);
|
||||
}
|
||||
|
||||
// 3. Article Supplier Lookups
|
||||
if (ctx.articles.length > 0) {
|
||||
const sampleSku = ctx.articles[iter % ctx.articles.length].sku;
|
||||
await store.articlesupplier.listByArticleSku(ctx.googleAuthId, sampleSku);
|
||||
if (ctx.suppliers.length > 0) {
|
||||
await store.articlesupplier.listByEntityAndSkus(ctx.googleAuthId, {
|
||||
supplierId: ctx.suppliers[0].id,
|
||||
skus: [sampleSku],
|
||||
});
|
||||
await store.articlesupplier.listBySupplierAndSkus(ctx.googleAuthId, {
|
||||
supplierId: ctx.suppliers[0].id,
|
||||
skus: [sampleSku],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Pic & Thumbnail Reads
|
||||
if (ctx.sampleImageHash) {
|
||||
await store.pic.getImageByHash(ctx.googleAuthId, ctx.sampleImageHash);
|
||||
await store.pic.getThumbByHash(ctx.googleAuthId, ctx.sampleImageHash);
|
||||
}
|
||||
|
||||
// 5. Entities (Customers) Lookups
|
||||
await store.entity.list(ctx.googleAuthId);
|
||||
if (ctx.customergroups.length > 0) {
|
||||
await store.entity.list(ctx.googleAuthId, { customergroupId: ctx.customergroups[0].id });
|
||||
}
|
||||
if (ctx.entities.length > 0) {
|
||||
const sampleEnt = ctx.entities[iter % ctx.entities.length];
|
||||
await store.entity.getById(ctx.googleAuthId, sampleEnt.id);
|
||||
await store.entityaddress.list(ctx.googleAuthId, sampleEnt.id);
|
||||
await store.entitycontact.list(ctx.googleAuthId, sampleEnt.id);
|
||||
}
|
||||
|
||||
// 6. Suppliers Lookups
|
||||
await store.supplier.list(ctx.googleAuthId);
|
||||
|
||||
// 7. Manifests Lookups (Delivery, Order, Quote)
|
||||
await store.delivery.list(ctx.googleAuthId);
|
||||
if (ctx.deliveries.length > 0) {
|
||||
const sampleDel = ctx.deliveries[iter % ctx.deliveries.length];
|
||||
await store.delivery.getById(ctx.googleAuthId, sampleDel.id);
|
||||
}
|
||||
|
||||
await store.order.list(ctx.googleAuthId);
|
||||
if (ctx.orders.length > 0) {
|
||||
const sampleOrd = ctx.orders[iter % ctx.orders.length];
|
||||
await store.order.getById(ctx.googleAuthId, sampleOrd.id);
|
||||
}
|
||||
|
||||
await store.quote.list(ctx.googleAuthId);
|
||||
if (ctx.quotes.length > 0) {
|
||||
const sampleQuo = ctx.quotes[iter % ctx.quotes.length];
|
||||
await store.quote.getById(ctx.googleAuthId, sampleQuo.id);
|
||||
}
|
||||
|
||||
// 8. Manifest Attachments
|
||||
if (ctx.attachments.length > 0) {
|
||||
const sampleAttach = ctx.attachments[0];
|
||||
await store.manifestattachment.listByManifestId(ctx.googleAuthId, sampleAttach.manifestId);
|
||||
await store.manifestattachment.getById(ctx.googleAuthId, sampleAttach.manifestId, sampleAttach.id);
|
||||
}
|
||||
|
||||
// 9. Auth & Admin Queries
|
||||
await store.google_auth.getById(ctx.googleAuthId);
|
||||
await store.google_auth.getByEmail(`benchmark.admin.${ctx.googleAuthId}`); // may return undefined or auth object
|
||||
await store.companysetting.list(ctx.googleAuthId);
|
||||
await store.posterminal.list(ctx.googleAuthId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes Phase 3: Lifecycle & Deletion APIs.
|
||||
* Uses dedicated throwaway test items to exercise all delete and removal APIs
|
||||
* without destroying the core benchmarked dataset.
|
||||
*/
|
||||
export async function testLifecycleDeletions(store, ctx) {
|
||||
const sampleImage = await makeSampleImage(80, 80, { r: 180, g: 80, b: 80 });
|
||||
|
||||
// 1. Temporary Category with Image & Delete
|
||||
const tempCat = await store.category.createUnderParent({
|
||||
googleAuthId: ctx.googleAuthId,
|
||||
parentId: ctx.company.rootCategoryId,
|
||||
name: 'Temporary Cat for Delete',
|
||||
});
|
||||
await store.category.addImage(ctx.googleAuthId, tempCat.id, sampleImage);
|
||||
await store.category.deleteImage(ctx.googleAuthId, tempCat.id);
|
||||
await store.category.delete({ googleAuthId: ctx.googleAuthId, id: tempCat.id });
|
||||
|
||||
// 2. Temporary Article with Image & Delete
|
||||
const tempArt = await store.article.createUnderParent({
|
||||
googleAuthId: ctx.googleAuthId,
|
||||
parentId: ctx.categories[0].id,
|
||||
name: 'Temporary Article for Delete',
|
||||
});
|
||||
const artImg = await store.article.addImage(ctx.googleAuthId, tempArt.sku, sampleImage);
|
||||
await store.article.deleteImage(ctx.googleAuthId, tempArt.sku, artImg.fullHash);
|
||||
|
||||
// Article Supplier Delete
|
||||
if (ctx.suppliers.length > 0) {
|
||||
const tempArtSup = await store.articlesupplier.create(ctx.googleAuthId, tempArt.sku, {
|
||||
entityId: ctx.suppliers[0].id,
|
||||
sku: 'TEMP-SUP-SKU',
|
||||
});
|
||||
await store.articlesupplier.delete(ctx.googleAuthId, tempArt.sku, tempArtSup.id);
|
||||
}
|
||||
await store.article.delete(ctx.googleAuthId, tempArt.sku);
|
||||
|
||||
// 3. Manifest Attachment Remove
|
||||
if (ctx.deliveries.length > 0) {
|
||||
const tempAttach = await store.manifestattachment.add(ctx.googleAuthId, ctx.deliveries[0].id, {
|
||||
name: 'delete-me.txt',
|
||||
mimetype: 'text/plain',
|
||||
content: Buffer.from('temp file to remove'),
|
||||
});
|
||||
await store.manifestattachment.remove(ctx.googleAuthId, ctx.deliveries[0].id, tempAttach.id);
|
||||
}
|
||||
|
||||
// 4. Temporary Delivery, Order, Quote Delete
|
||||
if (ctx.suppliers.length > 0) {
|
||||
const tempDel = await store.delivery.create(ctx.googleAuthId, {
|
||||
supplierId: ctx.suppliers[0].id,
|
||||
date: new Date().toISOString(),
|
||||
lines: [{ name: 'Temp item', amount: 1, net: 10, taxrate: 19 }],
|
||||
});
|
||||
await store.delivery.delete(ctx.googleAuthId, tempDel.id);
|
||||
}
|
||||
|
||||
const tempOrd = await store.order.create(ctx.googleAuthId, {
|
||||
number: `TEMP-ORD-${Date.now()}`,
|
||||
date: new Date().toISOString(),
|
||||
lines: [{ name: 'Temp item', amount: 1, net: 10, taxrate: 19 }],
|
||||
});
|
||||
await store.order.delete(ctx.googleAuthId, tempOrd.id);
|
||||
|
||||
const tempQuo = await store.quote.create(ctx.googleAuthId, {
|
||||
number: `TEMP-QUO-${Date.now()}`,
|
||||
date: new Date().toISOString(),
|
||||
lines: [{ name: 'Temp item', amount: 1, net: 10, taxrate: 19 }],
|
||||
});
|
||||
await store.quote.delete(ctx.googleAuthId, tempQuo.id);
|
||||
|
||||
// 5. Temporary Entity with Contact & Address Delete
|
||||
const tempEnt = await store.entity.create(ctx.googleAuthId, {
|
||||
customergroupId: ctx.customergroups[0].id,
|
||||
name: 'Temporary Delete Entity',
|
||||
});
|
||||
const tempAddr = await store.entityaddress.create(ctx.googleAuthId, tempEnt.id, {
|
||||
addressType: 'billing',
|
||||
street: 'Temp St',
|
||||
});
|
||||
await store.entityaddress.delete(ctx.googleAuthId, tempEnt.id, tempAddr.id);
|
||||
|
||||
const tempContact = await store.entitycontact.create(ctx.googleAuthId, tempEnt.id, {
|
||||
contactType: 'phone',
|
||||
contact: '+4912345678',
|
||||
});
|
||||
await store.entitycontact.delete(ctx.googleAuthId, tempEnt.id, tempContact.id);
|
||||
await store.entity.delete(ctx.googleAuthId, tempEnt.id);
|
||||
|
||||
// 6. Temporary Supplier Delete
|
||||
const tempSup = await store.supplier.create(ctx.googleAuthId, { name: 'Temp Sup for Delete' });
|
||||
await store.supplier.delete(ctx.googleAuthId, tempSup.id);
|
||||
|
||||
// 7. Temporary Customergroup Delete
|
||||
const tempCg = await store.customergroup.create(ctx.googleAuthId, { name: 'Temp CG Delete' });
|
||||
await store.customergroup.delete(ctx.googleAuthId, tempCg.id);
|
||||
|
||||
// 8. Temporary Brand Delete
|
||||
const tempBrand = await store.brand.upsert(ctx.googleAuthId, { name: `TempBrand-${Date.now()}` });
|
||||
await store.brand.delete(ctx.googleAuthId, tempBrand.id);
|
||||
|
||||
// 9. Temporary Unit Delete
|
||||
const tempUnitSi = `tunit-${Date.now()}`.slice(0, 15);
|
||||
await store.unit.upsert(ctx.googleAuthId, {
|
||||
si: tempUnitSi,
|
||||
basesi: tempUnitSi,
|
||||
basesifactor: 1,
|
||||
symbol: 'tu',
|
||||
name: 'Temp Unit',
|
||||
});
|
||||
await store.unit.delete(ctx.googleAuthId, tempUnitSi);
|
||||
|
||||
// 10. Temporary Terminal Delete
|
||||
const tempTerm = await store.posterminal.upsert(ctx.googleAuthId, {
|
||||
name: 'Temp Terminal Delete',
|
||||
pairingpin: '9999',
|
||||
});
|
||||
await store.posterminal.delete(ctx.googleAuthId, tempTerm.id);
|
||||
|
||||
// 11. Company User Delete
|
||||
if (ctx.compUserId) {
|
||||
await store.companyuser.delete(ctx.compUserId);
|
||||
}
|
||||
|
||||
// 12. Allowed Email Remove
|
||||
const tempEmail = `bench.remove.${Date.now()}@example.com`;
|
||||
await store.google_auth.add(tempEmail);
|
||||
await store.google_auth.removeAllowedEmail(tempEmail);
|
||||
|
||||
// 13. Lang Delete
|
||||
const tempLangId = await store.lang.create('nl');
|
||||
await store.lang.delete(tempLangId);
|
||||
|
||||
// 14. Secondary Company Delete
|
||||
if (ctx.secondaryCompany) {
|
||||
await store.company.delete(ctx.secondaryCompany.id);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
/** Drop order = reverse of schema create dependency order. */
|
||||
export const TABLES = [
|
||||
'manifestattachment',
|
||||
'file',
|
||||
'companyuser',
|
||||
'syncpos',
|
||||
'companysetting',
|
||||
@@ -48,10 +50,12 @@ export const PROCEDURES = [
|
||||
|
||||
export const VIEWS = ['v_entity_company_number'];
|
||||
|
||||
/** Drop before tables — breaks company ↔ customergroup cycle for purge. */
|
||||
/** Drop before tables — drops all foreign keys so all tables can be purged safely. */
|
||||
export const CONSTRAINTS = [
|
||||
`IF EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = N'FK_company_poscustomergroup')
|
||||
ALTER TABLE company DROP CONSTRAINT FK_company_poscustomergroup;`,
|
||||
`DECLARE @dropFkSql NVARCHAR(MAX) = N'';
|
||||
SELECT @dropFkSql += N'ALTER TABLE ' + QUOTENAME(OBJECT_SCHEMA_NAME(parent_object_id)) + N'.' + QUOTENAME(OBJECT_NAME(parent_object_id)) + N' DROP CONSTRAINT ' + QUOTENAME(name) + N';'
|
||||
FROM sys.foreign_keys;
|
||||
IF @dropFkSql <> N'' EXEC sp_executesql @dropFkSql;`,
|
||||
];
|
||||
|
||||
export function buildPurgeSql() {
|
||||
|
||||
@@ -76,7 +76,8 @@ WHERE ean = @ean
|
||||
/** Link an article to its father article and mark the father. */
|
||||
export const UPDATE_FATHER_BY_SKU = `
|
||||
UPDATE article
|
||||
SET fatherarticle_id = @fatherArticleId
|
||||
SET fatherarticle_id = @fatherArticleId,
|
||||
fathersort = COALESCE(@fathersort, 0)
|
||||
WHERE sku = @sku
|
||||
`;
|
||||
|
||||
|
||||
@@ -53,6 +53,6 @@ WHERE id = @googleAuthId
|
||||
|
||||
export const SET_PROFILE = `
|
||||
UPDATE google_auth
|
||||
SET picture = @picture, picturebinary = @pictureBinary, sub = @sub
|
||||
SET picture = @picture, picturebinary = CONVERT(VARBINARY(MAX), @pictureBinary), sub = @sub
|
||||
WHERE id = @googleAuthId
|
||||
`;
|
||||
|
||||
@@ -283,22 +283,17 @@ export default function importRows(provider) {
|
||||
const father = await runner.query(articleQueries.GET_ID_BY_SKU, { sku: fatherSku });
|
||||
const fatherId = father.rows[0]?.articleId;
|
||||
if (fatherId && fatherId !== childId) {
|
||||
await runner.query(articleQueries.UPDATE_FATHER_BY_SKU, {
|
||||
sku,
|
||||
fatherArticleId: fatherId,
|
||||
});
|
||||
await runner.query(articleQueries.SET_IS_FATHER, {
|
||||
articleId: fatherId,
|
||||
});
|
||||
const childCount = await runner.query(getChildCountSql, {
|
||||
articleId: fatherId,
|
||||
});
|
||||
const count = childCount.rows[0].count;
|
||||
// The child's sort is its 0-based position among the father's children.
|
||||
await runner.query(articleQueries.SET_FATHER_SORT, {
|
||||
articleId: childId,
|
||||
await runner.query(articleQueries.UPDATE_FATHER_BY_SKU, {
|
||||
sku,
|
||||
fatherArticleId: fatherId,
|
||||
fathersort: count - 1,
|
||||
fathersort: count,
|
||||
});
|
||||
await runner.query(articleQueries.SET_IS_FATHER, {
|
||||
articleId: fatherId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,25 +53,19 @@ export default function setFatherBySku(provider) {
|
||||
throw new Error('Invalid father');
|
||||
}
|
||||
|
||||
await runner.query(queries.UPDATE_FATHER_BY_SKU, {
|
||||
sku,
|
||||
fatherArticleId: fatherId,
|
||||
});
|
||||
|
||||
await runner.query(queries.SET_IS_FATHER, {
|
||||
articleId: fatherId,
|
||||
});
|
||||
|
||||
const childCount = await runner.query(getChildCountSql, {
|
||||
articleId: fatherId,
|
||||
});
|
||||
const count = childCount.rows[0].count;
|
||||
|
||||
// The child's sort is its 0-based position among the father's children.
|
||||
await runner.query(queries.SET_FATHER_SORT, {
|
||||
articleId,
|
||||
await runner.query(queries.UPDATE_FATHER_BY_SKU, {
|
||||
sku,
|
||||
fatherArticleId: fatherId,
|
||||
fathersort: count - 1,
|
||||
fathersort: count,
|
||||
});
|
||||
|
||||
await runner.query(queries.SET_IS_FATHER, {
|
||||
articleId: fatherId,
|
||||
});
|
||||
|
||||
const article = await load(articleId, languageId, runner);
|
||||
|
||||
Reference in New Issue
Block a user