@hot-updater/cloudflare 0.32.0 → 0.33.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/dist/index.cjs CHANGED
@@ -281,6 +281,11 @@ var import_lib = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((export
281
281
  parser.parsingErrorCode = error.parsingErrorCode;
282
282
  module.exports = parser;
283
283
  })))(), 1);
284
+ const buildJsonEachInClause = (columnName, values, params) => {
285
+ if (values.length === 0) return "1 = 0";
286
+ params.push(JSON.stringify(values));
287
+ return `${columnName} IN (SELECT value FROM json_each(?))`;
288
+ };
284
289
  async function resolvePage(singlePage) {
285
290
  const results = [];
286
291
  for await (const page of singlePage.iterPages()) {
@@ -304,11 +309,7 @@ function buildWhereClause(conditions) {
304
309
  clauses.push("enabled = ?");
305
310
  params.push(conditions.enabled ? 1 : 0);
306
311
  }
307
- if (conditions.id?.in) if (conditions.id.in.length === 0) clauses.push("1 = 0");
308
- else {
309
- clauses.push(`id IN (${conditions.id.in.map(() => "?").join(", ")})`);
310
- params.push(...conditions.id.in);
311
- }
312
+ if (conditions.id?.in) clauses.push(buildJsonEachInClause("id", conditions.id.in, params));
312
313
  if (conditions.id?.eq) {
313
314
  clauses.push("id = ?");
314
315
  params.push(conditions.id.eq);
@@ -335,11 +336,7 @@ function buildWhereClause(conditions) {
335
336
  clauses.push("target_app_version = ?");
336
337
  params.push(conditions.targetAppVersion);
337
338
  }
338
- if (conditions.targetAppVersionIn) if (conditions.targetAppVersionIn.length === 0) clauses.push("1 = 0");
339
- else {
340
- clauses.push(`target_app_version IN (${conditions.targetAppVersionIn.map(() => "?").join(", ")})`);
341
- params.push(...conditions.targetAppVersionIn);
342
- }
339
+ if (conditions.targetAppVersionIn) clauses.push(buildJsonEachInClause("target_app_version", conditions.targetAppVersionIn, params));
343
340
  if (conditions.fingerprintHash !== void 0) if (conditions.fingerprintHash === null) clauses.push("fingerprint_hash IS NULL");
344
341
  else {
345
342
  clauses.push("fingerprint_hash = ?");
@@ -424,13 +421,13 @@ const d1Database = (0, _hot_updater_plugin_core.createDatabasePlugin)({
424
421
  const sql = (0, import_lib.default)(`
425
422
  SELECT *
426
423
  FROM bundle_patches
427
- WHERE bundle_id IN (${bundleIds.map(() => "?").join(", ")})
424
+ WHERE bundle_id IN (SELECT value FROM json_each(?))
428
425
  ORDER BY order_index ASC, base_bundle_id ASC
429
426
  `);
430
427
  const rows = await resolvePage(await cf.d1.database.query(config.databaseId, {
431
428
  account_id: config.accountId,
432
429
  sql,
433
- params: bundleIds
430
+ params: [JSON.stringify(bundleIds)]
434
431
  }));
435
432
  for (const row of rows) {
436
433
  const current = patchMap.get(row.bundle_id) ?? [];
package/dist/index.mjs CHANGED
@@ -276,6 +276,11 @@ var import_lib = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((export
276
276
  parser.parsingErrorCode = error.parsingErrorCode;
277
277
  module.exports = parser;
278
278
  })))(), 1);
279
+ const buildJsonEachInClause = (columnName, values, params) => {
280
+ if (values.length === 0) return "1 = 0";
281
+ params.push(JSON.stringify(values));
282
+ return `${columnName} IN (SELECT value FROM json_each(?))`;
283
+ };
279
284
  async function resolvePage(singlePage) {
280
285
  const results = [];
281
286
  for await (const page of singlePage.iterPages()) {
@@ -299,11 +304,7 @@ function buildWhereClause(conditions) {
299
304
  clauses.push("enabled = ?");
300
305
  params.push(conditions.enabled ? 1 : 0);
301
306
  }
302
- if (conditions.id?.in) if (conditions.id.in.length === 0) clauses.push("1 = 0");
303
- else {
304
- clauses.push(`id IN (${conditions.id.in.map(() => "?").join(", ")})`);
305
- params.push(...conditions.id.in);
306
- }
307
+ if (conditions.id?.in) clauses.push(buildJsonEachInClause("id", conditions.id.in, params));
307
308
  if (conditions.id?.eq) {
308
309
  clauses.push("id = ?");
309
310
  params.push(conditions.id.eq);
@@ -330,11 +331,7 @@ function buildWhereClause(conditions) {
330
331
  clauses.push("target_app_version = ?");
331
332
  params.push(conditions.targetAppVersion);
332
333
  }
333
- if (conditions.targetAppVersionIn) if (conditions.targetAppVersionIn.length === 0) clauses.push("1 = 0");
334
- else {
335
- clauses.push(`target_app_version IN (${conditions.targetAppVersionIn.map(() => "?").join(", ")})`);
336
- params.push(...conditions.targetAppVersionIn);
337
- }
334
+ if (conditions.targetAppVersionIn) clauses.push(buildJsonEachInClause("target_app_version", conditions.targetAppVersionIn, params));
338
335
  if (conditions.fingerprintHash !== void 0) if (conditions.fingerprintHash === null) clauses.push("fingerprint_hash IS NULL");
339
336
  else {
340
337
  clauses.push("fingerprint_hash = ?");
@@ -419,13 +416,13 @@ const d1Database = createDatabasePlugin({
419
416
  const sql = (0, import_lib.default)(`
420
417
  SELECT *
421
418
  FROM bundle_patches
422
- WHERE bundle_id IN (${bundleIds.map(() => "?").join(", ")})
419
+ WHERE bundle_id IN (SELECT value FROM json_each(?))
423
420
  ORDER BY order_index ASC, base_bundle_id ASC
424
421
  `);
425
422
  const rows = await resolvePage(await cf.d1.database.query(config.databaseId, {
426
423
  account_id: config.accountId,
427
424
  sql,
428
- params: bundleIds
425
+ params: [JSON.stringify(bundleIds)]
429
426
  }));
430
427
  for (const row of rows) {
431
428
  const current = patchMap.get(row.bundle_id) ?? [];
@@ -3,6 +3,11 @@ let _hot_updater_js = require("@hot-updater/js");
3
3
  let _hot_updater_core = require("@hot-updater/core");
4
4
  let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
5
5
  //#region src/cloudflareWorkerDatabase.ts
6
+ const buildJsonEachInClause = (columnName, values, params) => {
7
+ if (values.length === 0) return "1 = 0";
8
+ params.push(JSON.stringify(values));
9
+ return `${columnName} IN (SELECT value FROM json_each(?))`;
10
+ };
6
11
  function buildWhereClause(conditions) {
7
12
  if (!conditions) return {
8
13
  sql: "",
@@ -22,11 +27,7 @@ function buildWhereClause(conditions) {
22
27
  clauses.push("enabled = ?");
23
28
  params.push(conditions.enabled ? 1 : 0);
24
29
  }
25
- if (conditions.id?.in) if (conditions.id.in.length === 0) clauses.push("1 = 0");
26
- else {
27
- clauses.push(`id IN (${conditions.id.in.map(() => "?").join(", ")})`);
28
- params.push(...conditions.id.in);
29
- }
30
+ if (conditions.id?.in) clauses.push(buildJsonEachInClause("id", conditions.id.in, params));
30
31
  if (conditions.id?.eq) {
31
32
  clauses.push("id = ?");
32
33
  params.push(conditions.id.eq);
@@ -53,11 +54,7 @@ function buildWhereClause(conditions) {
53
54
  clauses.push("target_app_version = ?");
54
55
  params.push(conditions.targetAppVersion);
55
56
  }
56
- if (conditions.targetAppVersionIn) if (conditions.targetAppVersionIn.length === 0) clauses.push("1 = 0");
57
- else {
58
- clauses.push(`target_app_version IN (${conditions.targetAppVersionIn.map(() => "?").join(", ")})`);
59
- params.push(...conditions.targetAppVersionIn);
60
- }
57
+ if (conditions.targetAppVersionIn) clauses.push(buildJsonEachInClause("target_app_version", conditions.targetAppVersionIn, params));
61
58
  if (conditions.fingerprintHash !== void 0) if (conditions.fingerprintHash === null) clauses.push("fingerprint_hash IS NULL");
62
59
  else {
63
60
  clauses.push("fingerprint_hash = ?");
@@ -152,9 +149,9 @@ const d1WorkerDatabase = () => (0, _hot_updater_plugin_core.createDatabasePlugin
152
149
  const rows = await queryAll(`
153
150
  SELECT *
154
151
  FROM bundle_patches
155
- WHERE bundle_id IN (${bundleIds.map(() => "?").join(", ")})
152
+ WHERE bundle_id IN (SELECT value FROM json_each(?))
156
153
  ORDER BY order_index ASC, base_bundle_id ASC
157
- `, bundleIds, context);
154
+ `, [JSON.stringify(bundleIds)], context);
158
155
  for (const row of rows) {
159
156
  const current = patchMap.get(row.bundle_id) ?? [];
160
157
  current.push(row);
@@ -2,6 +2,11 @@ import { signToken, verifyJwtSignedUrl } from "@hot-updater/js";
2
2
  import { DEFAULT_ROLLOUT_COHORT_COUNT, getAssetBaseStorageUri, getBundlePatches, getManifestFileHash, getManifestStorageUri, stripBundleArtifactMetadata } from "@hot-updater/core";
3
3
  import { calculatePagination, createDatabasePlugin, createDatabasePluginGetUpdateInfo, createRuntimeStoragePlugin } from "@hot-updater/plugin-core";
4
4
  //#region src/cloudflareWorkerDatabase.ts
5
+ const buildJsonEachInClause = (columnName, values, params) => {
6
+ if (values.length === 0) return "1 = 0";
7
+ params.push(JSON.stringify(values));
8
+ return `${columnName} IN (SELECT value FROM json_each(?))`;
9
+ };
5
10
  function buildWhereClause(conditions) {
6
11
  if (!conditions) return {
7
12
  sql: "",
@@ -21,11 +26,7 @@ function buildWhereClause(conditions) {
21
26
  clauses.push("enabled = ?");
22
27
  params.push(conditions.enabled ? 1 : 0);
23
28
  }
24
- if (conditions.id?.in) if (conditions.id.in.length === 0) clauses.push("1 = 0");
25
- else {
26
- clauses.push(`id IN (${conditions.id.in.map(() => "?").join(", ")})`);
27
- params.push(...conditions.id.in);
28
- }
29
+ if (conditions.id?.in) clauses.push(buildJsonEachInClause("id", conditions.id.in, params));
29
30
  if (conditions.id?.eq) {
30
31
  clauses.push("id = ?");
31
32
  params.push(conditions.id.eq);
@@ -52,11 +53,7 @@ function buildWhereClause(conditions) {
52
53
  clauses.push("target_app_version = ?");
53
54
  params.push(conditions.targetAppVersion);
54
55
  }
55
- if (conditions.targetAppVersionIn) if (conditions.targetAppVersionIn.length === 0) clauses.push("1 = 0");
56
- else {
57
- clauses.push(`target_app_version IN (${conditions.targetAppVersionIn.map(() => "?").join(", ")})`);
58
- params.push(...conditions.targetAppVersionIn);
59
- }
56
+ if (conditions.targetAppVersionIn) clauses.push(buildJsonEachInClause("target_app_version", conditions.targetAppVersionIn, params));
60
57
  if (conditions.fingerprintHash !== void 0) if (conditions.fingerprintHash === null) clauses.push("fingerprint_hash IS NULL");
61
58
  else {
62
59
  clauses.push("fingerprint_hash = ?");
@@ -151,9 +148,9 @@ const d1WorkerDatabase = () => createDatabasePlugin({
151
148
  const rows = await queryAll(`
152
149
  SELECT *
153
150
  FROM bundle_patches
154
- WHERE bundle_id IN (${bundleIds.map(() => "?").join(", ")})
151
+ WHERE bundle_id IN (SELECT value FROM json_each(?))
155
152
  ORDER BY order_index ASC, base_bundle_id ASC
156
- `, bundleIds, context);
153
+ `, [JSON.stringify(bundleIds)], context);
157
154
  for (const row of rows) {
158
155
  const current = patchMap.get(row.bundle_id) ?? [];
159
156
  current.push(row);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hot-updater/cloudflare",
3
3
  "type": "module",
4
- "version": "0.32.0",
4
+ "version": "0.33.0",
5
5
  "description": "React Native OTA solution for self-hosted",
6
6
  "main": "dist/index.cjs",
7
7
  "module": "dist/index.mjs",
@@ -53,11 +53,11 @@
53
53
  "cloudflare": "4.2.0",
54
54
  "hono": "4.12.9",
55
55
  "uuidv7": "^1.0.2",
56
- "@hot-updater/core": "0.32.0",
57
- "@hot-updater/js": "0.32.0",
58
- "@hot-updater/plugin-core": "0.32.0",
59
- "@hot-updater/server": "0.32.0",
60
- "@hot-updater/cli-tools": "0.32.0"
56
+ "@hot-updater/core": "0.33.0",
57
+ "@hot-updater/js": "0.33.0",
58
+ "@hot-updater/cli-tools": "0.33.0",
59
+ "@hot-updater/plugin-core": "0.33.0",
60
+ "@hot-updater/server": "0.33.0"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@cloudflare/vitest-pool-workers": "0.13.0",
@@ -74,7 +74,7 @@
74
74
  "vitest": "4.1.4",
75
75
  "wrangler": "^4.5.0",
76
76
  "xdg-app-paths": "^8.3.0",
77
- "@hot-updater/test-utils": "0.32.0"
77
+ "@hot-updater/test-utils": "0.33.0"
78
78
  },
79
79
  "scripts": {
80
80
  "build": "tsdown && pnpm build:worker",
@@ -0,0 +1,260 @@
1
+ import type { DatabasePlugin } from "@hot-updater/plugin-core";
2
+ import { beforeEach, describe, expect, it } from "vitest";
3
+
4
+ import { d1Database, type RequestEnvContext } from "./worker";
5
+
6
+ type WorkerBundleRow = {
7
+ id: string;
8
+ channel: string;
9
+ enabled: number;
10
+ should_force_update: number;
11
+ file_hash: string;
12
+ git_commit_hash: string | null;
13
+ message: string | null;
14
+ platform: "ios" | "android";
15
+ target_app_version: string | null;
16
+ storage_uri: string;
17
+ fingerprint_hash: string | null;
18
+ metadata: string;
19
+ manifest_storage_uri: string | null;
20
+ manifest_file_hash: string | null;
21
+ asset_base_storage_uri: string | null;
22
+ rollout_cohort_count: number | null;
23
+ target_cohorts: string | null;
24
+ };
25
+
26
+ type WorkerPatchRow = {
27
+ id: string;
28
+ bundle_id: string;
29
+ base_bundle_id: string;
30
+ base_file_hash: string;
31
+ patch_file_hash: string;
32
+ patch_storage_uri: string;
33
+ order_index: number | null;
34
+ };
35
+
36
+ type TestEnv = {
37
+ DB: ReturnType<typeof createD1Binding>;
38
+ JWT_SECRET: string;
39
+ BUCKET: {
40
+ get: (key: string) => Promise<{ text: () => Promise<string> } | null>;
41
+ };
42
+ };
43
+
44
+ const rows = new Map<string, WorkerBundleRow>();
45
+ const patchRows = new Map<string, WorkerPatchRow>();
46
+
47
+ const createBundleRow = (index: number): WorkerBundleRow => {
48
+ const id = `00000000-0000-0000-0000-${String(index).padStart(12, "0")}`;
49
+ return {
50
+ id,
51
+ channel: "production",
52
+ enabled: 1,
53
+ should_force_update: 0,
54
+ file_hash: `hash-${index}`,
55
+ git_commit_hash: null,
56
+ message: null,
57
+ platform: "ios",
58
+ target_app_version: `>=0.${index}.0`,
59
+ storage_uri: `r2://bucket/${id}.zip`,
60
+ fingerprint_hash: null,
61
+ metadata: "{}",
62
+ manifest_storage_uri: null,
63
+ manifest_file_hash: null,
64
+ asset_base_storage_uri: null,
65
+ rollout_cohort_count: 1000,
66
+ target_cohorts: null,
67
+ };
68
+ };
69
+
70
+ const normalizeSql = (sql: string) => sql.replace(/\s+/g, " ").trim();
71
+
72
+ const filterRows = (sql: string, params: unknown[]) => {
73
+ let filteredRows = Array.from(rows.values());
74
+ let index = 0;
75
+
76
+ if (sql.includes("channel = ?")) {
77
+ const channel = params[index++];
78
+ filteredRows = filteredRows.filter((row) => row.channel === channel);
79
+ }
80
+
81
+ if (sql.includes("platform = ?")) {
82
+ const platform = params[index++];
83
+ filteredRows = filteredRows.filter((row) => row.platform === platform);
84
+ }
85
+
86
+ if (sql.includes("enabled = ?")) {
87
+ const enabled = Number(params[index++]);
88
+ filteredRows = filteredRows.filter((row) => row.enabled === enabled);
89
+ }
90
+
91
+ if (sql.includes("id >= ?")) {
92
+ const id = String(params[index++]);
93
+ filteredRows = filteredRows.filter((row) => row.id.localeCompare(id) >= 0);
94
+ }
95
+
96
+ if (sql.includes("target_app_version IS NOT NULL")) {
97
+ filteredRows = filteredRows.filter(
98
+ (row) => row.target_app_version !== null,
99
+ );
100
+ }
101
+
102
+ const inMatch = sql.match(/target_app_version IN \(([^)]+)\)/);
103
+ if (inMatch) {
104
+ const body = inMatch[1] ?? "";
105
+ const inValues = body.includes("json_each(")
106
+ ? (JSON.parse(String(params[index++])) as unknown[])
107
+ : params.slice(index, index + (body.match(/\?/g) ?? []).length);
108
+ const values = new Set(inValues);
109
+ filteredRows = filteredRows.filter((row) =>
110
+ values.has(row.target_app_version),
111
+ );
112
+ if (!body.includes("json_each(")) {
113
+ index += inValues.length;
114
+ }
115
+ }
116
+
117
+ return { filteredRows, index };
118
+ };
119
+
120
+ function createD1Binding() {
121
+ return {
122
+ prepare(sql: string) {
123
+ return {
124
+ bind(...params: unknown[]) {
125
+ if (params.length > 100) {
126
+ throw new Error(
127
+ "D1_ERROR: too many SQL variables at offset 386: SQLITE_ERROR",
128
+ );
129
+ }
130
+
131
+ return {
132
+ async all<T>() {
133
+ const normalizedSql = normalizeSql(sql).toLowerCase();
134
+
135
+ if (
136
+ normalizedSql.startsWith(
137
+ "select target_app_version from bundles",
138
+ )
139
+ ) {
140
+ const { filteredRows } = filterRows(sql, params);
141
+ const targetAppVersions = Array.from(
142
+ new Set(
143
+ filteredRows
144
+ .map((row) => row.target_app_version)
145
+ .filter(
146
+ (targetAppVersion): targetAppVersion is string =>
147
+ targetAppVersion !== null,
148
+ ),
149
+ ),
150
+ ).map((targetAppVersion) => ({
151
+ target_app_version: targetAppVersion,
152
+ }));
153
+
154
+ return { results: targetAppVersions as T[] };
155
+ }
156
+
157
+ if (
158
+ normalizedSql.startsWith(
159
+ "select count(*) as total from bundles",
160
+ )
161
+ ) {
162
+ const { filteredRows } = filterRows(sql, params);
163
+ return { results: [{ total: filteredRows.length }] as T[] };
164
+ }
165
+
166
+ if (normalizedSql.startsWith("select * from bundles")) {
167
+ const { filteredRows, index } = filterRows(sql, params);
168
+ const limit = Number(params[index] ?? filteredRows.length);
169
+ const offset = Number(params[index + 1] ?? 0);
170
+ const result = filteredRows
171
+ .sort((left, right) => right.id.localeCompare(left.id))
172
+ .slice(offset, offset + limit);
173
+
174
+ return { results: result as T[] };
175
+ }
176
+
177
+ if (
178
+ normalizedSql.startsWith(
179
+ "select * from bundle_patches where bundle_id in",
180
+ )
181
+ ) {
182
+ const selectedBundleIds = new Set(
183
+ normalizedSql.includes("json_each")
184
+ ? (JSON.parse(String(params[0])) as unknown[]).map(String)
185
+ : params.map(String),
186
+ );
187
+ const result = Array.from(patchRows.values()).filter((row) =>
188
+ selectedBundleIds.has(row.bundle_id),
189
+ );
190
+
191
+ return { results: result as T[] };
192
+ }
193
+
194
+ throw new Error(`Unsupported SQL in D1 worker mock: ${sql}`);
195
+ },
196
+ async first<T>() {
197
+ return (await this.all<T>()).results?.[0] ?? null;
198
+ },
199
+ async run() {
200
+ return {};
201
+ },
202
+ };
203
+ },
204
+ };
205
+ },
206
+ };
207
+ }
208
+
209
+ describe("cloudflare worker d1Database", () => {
210
+ let plugin: DatabasePlugin<RequestEnvContext<TestEnv>>;
211
+ let context: RequestEnvContext<TestEnv>;
212
+
213
+ beforeEach(() => {
214
+ rows.clear();
215
+ patchRows.clear();
216
+ plugin = d1Database<RequestEnvContext<TestEnv>>()();
217
+ context = {
218
+ env: {
219
+ DB: createD1Binding(),
220
+ JWT_SECRET: "test-secret",
221
+ BUCKET: {
222
+ get: async () => null,
223
+ },
224
+ },
225
+ };
226
+ });
227
+
228
+ it("queries getUpdateInfo with 200 distinct target_app_versions without exceeding D1's 100-bind cap", async () => {
229
+ for (let index = 0; index < 200; index++) {
230
+ const row = createBundleRow(index);
231
+ rows.set(row.id, row);
232
+ }
233
+
234
+ const result = await plugin.getUpdateInfo?.(
235
+ {
236
+ appVersion: "1.0.0",
237
+ bundleId: "00000000-0000-0000-0000-000000000000",
238
+ platform: "ios",
239
+ channel: "production",
240
+ minBundleId: "00000000-0000-0000-0000-000000000000",
241
+ _updateStrategy: "appVersion",
242
+ },
243
+ context,
244
+ );
245
+
246
+ expect(result).not.toBeNull();
247
+ });
248
+
249
+ it("queries patches for 200 listed bundles without exceeding D1's 100-bind cap", async () => {
250
+ for (let index = 0; index < 200; index++) {
251
+ const row = createBundleRow(index);
252
+ rows.set(row.id, row);
253
+ }
254
+
255
+ const result = await plugin.getBundles({ limit: 200 }, context);
256
+
257
+ expect(result.data).toHaveLength(200);
258
+ expect(result.pagination.total).toBe(200);
259
+ });
260
+ });
@@ -52,6 +52,19 @@ interface BuildQueryResult {
52
52
  params: unknown[];
53
53
  }
54
54
 
55
+ const buildJsonEachInClause = (
56
+ columnName: string,
57
+ values: string[],
58
+ params: unknown[],
59
+ ) => {
60
+ if (values.length === 0) {
61
+ return "1 = 0";
62
+ }
63
+
64
+ params.push(JSON.stringify(values));
65
+ return `${columnName} IN (SELECT value FROM json_each(?))`;
66
+ };
67
+
55
68
  interface D1WorkerBundleRow {
56
69
  id: string;
57
70
  channel: string;
@@ -108,12 +121,7 @@ function buildWhereClause(
108
121
  }
109
122
 
110
123
  if (conditions.id?.in) {
111
- if (conditions.id.in.length === 0) {
112
- clauses.push("1 = 0");
113
- } else {
114
- clauses.push(`id IN (${conditions.id.in.map(() => "?").join(", ")})`);
115
- params.push(...conditions.id.in);
116
- }
124
+ clauses.push(buildJsonEachInClause("id", conditions.id.in, params));
117
125
  }
118
126
 
119
127
  if (conditions.id?.eq) {
@@ -155,16 +163,13 @@ function buildWhereClause(
155
163
  }
156
164
 
157
165
  if (conditions.targetAppVersionIn) {
158
- if (conditions.targetAppVersionIn.length === 0) {
159
- clauses.push("1 = 0");
160
- } else {
161
- clauses.push(
162
- `target_app_version IN (${conditions.targetAppVersionIn
163
- .map(() => "?")
164
- .join(", ")})`,
165
- );
166
- params.push(...conditions.targetAppVersionIn);
167
- }
166
+ clauses.push(
167
+ buildJsonEachInClause(
168
+ "target_app_version",
169
+ conditions.targetAppVersionIn,
170
+ params,
171
+ ),
172
+ );
168
173
  }
169
174
 
170
175
  if (conditions.fingerprintHash !== undefined) {
@@ -335,15 +340,14 @@ export const d1WorkerDatabase = <
335
340
  return patchMap;
336
341
  }
337
342
 
338
- const placeholders = bundleIds.map(() => "?").join(", ");
339
343
  const rows = await queryAll<D1WorkerBundlePatchRow>(
340
344
  `
341
345
  SELECT *
342
346
  FROM bundle_patches
343
- WHERE bundle_id IN (${placeholders})
347
+ WHERE bundle_id IN (SELECT value FROM json_each(?))
344
348
  ORDER BY order_index ASC, base_bundle_id ASC
345
349
  `,
346
- bundleIds,
350
+ [JSON.stringify(bundleIds)],
347
351
  context,
348
352
  );
349
353
 
@@ -64,7 +64,13 @@ const getFilteredRows = (sql: string, params: any[]) => {
64
64
  return null;
65
65
  }
66
66
 
67
- const count = (match[1]?.match(/\?/g) ?? []).length;
67
+ const body = match[1] ?? "";
68
+ if (body.includes("json_each(")) {
69
+ const values = JSON.parse(String(params[index++])) as unknown;
70
+ return Array.isArray(values) ? values : [];
71
+ }
72
+
73
+ const count = (body.match(/\?/g) ?? []).length;
68
74
  const values = params.slice(index, index + count);
69
75
  index += count;
70
76
  return values;
@@ -169,6 +175,12 @@ vi.mock("cloudflare", () => ({
169
175
  params?: any[];
170
176
  },
171
177
  ) => {
178
+ if (params.length > 100) {
179
+ throw new Error(
180
+ "D1_ERROR: too many SQL variables at offset 386: SQLITE_ERROR",
181
+ );
182
+ }
183
+
172
184
  const normalizedSql = sql.replace(/\s+/g, " ").trim().toLowerCase();
173
185
 
174
186
  if (
@@ -190,7 +202,9 @@ vi.mock("cloudflare", () => ({
190
202
  )
191
203
  ) {
192
204
  const selectedBundleIds = new Set(
193
- params.map((value) => String(value)),
205
+ normalizedSql.includes("json_each")
206
+ ? (JSON.parse(String(params[0])) as unknown[]).map(String)
207
+ : params.map((value) => String(value)),
194
208
  );
195
209
  const result = Array.from(patchRows.values())
196
210
  .filter((row) => selectedBundleIds.has(row.bundle_id))