@cavulsqa/create 2.9.3 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/bin/create.mjs +18 -25
- package/lib/templates.mjs +56 -0
- package/package.json +13 -5
- package/templates/f7-app/auto-imports.d.ts +9 -0
- package/templates/f7-app/package.json +6 -3
- package/templates/f7-app/src/env.d.ts +1 -0
- package/templates/f7-app/src/main.ts +3 -0
- package/templates/f7-app/src/plugins/bootstrapError.ts +2 -0
- package/templates/f7-app/src/plugins/recorder.plugin.ts +155 -0
- package/templates/f7-app/src/shared/recorder/capacitorSink.ts +124 -0
- package/templates/f7-app/vite.config.ts +1 -0
- package/templates/m3e-app/.claude/rules/data-fetching.md +68 -0
- package/templates/m3e-app/.claude/rules/database.md +106 -0
- package/templates/m3e-app/.claude/rules/m3e-ui.md +48 -0
- package/templates/m3e-app/.claude/rules/modules.md +43 -0
- package/templates/m3e-app/.claude/rules/native.md +60 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/SKILL.md +68 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/color.md +44 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/components.md +325 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/layout.md +42 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/motion.md +51 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/shapes-type.md +48 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/sources.md +53 -0
- package/templates/m3e-app/.claude/skills/module-architecture/SKILL.md +65 -0
- package/templates/m3e-app/.claude/skills/module-architecture/file-templates.md +178 -0
- package/templates/m3e-app/.claude/skills/reactive-data/SKILL.md +90 -0
- package/templates/m3e-app/.claude/skills/reactive-data/testing.md +55 -0
- package/templates/m3e-app/.env.example +32 -0
- package/templates/m3e-app/CLAUDE.md +93 -0
- package/templates/m3e-app/auto-imports.d.ts +851 -0
- package/templates/m3e-app/capacitor.config.ts +44 -0
- package/templates/m3e-app/components.d.ts +252 -0
- package/templates/m3e-app/index.html +16 -0
- package/templates/m3e-app/package.json +107 -0
- package/templates/m3e-app/src/App.vue +129 -0
- package/templates/m3e-app/src/app/pragmas.config.ts +35 -0
- package/templates/m3e-app/src/app/scroll.config.ts +20 -0
- package/templates/m3e-app/src/app/storage.config.ts +65 -0
- package/templates/m3e-app/src/app/tabs.ts +33 -0
- package/templates/m3e-app/src/app/theme.config.ts +51 -0
- package/templates/m3e-app/src/assets/css/app.css +18 -0
- package/templates/m3e-app/src/assets/css/base.css +53 -0
- package/templates/m3e-app/src/assets/css/layout/container-transform.css +113 -0
- package/templates/m3e-app/src/assets/css/layout/shell.css +69 -0
- package/templates/m3e-app/src/assets/css/layout/transitions.css +121 -0
- package/templates/m3e-app/src/assets/css/m3e.css +6 -0
- package/templates/m3e-app/src/assets/css/theme/framework7.css +18 -0
- package/templates/m3e-app/src/assets/css/theme/tailwind.css +365 -0
- package/templates/m3e-app/src/domains/benchmark/benchmark.dataset.ts +209 -0
- package/templates/m3e-app/src/domains/benchmark/benchmark.suite.ts +662 -0
- package/templates/m3e-app/src/domains/sales/sales.repository.ts +458 -0
- package/templates/m3e-app/src/env.d.ts +40 -0
- package/templates/m3e-app/src/locales/ar.json +1260 -0
- package/templates/m3e-app/src/locales/en.json +1260 -0
- package/templates/m3e-app/src/locales/fr.json +1260 -0
- package/templates/m3e-app/src/main.ts +41 -0
- package/templates/m3e-app/src/modules/demo/components/DemoBenchmark.vue +119 -0
- package/templates/m3e-app/src/modules/demo/components/DemoBusLog.vue +63 -0
- package/templates/m3e-app/src/modules/demo/components/DemoCreateOrderSheet.vue +203 -0
- package/templates/m3e-app/src/modules/demo/components/DemoMetricsSheet.vue +75 -0
- package/templates/m3e-app/src/modules/demo/components/DemoOrderList.vue +75 -0
- package/templates/m3e-app/src/modules/demo/components/DemoPipelineBenchmark.vue +65 -0
- package/templates/m3e-app/src/modules/demo/components/DemoStatCards.vue +71 -0
- package/templates/m3e-app/src/modules/demo/composables/useBenchmark.ts +139 -0
- package/templates/m3e-app/src/modules/demo/composables/useOrderStatus.ts +64 -0
- package/templates/m3e-app/src/modules/demo/composables/useReactiveDemo.ts +171 -0
- package/templates/m3e-app/src/modules/demo/router/routes/demo.routes.ts +22 -0
- package/templates/m3e-app/src/modules/demo/views/DemoView.vue +210 -0
- package/templates/m3e-app/src/modules/demo/views/OrderDetailView.vue +136 -0
- package/templates/m3e-app/src/modules/demo/views/OrderSearchView.vue +74 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryBlock.vue +16 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryCarouselTile.vue +71 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryCustomerForm.vue +76 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryProofOfDelivery.vue +69 -0
- package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaDay.vue +106 -0
- package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaMonth.vue +40 -0
- package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaVisitList.vue +45 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatAttachSheet.vue +106 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatContactPicker.vue +85 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatInviteComposer.vue +131 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatPollComposer.vue +148 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatRecentPhotos.vue +62 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/InputsAccount.vue +79 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/InputsTags.vue +53 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/InputsVerification.vue +73 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/PasswordStrength.vue +37 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryButtons.vue +128 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryCarousels.vue +236 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryCharts.vue +84 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryFabs.vue +70 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryInputs.vue +219 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryNavigation.vue +141 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryOverlays.vue +368 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryPickers.vue +175 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryProgress.vue +118 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryScale.vue +103 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GallerySelection.vue +219 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryShapes.vue +48 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GallerySurfaces.vue +213 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryTables.vue +185 -0
- package/templates/m3e-app/src/modules/gallery/components/surfaces/SurfacesContainerTransform.vue +46 -0
- package/templates/m3e-app/src/modules/gallery/composables/chatTeam.ts +18 -0
- package/templates/m3e-app/src/modules/gallery/composables/composeKind.ts +2 -0
- package/templates/m3e-app/src/modules/gallery/composables/currentPosition.ts +35 -0
- package/templates/m3e-app/src/modules/gallery/composables/demoOrders.ts +29 -0
- package/templates/m3e-app/src/modules/gallery/composables/featuredStories.ts +73 -0
- package/templates/m3e-app/src/modules/gallery/composables/fieldFormats.ts +36 -0
- package/templates/m3e-app/src/modules/gallery/composables/openExternal.ts +11 -0
- package/templates/m3e-app/src/modules/gallery/composables/routePlan.ts +90 -0
- package/templates/m3e-app/src/modules/gallery/composables/useAgenda.ts +61 -0
- package/templates/m3e-app/src/modules/gallery/composables/useChatCustomers.ts +31 -0
- package/templates/m3e-app/src/modules/gallery/composables/useChatDemo.ts +299 -0
- package/templates/m3e-app/src/modules/gallery/composables/useGallerySections.ts +230 -0
- package/templates/m3e-app/src/modules/gallery/composables/useLocalAttachments.ts +71 -0
- package/templates/m3e-app/src/modules/gallery/composables/useLocationShare.ts +38 -0
- package/templates/m3e-app/src/modules/gallery/composables/usePhotoScenes.ts +82 -0
- package/templates/m3e-app/src/modules/gallery/composables/useTreeDemo.ts +67 -0
- package/templates/m3e-app/src/modules/gallery/composables/wilayas.ts +67 -0
- package/templates/m3e-app/src/modules/gallery/router/routes/gallery.routes.ts +52 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryAgendaView.vue +117 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryChatView.vue +292 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryContactsView.vue +77 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryFeaturedView.vue +60 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryLoginView.vue +135 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryOnboardingView.vue +116 -0
- package/templates/m3e-app/src/modules/gallery/views/GallerySectionView.vue +27 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryTabsView.vue +135 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryView.vue +50 -0
- package/templates/m3e-app/src/modules/home/components/HomeHero.vue +57 -0
- package/templates/m3e-app/src/modules/home/composables/useHomeFeatures.ts +128 -0
- package/templates/m3e-app/src/modules/home/composables/useOpenFeature.ts +22 -0
- package/templates/m3e-app/src/modules/home/router/routes/home.routes.ts +21 -0
- package/templates/m3e-app/src/modules/home/views/FeatureDetailView.vue +66 -0
- package/templates/m3e-app/src/modules/home/views/HomeView.vue +47 -0
- package/templates/m3e-app/src/modules/settings/components/RolePalette.vue +38 -0
- package/templates/m3e-app/src/modules/settings/components/SeedSwatches.vue +41 -0
- package/templates/m3e-app/src/modules/settings/components/SettingsChoice.vue +43 -0
- package/templates/m3e-app/src/modules/settings/components/StudioPreview.vue +52 -0
- package/templates/m3e-app/src/modules/settings/router/routes/settings.routes.ts +17 -0
- package/templates/m3e-app/src/modules/settings/types.ts +7 -0
- package/templates/m3e-app/src/modules/settings/views/ColorStudioView.vue +166 -0
- package/templates/m3e-app/src/modules/settings/views/SettingsView.vue +157 -0
- package/templates/m3e-app/src/plugins/bootstrapError.ts +62 -0
- package/templates/m3e-app/src/plugins/capacitor/index.ts +14 -0
- package/templates/m3e-app/src/plugins/capacitor/useAndroidBackButton.ts +36 -0
- package/templates/m3e-app/src/plugins/capacitor/useKeyboard.ts +48 -0
- package/templates/m3e-app/src/plugins/capacitor/useSplashScreen.ts +43 -0
- package/templates/m3e-app/src/plugins/capacitor/useStatusBar.ts +16 -0
- package/templates/m3e-app/src/plugins/framework7.plugin.ts +36 -0
- package/templates/m3e-app/src/plugins/i18n.plugin.ts +87 -0
- package/templates/m3e-app/src/plugins/m3e.plugin.ts +22 -0
- package/templates/m3e-app/src/plugins/seed.plugin.ts +7 -0
- package/templates/m3e-app/src/plugins/sqlite.plugin.ts +32 -0
- package/templates/m3e-app/src/router/global/global.routes.ts +12 -0
- package/templates/m3e-app/src/router/index.ts +20 -0
- package/templates/m3e-app/src/shared/components/error/404.vue +13 -0
- package/templates/m3e-app/src/shared/components/layout/EmptyState.vue +30 -0
- package/templates/m3e-app/src/shared/components/layout/SectionHeader.vue +10 -0
- package/templates/m3e-app/src/shared/components/page/AppPage.vue +104 -0
- package/templates/m3e-app/src/shared/composables/navigation/useActiveTab.ts +39 -0
- package/templates/m3e-app/src/shared/composables/navigation/useContainerTransform.ts +117 -0
- package/templates/m3e-app/src/shared/composables/navigation/useNavigationGuard.ts +48 -0
- package/templates/m3e-app/src/shared/composables/navigation/useNavigationVisibility.ts +64 -0
- package/templates/m3e-app/src/shared/composables/navigation/useViewRouter.ts +25 -0
- package/templates/m3e-app/src/shared/composables/navigation/useWindowClass.ts +22 -0
- package/templates/m3e-app/src/shared/composables/theme/useThemeSettings.ts +155 -0
- package/templates/m3e-app/src/shared/database/candidates/capacitorSqlite.ts +36 -0
- package/templates/m3e-app/src/shared/database/candidates/index.ts +4 -0
- package/templates/m3e-app/src/shared/database/candidates/opfsSahPool.ts +25 -0
- package/templates/m3e-app/src/shared/database/candidates/types.ts +56 -0
- package/templates/m3e-app/src/shared/database/candidates/waSqlite.ts +56 -0
- package/templates/m3e-app/src/shared/database/database.ts +219 -0
- package/templates/m3e-app/src/shared/database/index.ts +3 -0
- package/templates/m3e-app/src/shared/database/migrations.ts +223 -0
- package/templates/m3e-app/src/shared/database/opfs.worker.ts +5 -0
- package/templates/m3e-app/src/shared/database/queries.ts +26 -0
- package/templates/m3e-app/src/shared/database/schema.ts +153 -0
- package/templates/m3e-app/src/shared/database/storage.ts +64 -0
- package/templates/m3e-app/src/shared/database/wa.worker.ts +5 -0
- package/templates/m3e-app/src/shared/utils/lazyRoute.ts +41 -0
- package/templates/m3e-app/src/shared/utils/resolvers/resolvers.ts +42 -0
- package/templates/m3e-app/src/shared/utils/textDirection.ts +22 -0
- package/templates/m3e-app/src/shared/utils/theme/themeSettings.ts +59 -0
- package/templates/m3e-app/src/shared/utils/tone.ts +8 -0
- package/templates/m3e-app/tests/benchmark.suite.test.ts +104 -0
- package/templates/m3e-app/tests/containerTransform.test.ts +58 -0
- package/templates/m3e-app/tests/fieldFormats.test.ts +25 -0
- package/templates/m3e-app/tests/lazyRoute.test.ts +30 -0
- package/templates/m3e-app/tests/locales.test.ts +73 -0
- package/templates/m3e-app/tests/migrations.test.ts +74 -0
- package/templates/m3e-app/tests/navigationGuard.test.ts +82 -0
- package/templates/m3e-app/tests/openDatabase.test.ts +54 -0
- package/templates/m3e-app/tests/sales.repository.test.ts +329 -0
- package/templates/m3e-app/tests/storage.test.ts +117 -0
- package/templates/m3e-app/tests/textDirection.test.ts +11 -0
- package/templates/m3e-app/tooling/linkedPackages.ts +112 -0
- package/templates/m3e-app/tsconfig.json +21 -0
- package/templates/m3e-app/vite.config.ts +144 -0
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
import { sql, type Kysely } from "kysely";
|
|
2
|
+
import { datasetRowCount, ensureDataset, SCALE } from "./benchmark.dataset";
|
|
3
|
+
import type { Database } from "@/shared/database/schema";
|
|
4
|
+
|
|
5
|
+
export interface CaseResult {
|
|
6
|
+
name: string;
|
|
7
|
+
group: "write" | "read" | "join" | "transaction" | "concurrency" | "schema" | "sync";
|
|
8
|
+
/** How many logical operations the case performed. */
|
|
9
|
+
operations: number;
|
|
10
|
+
/** Median of the per-iteration timings, which is what to compare. */
|
|
11
|
+
medianMs: number;
|
|
12
|
+
/** Slowest iteration - where a stall or a lock shows up. */
|
|
13
|
+
worstMs: number;
|
|
14
|
+
totalMs: number;
|
|
15
|
+
/** Per logical operation, so a bulk case is comparable with a single-row one. */
|
|
16
|
+
msPerOperation: number;
|
|
17
|
+
/** Rows touched or returned, so a case cannot look fast by doing nothing. */
|
|
18
|
+
rows?: number;
|
|
19
|
+
note?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SuiteResult {
|
|
23
|
+
engine: string;
|
|
24
|
+
/** The PRAGMAs in force. Two runs under different settings are not comparable. */
|
|
25
|
+
pragmas?: readonly string[];
|
|
26
|
+
at: number;
|
|
27
|
+
rowsSeeded: number;
|
|
28
|
+
/** Present only on the run that had to build the dataset. */
|
|
29
|
+
seedMs?: number;
|
|
30
|
+
cases: CaseResult[];
|
|
31
|
+
totalMs: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Median rather than mean: one GC pause skews a mean badly, and the question is what a typical
|
|
36
|
+
* operation costs.
|
|
37
|
+
*/
|
|
38
|
+
function summarise(
|
|
39
|
+
name: string,
|
|
40
|
+
group: CaseResult["group"],
|
|
41
|
+
timings: number[],
|
|
42
|
+
operationsPerIteration: number,
|
|
43
|
+
extra?: { rows?: number; note?: string },
|
|
44
|
+
): CaseResult {
|
|
45
|
+
const sorted = [...timings].sort((a, b) => a - b);
|
|
46
|
+
const median = sorted[Math.floor(sorted.length / 2)] ?? 0;
|
|
47
|
+
const total = timings.reduce((sum, ms) => sum + ms, 0);
|
|
48
|
+
const operations = timings.length * operationsPerIteration;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
name,
|
|
52
|
+
group,
|
|
53
|
+
operations,
|
|
54
|
+
medianMs: round(median),
|
|
55
|
+
worstMs: round(sorted.at(-1) ?? 0),
|
|
56
|
+
totalMs: round(total),
|
|
57
|
+
msPerOperation: round(total / Math.max(operations, 1)),
|
|
58
|
+
rows: extra?.rows,
|
|
59
|
+
note: extra?.note,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const round = (value: number) => Number(value.toFixed(3));
|
|
64
|
+
|
|
65
|
+
async function time(
|
|
66
|
+
iterations: number,
|
|
67
|
+
work: (index: number) => Promise<unknown>,
|
|
68
|
+
): Promise<{ timings: number[]; rows: number }> {
|
|
69
|
+
const timings: number[] = [];
|
|
70
|
+
let rows = 0;
|
|
71
|
+
for (let index = 0; index < iterations; index++) {
|
|
72
|
+
const started = performance.now();
|
|
73
|
+
const outcome = await work(index);
|
|
74
|
+
timings.push(performance.now() - started);
|
|
75
|
+
if (Array.isArray(outcome)) rows += outcome.length;
|
|
76
|
+
}
|
|
77
|
+
return { timings, rows };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Every shape a distribution app performs, at a cardinality where the answer is not obvious:
|
|
82
|
+
* five- and six-table joins over 40k lines, a group-by with HAVING, pagination 30k rows deep, an
|
|
83
|
+
* unindexed scan, index creation on a full table, a cascading delete, and a 1000-row transaction the
|
|
84
|
+
* size of a sync batch.
|
|
85
|
+
*
|
|
86
|
+
* The dataset is seeded once and left alone. Write cases either target rows they created themselves
|
|
87
|
+
* or clean up after the run, because a suite that shrinks its own dataset gets faster every time and
|
|
88
|
+
* the numbers stop meaning anything.
|
|
89
|
+
*/
|
|
90
|
+
export async function runBenchmark(
|
|
91
|
+
db: Kysely<Database>,
|
|
92
|
+
onProgress?: (name: string) => void,
|
|
93
|
+
): Promise<SuiteResult> {
|
|
94
|
+
const started = performance.now();
|
|
95
|
+
const cases: CaseResult[] = [];
|
|
96
|
+
const step = async (name: string, run: () => Promise<CaseResult>) => {
|
|
97
|
+
onProgress?.(name);
|
|
98
|
+
cases.push(await run());
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const seedStarted = performance.now();
|
|
102
|
+
const { seeded } = await ensureDataset(db, onProgress);
|
|
103
|
+
const seedMs = seeded ? round(performance.now() - seedStarted) : undefined;
|
|
104
|
+
const rowsSeeded = await datasetRowCount(db);
|
|
105
|
+
|
|
106
|
+
// ---- reads --------------------------------------------------------------------------------
|
|
107
|
+
await step("Point select by primary key", async () => {
|
|
108
|
+
const { timings } = await time(60, (i) =>
|
|
109
|
+
db
|
|
110
|
+
.selectFrom("bench_order_line")
|
|
111
|
+
.selectAll()
|
|
112
|
+
.where("id", "=", ((i * 617) % SCALE.orderLines) + 1)
|
|
113
|
+
.executeTakeFirst(),
|
|
114
|
+
);
|
|
115
|
+
return summarise("Point select by primary key", "read", timings, 1, {
|
|
116
|
+
note: "One row out of 40k, straight down the index",
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await step("Indexed lookup, 40k table", async () => {
|
|
121
|
+
const { timings, rows } = await time(40, (i) =>
|
|
122
|
+
db
|
|
123
|
+
.selectFrom("bench_order_line")
|
|
124
|
+
.selectAll()
|
|
125
|
+
.where("order_id", "=", ((i * 37) % SCALE.orders) + 1)
|
|
126
|
+
.execute(),
|
|
127
|
+
);
|
|
128
|
+
return summarise("Indexed lookup, 40k table", "read", timings, 1, { rows });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await step("Composite index lookup", async () => {
|
|
132
|
+
const { timings, rows } = await time(40, (i) =>
|
|
133
|
+
db
|
|
134
|
+
.selectFrom("bench_stock")
|
|
135
|
+
.selectAll()
|
|
136
|
+
.where("warehouse_id", "=", (i % SCALE.warehouses) + 1)
|
|
137
|
+
.where("product_id", "=", ((i * 13) % SCALE.products) + 1)
|
|
138
|
+
.execute(),
|
|
139
|
+
);
|
|
140
|
+
return summarise("Composite index lookup", "read", timings, 1, {
|
|
141
|
+
rows,
|
|
142
|
+
note: "Both columns, neither selective alone",
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await step("Unindexed LIKE over 5k rows", async () => {
|
|
147
|
+
const { timings, rows } = await time(15, (i) =>
|
|
148
|
+
db
|
|
149
|
+
.selectFrom("bench_customer")
|
|
150
|
+
.select(["id", "name"])
|
|
151
|
+
.where("notes", "like", `%sector ${String(i % 12)}%`)
|
|
152
|
+
.execute(),
|
|
153
|
+
);
|
|
154
|
+
return summarise("Unindexed LIKE over 5k rows", "read", timings, 1, {
|
|
155
|
+
rows,
|
|
156
|
+
note: "The search box. A full scan by construction",
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await step("Deep pagination, offset 30k", async () => {
|
|
161
|
+
const { timings, rows } = await time(15, (i) =>
|
|
162
|
+
db
|
|
163
|
+
.selectFrom("bench_order_line")
|
|
164
|
+
.selectAll()
|
|
165
|
+
.orderBy("id")
|
|
166
|
+
.limit(50)
|
|
167
|
+
.offset(30_000 + i * 100)
|
|
168
|
+
.execute(),
|
|
169
|
+
);
|
|
170
|
+
return summarise("Deep pagination, offset 30k", "read", timings, 1, {
|
|
171
|
+
rows,
|
|
172
|
+
note: "SQLite still walks the skipped rows - the cost of OFFSET",
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
await step("Sort 10k rows without an index", async () => {
|
|
177
|
+
const { timings, rows } = await time(10, () =>
|
|
178
|
+
db
|
|
179
|
+
.selectFrom("bench_order")
|
|
180
|
+
.select(["id", "total_cents"])
|
|
181
|
+
.orderBy("total_cents", "desc")
|
|
182
|
+
.limit(100)
|
|
183
|
+
.execute(),
|
|
184
|
+
);
|
|
185
|
+
return summarise("Sort 10k rows without an index", "read", timings, 1, {
|
|
186
|
+
rows,
|
|
187
|
+
note: "total_cents is unindexed on purpose",
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await step("Count 40k rows", async () => {
|
|
192
|
+
const { timings } = await time(20, () =>
|
|
193
|
+
db
|
|
194
|
+
.selectFrom("bench_order_line")
|
|
195
|
+
.select(({ fn }) => fn.countAll<number>().as("n"))
|
|
196
|
+
.executeTakeFirst(),
|
|
197
|
+
);
|
|
198
|
+
return summarise("Count 40k rows", "read", timings, 1);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// ---- joins --------------------------------------------------------------------------------
|
|
202
|
+
await step("Two-table join with aggregate", async () => {
|
|
203
|
+
const { timings, rows } = await time(10, () =>
|
|
204
|
+
db
|
|
205
|
+
.selectFrom("bench_order")
|
|
206
|
+
.innerJoin("bench_order_line", "bench_order_line.order_id", "bench_order.id")
|
|
207
|
+
.select(({ fn }) => [
|
|
208
|
+
"bench_order.status as status",
|
|
209
|
+
fn.count<number>("bench_order_line.id").as("lines"),
|
|
210
|
+
])
|
|
211
|
+
.groupBy("bench_order.status")
|
|
212
|
+
.execute(),
|
|
213
|
+
);
|
|
214
|
+
return summarise("Two-table join with aggregate", "join", timings, 1, { rows });
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
await step("Five-table join, grouped and sorted", async () => {
|
|
218
|
+
const { timings, rows } = await time(8, () =>
|
|
219
|
+
db
|
|
220
|
+
.selectFrom("bench_order_line")
|
|
221
|
+
.innerJoin("bench_order", "bench_order.id", "bench_order_line.order_id")
|
|
222
|
+
.innerJoin("bench_customer", "bench_customer.id", "bench_order.customer_id")
|
|
223
|
+
.innerJoin("bench_city", "bench_city.id", "bench_customer.city_id")
|
|
224
|
+
.innerJoin("bench_region", "bench_region.id", "bench_city.region_id")
|
|
225
|
+
.select(({ fn }) => [
|
|
226
|
+
"bench_region.name as region",
|
|
227
|
+
fn.count<number>("bench_order_line.id").as("lines"),
|
|
228
|
+
sql<number>`sum(bench_order_line.quantity * bench_order_line.unit_price_cents)`.as(
|
|
229
|
+
"revenue",
|
|
230
|
+
),
|
|
231
|
+
])
|
|
232
|
+
.groupBy("bench_region.name")
|
|
233
|
+
.orderBy("revenue", "desc")
|
|
234
|
+
.execute(),
|
|
235
|
+
);
|
|
236
|
+
return summarise("Five-table join, grouped and sorted", "join", timings, 1, {
|
|
237
|
+
rows,
|
|
238
|
+
note: "40k lines up through order, customer, city, region",
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
await step("Six-table join with HAVING", async () => {
|
|
243
|
+
const { timings, rows } = await time(8, (i) =>
|
|
244
|
+
db
|
|
245
|
+
.selectFrom("bench_order_line")
|
|
246
|
+
.innerJoin("bench_product", "bench_product.id", "bench_order_line.product_id")
|
|
247
|
+
.innerJoin("bench_category", "bench_category.id", "bench_product.category_id")
|
|
248
|
+
.innerJoin("bench_order", "bench_order.id", "bench_order_line.order_id")
|
|
249
|
+
.innerJoin("bench_customer", "bench_customer.id", "bench_order.customer_id")
|
|
250
|
+
.innerJoin("bench_city", "bench_city.id", "bench_customer.city_id")
|
|
251
|
+
.select(({ fn }) => [
|
|
252
|
+
"bench_category.name as category",
|
|
253
|
+
"bench_city.name as city",
|
|
254
|
+
fn.count<number>("bench_order_line.id").as("lines"),
|
|
255
|
+
])
|
|
256
|
+
.where("bench_order.status", "=", ["draft", "confirmed", "delivered"][i % 3]!)
|
|
257
|
+
.groupBy(["bench_category.name", "bench_city.name"])
|
|
258
|
+
.having((eb) => eb(eb.fn.count("bench_order_line.id"), ">", 2))
|
|
259
|
+
.limit(200)
|
|
260
|
+
.execute(),
|
|
261
|
+
);
|
|
262
|
+
return summarise("Six-table join with HAVING", "join", timings, 1, {
|
|
263
|
+
rows,
|
|
264
|
+
note: "Grouped on two columns across six tables",
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
await step("Correlated subquery per row", async () => {
|
|
269
|
+
const { timings, rows } = await time(8, () =>
|
|
270
|
+
db
|
|
271
|
+
.selectFrom("bench_customer")
|
|
272
|
+
.select((eb) => [
|
|
273
|
+
"bench_customer.id",
|
|
274
|
+
eb
|
|
275
|
+
.selectFrom("bench_order")
|
|
276
|
+
.select((e) => e.fn.countAll<number>().as("n"))
|
|
277
|
+
.whereRef("bench_order.customer_id", "=", "bench_customer.id")
|
|
278
|
+
.as("orders"),
|
|
279
|
+
])
|
|
280
|
+
.limit(300)
|
|
281
|
+
.execute(),
|
|
282
|
+
);
|
|
283
|
+
return summarise("Correlated subquery per row", "join", timings, 1, {
|
|
284
|
+
rows,
|
|
285
|
+
note: "300 subqueries in one statement",
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
await step("Left join finding absences", async () => {
|
|
290
|
+
const { timings, rows } = await time(8, () =>
|
|
291
|
+
db
|
|
292
|
+
.selectFrom("bench_order")
|
|
293
|
+
.leftJoin("bench_payment", "bench_payment.order_id", "bench_order.id")
|
|
294
|
+
.select(["bench_order.id"])
|
|
295
|
+
.where("bench_payment.id", "is", null)
|
|
296
|
+
.limit(500)
|
|
297
|
+
.execute(),
|
|
298
|
+
);
|
|
299
|
+
return summarise("Left join finding absences", "join", timings, 1, {
|
|
300
|
+
rows,
|
|
301
|
+
note: "Unpaid orders - what a reconciliation screen asks",
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// ---- writes -------------------------------------------------------------------------------
|
|
306
|
+
await step("Insert one row", async () => {
|
|
307
|
+
const { timings } = await time(30, (i) =>
|
|
308
|
+
db
|
|
309
|
+
.insertInto("bench_payment")
|
|
310
|
+
.values({
|
|
311
|
+
order_id: (i % SCALE.orders) + 1,
|
|
312
|
+
amount_cents: 100,
|
|
313
|
+
method: "bench-single",
|
|
314
|
+
created_at: EPOCH,
|
|
315
|
+
})
|
|
316
|
+
.execute(),
|
|
317
|
+
);
|
|
318
|
+
return summarise("Insert one row", "write", timings, 1, {
|
|
319
|
+
note: "One statement, one commit - the worst case per row",
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
await step("Insert 1000 rows in one transaction", async () => {
|
|
324
|
+
const { timings } = await time(3, (run) =>
|
|
325
|
+
db.transaction().execute(async (trx) => {
|
|
326
|
+
for (let i = 0; i < 1000; i++) {
|
|
327
|
+
await trx
|
|
328
|
+
.insertInto("bench_payment")
|
|
329
|
+
.values({
|
|
330
|
+
order_id: (i % SCALE.orders) + 1,
|
|
331
|
+
amount_cents: 1,
|
|
332
|
+
method: `bench-bulk-${String(run)}`,
|
|
333
|
+
created_at: EPOCH,
|
|
334
|
+
})
|
|
335
|
+
.execute();
|
|
336
|
+
}
|
|
337
|
+
}),
|
|
338
|
+
);
|
|
339
|
+
return summarise("Insert 1000 rows in one transaction", "write", timings, 1000, {
|
|
340
|
+
note: "One commit for 1000 rows - what a sync batch should look like",
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
await step("Multi-row insert, 150 per statement", async () => {
|
|
345
|
+
const { timings } = await time(4, (run) =>
|
|
346
|
+
db
|
|
347
|
+
.insertInto("bench_payment")
|
|
348
|
+
.values(
|
|
349
|
+
Array.from({ length: 150 }, (_, i) => ({
|
|
350
|
+
order_id: (i % SCALE.orders) + 1,
|
|
351
|
+
amount_cents: 2,
|
|
352
|
+
method: `bench-multi-${String(run)}`,
|
|
353
|
+
created_at: EPOCH,
|
|
354
|
+
})),
|
|
355
|
+
)
|
|
356
|
+
.execute(),
|
|
357
|
+
);
|
|
358
|
+
return summarise("Multi-row insert, 150 per statement", "write", timings, 150, {
|
|
359
|
+
note: "Bounded by SQLite's parameter cap, not by choice",
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
await step("Update by primary key", async () => {
|
|
364
|
+
const { timings } = await time(30, (i) =>
|
|
365
|
+
db
|
|
366
|
+
.updateTable("bench_stock")
|
|
367
|
+
.set({ quantity: 500 + i })
|
|
368
|
+
.where("id", "=", ((i * 211) % SCALE.stock) + 1)
|
|
369
|
+
.execute(),
|
|
370
|
+
);
|
|
371
|
+
return summarise("Update by primary key", "write", timings, 1);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
await step("Bulk update, 2000 rows", async () => {
|
|
375
|
+
const { timings } = await time(3, (i) =>
|
|
376
|
+
db
|
|
377
|
+
.updateTable("bench_stock")
|
|
378
|
+
.set({ quantity: 700 + i })
|
|
379
|
+
.where("warehouse_id", "=", (i % SCALE.warehouses) + 1)
|
|
380
|
+
.execute(),
|
|
381
|
+
);
|
|
382
|
+
return summarise("Bulk update, 2000 rows", "write", timings, 2000, {
|
|
383
|
+
note: "One statement over an indexed predicate",
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
await step("Update through a subquery", async () => {
|
|
388
|
+
const { timings } = await time(3, () =>
|
|
389
|
+
db
|
|
390
|
+
.updateTable("bench_order")
|
|
391
|
+
.set({
|
|
392
|
+
total_cents: sql<number>`(select coalesce(sum(quantity * unit_price_cents), 0) from bench_order_line where bench_order_line.order_id = bench_order.id)`,
|
|
393
|
+
})
|
|
394
|
+
.where("id", "<=", 500)
|
|
395
|
+
.execute(),
|
|
396
|
+
);
|
|
397
|
+
return summarise("Update through a subquery", "write", timings, 500, {
|
|
398
|
+
note: "Recomputing order totals from their lines",
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
await step("Predicate delete over 8k rows", async () => {
|
|
403
|
+
const { timings } = await time(1, () =>
|
|
404
|
+
db.deleteFrom("bench_payment").where("method", "like", "bench-%").execute(),
|
|
405
|
+
);
|
|
406
|
+
return summarise("Predicate delete over 8k rows", "write", timings, 1, {
|
|
407
|
+
note: "Removes everything the write cases added, and measures the delete",
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// ---- transactions -------------------------------------------------------------------------
|
|
412
|
+
await step("Order with lines and payment, one transaction", async () => {
|
|
413
|
+
const { timings } = await time(15, (i) =>
|
|
414
|
+
db.transaction().execute(async (trx) => {
|
|
415
|
+
const order = await trx
|
|
416
|
+
.insertInto("bench_order")
|
|
417
|
+
.values({
|
|
418
|
+
customer_id: (i % SCALE.customers) + 1,
|
|
419
|
+
reference: `BENCH-${String(i)}`,
|
|
420
|
+
status: "draft",
|
|
421
|
+
created_at: EPOCH,
|
|
422
|
+
total_cents: 0,
|
|
423
|
+
})
|
|
424
|
+
.executeTakeFirstOrThrow();
|
|
425
|
+
|
|
426
|
+
const orderId = Number(order.insertId ?? 0);
|
|
427
|
+
if (!orderId) throw new Error("the order was written but reported no id");
|
|
428
|
+
|
|
429
|
+
for (let line = 0; line < 5; line++) {
|
|
430
|
+
await trx
|
|
431
|
+
.insertInto("bench_order_line")
|
|
432
|
+
.values({
|
|
433
|
+
order_id: orderId,
|
|
434
|
+
product_id: ((i + line) % SCALE.products) + 1,
|
|
435
|
+
quantity: line + 1,
|
|
436
|
+
unit_price_cents: 1000,
|
|
437
|
+
})
|
|
438
|
+
.execute();
|
|
439
|
+
}
|
|
440
|
+
await trx
|
|
441
|
+
.insertInto("bench_payment")
|
|
442
|
+
.values({ order_id: orderId, amount_cents: 5000, method: "bench-tx", created_at: EPOCH })
|
|
443
|
+
.execute();
|
|
444
|
+
}),
|
|
445
|
+
);
|
|
446
|
+
return summarise("Order with lines and payment, one transaction", "transaction", timings, 7, {
|
|
447
|
+
note: "Needs insertId inside a transaction - saving an order",
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
await step("Cascading delete", async () => {
|
|
452
|
+
const { timings } = await time(1, () =>
|
|
453
|
+
db.deleteFrom("bench_order").where("reference", "like", "BENCH-%").execute(),
|
|
454
|
+
);
|
|
455
|
+
return summarise("Cascading delete", "transaction", timings, 15, {
|
|
456
|
+
note: "Orders with their lines and payments, restoring the dataset",
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
await step("Rollback", async () => {
|
|
461
|
+
const { timings } = await time(10, (i) =>
|
|
462
|
+
db
|
|
463
|
+
.transaction()
|
|
464
|
+
.execute(async (trx) => {
|
|
465
|
+
await trx
|
|
466
|
+
.insertInto("bench_payment")
|
|
467
|
+
.values({
|
|
468
|
+
order_id: (i % SCALE.orders) + 1,
|
|
469
|
+
amount_cents: 1,
|
|
470
|
+
method: "bench-rollback",
|
|
471
|
+
created_at: EPOCH,
|
|
472
|
+
})
|
|
473
|
+
.execute();
|
|
474
|
+
throw new Error("rollback");
|
|
475
|
+
})
|
|
476
|
+
.catch(() => undefined),
|
|
477
|
+
);
|
|
478
|
+
return summarise("Rollback", "transaction", timings, 1, {
|
|
479
|
+
note: "An abandoned write costs something too",
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// ---- schema -------------------------------------------------------------------------------
|
|
484
|
+
await step("Create an index on 40k rows", async () => {
|
|
485
|
+
const { timings } = await time(3, async () => {
|
|
486
|
+
await sql.raw("DROP INDEX IF EXISTS idx_bench_line_qty").execute(db);
|
|
487
|
+
await sql.raw("CREATE INDEX idx_bench_line_qty ON bench_order_line(quantity)").execute(db);
|
|
488
|
+
return null;
|
|
489
|
+
});
|
|
490
|
+
await sql.raw("DROP INDEX IF EXISTS idx_bench_line_qty").execute(db);
|
|
491
|
+
return summarise("Create an index on 40k rows", "schema", timings, 1, {
|
|
492
|
+
note: "What a migration costs on a table that is already full",
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
await step("ANALYZE", async () => {
|
|
497
|
+
const { timings } = await time(2, () => sql.raw("ANALYZE").execute(db));
|
|
498
|
+
return summarise("ANALYZE", "schema", timings, 1, {
|
|
499
|
+
note: "The statistics the planner uses to pick an index",
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// ---- concurrency --------------------------------------------------------------------------
|
|
504
|
+
const heavyRead = () =>
|
|
505
|
+
db
|
|
506
|
+
.selectFrom("bench_order_line")
|
|
507
|
+
.innerJoin("bench_order", "bench_order.id", "bench_order_line.order_id")
|
|
508
|
+
.select(({ fn }) => [fn.count<number>("bench_order_line.id").as("n")])
|
|
509
|
+
.where("bench_order.status", "=", "confirmed")
|
|
510
|
+
.execute();
|
|
511
|
+
|
|
512
|
+
await step("Five heavy reads, issued together", async () => {
|
|
513
|
+
const { timings } = await time(8, () =>
|
|
514
|
+
Promise.all([heavyRead(), heavyRead(), heavyRead(), heavyRead(), heavyRead()]),
|
|
515
|
+
);
|
|
516
|
+
return summarise("Five heavy reads, issued together", "concurrency", timings, 5, {
|
|
517
|
+
note: "What a screen loading with Promise.all pays",
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
await step("Five heavy reads, one at a time", async () => {
|
|
522
|
+
const { timings } = await time(8, async () => {
|
|
523
|
+
for (let i = 0; i < 5; i++) await heavyRead();
|
|
524
|
+
return null;
|
|
525
|
+
});
|
|
526
|
+
return summarise("Five heavy reads, one at a time", "concurrency", timings, 5, {
|
|
527
|
+
note: "The same work awaited in a loop",
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
await step("Read while a 1000-row write is in flight", async () => {
|
|
532
|
+
const { timings } = await time(5, (run) => {
|
|
533
|
+
const write = db.transaction().execute(async (trx) => {
|
|
534
|
+
for (let i = 0; i < 1000; i++) {
|
|
535
|
+
await trx
|
|
536
|
+
.insertInto("bench_payment")
|
|
537
|
+
.values({
|
|
538
|
+
order_id: (i % SCALE.orders) + 1,
|
|
539
|
+
amount_cents: 3,
|
|
540
|
+
method: `bench-mixed-${String(run)}`,
|
|
541
|
+
created_at: EPOCH,
|
|
542
|
+
})
|
|
543
|
+
.execute();
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
return Promise.all([write, heavyRead()]);
|
|
547
|
+
});
|
|
548
|
+
return summarise("Read while a 1000-row write is in flight", "concurrency", timings, 1, {
|
|
549
|
+
note: "Where a lock that serialises reads behind a sync would show up",
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// ---- sync strategies ----------------------------------------------------------------------
|
|
554
|
+
/**
|
|
555
|
+
* The number that decides whether a sync freezes the UI: how long a read waits when it arrives
|
|
556
|
+
* while one is running.
|
|
557
|
+
*
|
|
558
|
+
* Every engine here funnels through one serial worker, so a read cannot overtake a write already
|
|
559
|
+
* in flight - it waits for whatever is queued ahead of it. That makes the question not "how fast is
|
|
560
|
+
* the write" but "how much work did the write commit to before the read arrived", and the three
|
|
561
|
+
* strategies below answer it very differently for the same 1000 rows.
|
|
562
|
+
*/
|
|
563
|
+
const readLatencyDuring = async (write: () => Promise<unknown>): Promise<number> => {
|
|
564
|
+
const running = write();
|
|
565
|
+
// Long enough that the write is genuinely in flight rather than merely constructed.
|
|
566
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
567
|
+
|
|
568
|
+
const started = performance.now();
|
|
569
|
+
await heavyRead();
|
|
570
|
+
const waited = performance.now() - started;
|
|
571
|
+
|
|
572
|
+
await running;
|
|
573
|
+
return waited;
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
const syncRow = (run: number, index: number) => ({
|
|
577
|
+
order_id: (index % SCALE.orders) + 1,
|
|
578
|
+
amount_cents: 4,
|
|
579
|
+
method: `bench-sync-${String(run)}`,
|
|
580
|
+
created_at: EPOCH,
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
await step("Sync 1000 rows, row by row in one transaction", async () => {
|
|
584
|
+
const timings: number[] = [];
|
|
585
|
+
for (let run = 0; run < 3; run++) {
|
|
586
|
+
timings.push(
|
|
587
|
+
await readLatencyDuring(() =>
|
|
588
|
+
db.transaction().execute(async (trx) => {
|
|
589
|
+
for (let i = 0; i < 1000; i++) {
|
|
590
|
+
await trx.insertInto("bench_payment").values(syncRow(run, i)).execute();
|
|
591
|
+
}
|
|
592
|
+
}),
|
|
593
|
+
),
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
return summarise("Sync 1000 rows, row by row in one transaction", "sync", timings, 1, {
|
|
597
|
+
note: "The read waits for all 1000 inserts. What a naive sync loop costs the UI",
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
await step("Sync 1000 rows, 150 per statement", async () => {
|
|
602
|
+
const timings: number[] = [];
|
|
603
|
+
for (let run = 0; run < 3; run++) {
|
|
604
|
+
timings.push(
|
|
605
|
+
await readLatencyDuring(() =>
|
|
606
|
+
db.transaction().execute(async (trx) => {
|
|
607
|
+
for (let offset = 0; offset < 1000; offset += 150) {
|
|
608
|
+
await trx
|
|
609
|
+
.insertInto("bench_payment")
|
|
610
|
+
.values(
|
|
611
|
+
Array.from({ length: Math.min(150, 1000 - offset) }, (_, i) =>
|
|
612
|
+
syncRow(run + 10, offset + i),
|
|
613
|
+
),
|
|
614
|
+
)
|
|
615
|
+
.execute();
|
|
616
|
+
}
|
|
617
|
+
}),
|
|
618
|
+
),
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
return summarise("Sync 1000 rows, 150 per statement", "sync", timings, 1, {
|
|
622
|
+
note: "Same transaction, seven statements instead of a thousand",
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
await step("Sync 1000 rows, ten transactions of 100", async () => {
|
|
627
|
+
const timings: number[] = [];
|
|
628
|
+
for (let run = 0; run < 3; run++) {
|
|
629
|
+
timings.push(
|
|
630
|
+
await readLatencyDuring(async () => {
|
|
631
|
+
for (let chunk = 0; chunk < 10; chunk++) {
|
|
632
|
+
await db.transaction().execute(async (trx) => {
|
|
633
|
+
await trx
|
|
634
|
+
.insertInto("bench_payment")
|
|
635
|
+
.values(Array.from({ length: 100 }, (_, i) => syncRow(run + 20, chunk * 100 + i)))
|
|
636
|
+
.execute();
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
}),
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
return summarise("Sync 1000 rows, ten transactions of 100", "sync", timings, 1, {
|
|
643
|
+
note: "Awaited one chunk at a time, so a read queues behind one chunk and not the batch",
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
// Everything this run added, gone - so the next run starts from the same dataset.
|
|
648
|
+
await db.deleteFrom("bench_payment").where("method", "like", "bench-%").execute();
|
|
649
|
+
await db.deleteFrom("bench_order").where("reference", "like", "BENCH-%").execute();
|
|
650
|
+
|
|
651
|
+
return {
|
|
652
|
+
engine: "",
|
|
653
|
+
at: 0,
|
|
654
|
+
rowsSeeded,
|
|
655
|
+
seedMs,
|
|
656
|
+
cases,
|
|
657
|
+
totalMs: round(performance.now() - started),
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/** Fixed, so a row's timestamp never varies between runs. */
|
|
662
|
+
const EPOCH = "1970-01-01T00:00:00.000Z";
|