@takosjp/yurucommu-core 3.4.5 → 4.1.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.
@@ -0,0 +1,380 @@
1
+ import type {
2
+ ObjectStore,
3
+ ObjectStoreBody,
4
+ ObjectStoreObject,
5
+ ObjectStorePutOptions,
6
+ } from "./types.ts";
7
+
8
+ /**
9
+ * The host supplies a Fetch-compatible capability that is already scoped to
10
+ * one object bucket. The adapter never receives (or needs) an endpoint,
11
+ * bucket name, region, or credential.
12
+ */
13
+ export interface S3ObjectFetcher {
14
+ fetch(request: Request): Promise<Response>;
15
+ }
16
+
17
+ export interface S3FetchObjectStoreOptions {
18
+ /** Maximum number of bytes a GET may expose to the application. */
19
+ readonly maxObjectBytes?: number;
20
+ }
21
+
22
+ export type S3FetchObjectOperation = "put" | "get" | "delete";
23
+
24
+ /** An S3 protocol failure without response-body or endpoint details. */
25
+ export class S3FetchObjectStoreError extends Error {
26
+ constructor(
27
+ readonly operation: S3FetchObjectOperation,
28
+ readonly code: string,
29
+ readonly status?: number,
30
+ ) {
31
+ super(code);
32
+ this.name = "S3FetchObjectStoreError";
33
+ }
34
+ }
35
+
36
+ const SYNTHETIC_ORIGIN = "https://s3.invalid";
37
+ const DEFAULT_MAX_OBJECT_BYTES = 64 * 1024 * 1024;
38
+ const MAX_HEADER_VALUE_LENGTH = 8 * 1024;
39
+ const FETCH_REJECTION_CODES: Record<S3FetchObjectOperation, string> = {
40
+ put: "s3_fetcher_put_rejected",
41
+ get: "s3_fetcher_get_rejected",
42
+ delete: "s3_fetcher_delete_rejected",
43
+ };
44
+
45
+ /**
46
+ * Adapt a bucket-scoped S3 HTTP Fetcher to the core's provider-neutral object
47
+ * store contract.
48
+ *
49
+ * The URL is intentionally synthetic. A host-owned Fetcher receives the
50
+ * request and supplies the actual endpoint and credentials internally; no
51
+ * provider materialization can escape through this public adapter.
52
+ */
53
+ export function createS3FetchObjectStore(
54
+ fetcher: S3ObjectFetcher,
55
+ options: S3FetchObjectStoreOptions = {},
56
+ ): ObjectStore {
57
+ if (!fetcher || typeof fetcher.fetch !== "function") {
58
+ throw new TypeError("s3_fetcher_invalid");
59
+ }
60
+ const maxObjectBytes = options.maxObjectBytes ?? DEFAULT_MAX_OBJECT_BYTES;
61
+ assertMaxObjectBytes(maxObjectBytes);
62
+
63
+ return new S3FetchObjectStore(fetcher, maxObjectBytes);
64
+ }
65
+
66
+ class S3FetchObjectStore implements ObjectStore {
67
+ constructor(
68
+ private readonly fetcher: S3ObjectFetcher,
69
+ private readonly maxObjectBytes: number,
70
+ ) {}
71
+
72
+ async put(
73
+ key: string,
74
+ value: ObjectStoreBody,
75
+ options?: ObjectStorePutOptions,
76
+ ): Promise<void> {
77
+ const headers = new Headers();
78
+ if (options?.contentType !== undefined) {
79
+ setBoundedHeader(headers, "content-type", options.contentType, "put");
80
+ }
81
+ const byteLength = knownBodyLength(value);
82
+ if (byteLength !== undefined) {
83
+ headers.set("content-length", String(byteLength));
84
+ }
85
+
86
+ const response = await fetchFromS3(
87
+ this.fetcher,
88
+ new Request(objectUrl(key), {
89
+ method: "PUT",
90
+ headers,
91
+ body: value as BodyInit,
92
+ }),
93
+ "put",
94
+ );
95
+ await expectSuccess(response, "put");
96
+ }
97
+
98
+ async get(key: string): Promise<ObjectStoreObject | null> {
99
+ const response = await fetchFromS3(
100
+ this.fetcher,
101
+ new Request(objectUrl(key), { method: "GET" }),
102
+ "get",
103
+ );
104
+ if (response.status === 404) {
105
+ await cancelBody(response);
106
+ return null;
107
+ }
108
+ await expectSuccess(response, "get", false);
109
+
110
+ let byteLength: number | undefined;
111
+ let contentType: string | undefined;
112
+ let etag: string | undefined;
113
+ try {
114
+ byteLength = parseContentLength(
115
+ response.headers.get("content-length"),
116
+ this.maxObjectBytes,
117
+ "get",
118
+ response.status,
119
+ );
120
+ contentType = boundedHeader(
121
+ response.headers,
122
+ "content-type",
123
+ "get",
124
+ response.status,
125
+ );
126
+ etag = boundedHeader(response.headers, "etag", "get", response.status);
127
+ } catch (error) {
128
+ await cancelBody(response);
129
+ throw error;
130
+ }
131
+ return {
132
+ key,
133
+ body:
134
+ response.body === null
135
+ ? null
136
+ : boundedBody(response.body, this.maxObjectBytes, "get"),
137
+ ...(contentType === undefined ? {} : { contentType }),
138
+ ...(etag === undefined ? {} : { etag }),
139
+ ...(byteLength === undefined ? {} : { byteLength }),
140
+ };
141
+ }
142
+
143
+ async delete(key: string | readonly string[]): Promise<void> {
144
+ const isBatch = typeof key !== "string";
145
+ const keys = [...new Set(isBatch ? key : [key])];
146
+ const failures: S3FetchObjectStoreError[] = [];
147
+ for (const entry of keys) {
148
+ try {
149
+ const response = await fetchFromS3(
150
+ this.fetcher,
151
+ new Request(objectUrl(entry), { method: "DELETE" }),
152
+ "delete",
153
+ );
154
+ await expectSuccess(response, "delete");
155
+ } catch (error) {
156
+ failures.push(
157
+ error instanceof S3FetchObjectStoreError
158
+ ? error
159
+ : new S3FetchObjectStoreError(
160
+ "delete",
161
+ "s3_delete_operation_failed",
162
+ ),
163
+ );
164
+ }
165
+ }
166
+ if (failures.length === 0) return;
167
+ if (!isBatch) {
168
+ throw failures[0];
169
+ }
170
+ throw new AggregateError(failures, "s3_batch_delete_failed");
171
+ }
172
+ }
173
+
174
+ async function fetchFromS3(
175
+ fetcher: S3ObjectFetcher,
176
+ request: Request,
177
+ operation: S3FetchObjectOperation,
178
+ ): Promise<Response> {
179
+ try {
180
+ return await fetcher.fetch(request);
181
+ } catch {
182
+ // Never retain the supplied rejection as `cause` or copy any of its text:
183
+ // the host Fetcher may contain endpoint and credential diagnostics.
184
+ throw new S3FetchObjectStoreError(
185
+ operation,
186
+ FETCH_REJECTION_CODES[operation],
187
+ );
188
+ }
189
+ }
190
+
191
+ function objectUrl(key: string): string {
192
+ return `${SYNTHETIC_ORIGIN}/${key
193
+ .split("/")
194
+ .map(encodePathSegment)
195
+ .join("/")}`;
196
+ }
197
+
198
+ /** Encode every path segment, including dot segments, so URL normalization
199
+ * cannot reinterpret a user key as traversal. */
200
+ function encodePathSegment(value: string): string {
201
+ const encoded = encodeURIComponent(value).replace(
202
+ /[.!'()*]/gu,
203
+ (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`,
204
+ );
205
+ // URL parsers normalize `%2E`/`%2E%2E` path segments as traversal before a
206
+ // Fetcher sees the request. Double-escape only those complete segments so
207
+ // the host can decode the wire path without losing the key boundary.
208
+ return value === "." || value === ".."
209
+ ? encoded.replace(/%2E/gu, "%252E")
210
+ : encoded;
211
+ }
212
+
213
+ function knownBodyLength(value: ObjectStoreBody): number | undefined {
214
+ if (value instanceof Blob) return value.size;
215
+ if (value instanceof ArrayBuffer) return value.byteLength;
216
+ if (typeof value === "string") {
217
+ return new TextEncoder().encode(value).byteLength;
218
+ }
219
+ return undefined;
220
+ }
221
+
222
+ async function expectSuccess(
223
+ response: Response,
224
+ operation: S3FetchObjectOperation,
225
+ cancelSuccessfulBody = true,
226
+ ): Promise<void> {
227
+ if (response.status >= 200 && response.status < 300) {
228
+ if (cancelSuccessfulBody) await cancelBody(response);
229
+ return;
230
+ }
231
+ await cancelBody(response);
232
+ throw new S3FetchObjectStoreError(
233
+ operation,
234
+ "s3_response_unexpected_status",
235
+ response.status,
236
+ );
237
+ }
238
+
239
+ async function cancelBody(response: Response): Promise<void> {
240
+ await response.body?.cancel().catch(() => undefined);
241
+ }
242
+
243
+ function boundedHeader(
244
+ headers: Headers,
245
+ name: string,
246
+ operation: S3FetchObjectOperation,
247
+ status: number,
248
+ ): string | undefined {
249
+ const value = headers.get(name);
250
+ if (value === null || value.length === 0) return undefined;
251
+ if (value.length > MAX_HEADER_VALUE_LENGTH) {
252
+ throw new S3FetchObjectStoreError(
253
+ operation,
254
+ "s3_response_header_too_large",
255
+ status,
256
+ );
257
+ }
258
+ return value;
259
+ }
260
+
261
+ function setBoundedHeader(
262
+ headers: Headers,
263
+ name: string,
264
+ value: string,
265
+ operation: S3FetchObjectOperation,
266
+ ): void {
267
+ if (value.length > MAX_HEADER_VALUE_LENGTH) {
268
+ throw new S3FetchObjectStoreError(operation, "s3_request_header_too_large");
269
+ }
270
+ headers.set(name, value);
271
+ }
272
+
273
+ function parseContentLength(
274
+ value: string | null,
275
+ maxObjectBytes: number,
276
+ operation: S3FetchObjectOperation,
277
+ status: number,
278
+ ): number | undefined {
279
+ if (value === null) return undefined;
280
+ if (
281
+ value.length > MAX_HEADER_VALUE_LENGTH ||
282
+ !/^\d+$/u.test(value) ||
283
+ !Number.isSafeInteger(Number(value))
284
+ ) {
285
+ throw new S3FetchObjectStoreError(
286
+ operation,
287
+ "s3_response_content_length_invalid",
288
+ status,
289
+ );
290
+ }
291
+ const parsed = Number(value);
292
+ if (parsed > maxObjectBytes) {
293
+ throw new S3FetchObjectStoreError(
294
+ operation,
295
+ "s3_response_object_too_large",
296
+ status,
297
+ );
298
+ }
299
+ return parsed;
300
+ }
301
+
302
+ function boundedBody(
303
+ stream: ReadableStream<Uint8Array>,
304
+ maxObjectBytes: number,
305
+ operation: S3FetchObjectOperation,
306
+ ): ReadableStream<Uint8Array> {
307
+ const reader = stream.getReader();
308
+ let size = 0;
309
+ let finished = false;
310
+ const release = (): void => {
311
+ if (finished) return;
312
+ finished = true;
313
+ try {
314
+ reader.releaseLock();
315
+ } catch {
316
+ // The bounded adapter owns no useful diagnostic at this boundary. A
317
+ // lock-release failure after read/cancel completion must not surface a
318
+ // provider error or retain its cause.
319
+ }
320
+ };
321
+
322
+ return new ReadableStream<Uint8Array>(
323
+ {
324
+ async pull(controller) {
325
+ try {
326
+ const result = await reader.read();
327
+ if (result.done) {
328
+ controller.close();
329
+ release();
330
+ return;
331
+ }
332
+ size += result.value.byteLength;
333
+ if (size > maxObjectBytes) {
334
+ await reader
335
+ .cancel("s3_response_object_too_large")
336
+ .catch(() => undefined);
337
+ controller.error(
338
+ new S3FetchObjectStoreError(
339
+ operation,
340
+ "s3_response_object_too_large",
341
+ ),
342
+ );
343
+ release();
344
+ return;
345
+ }
346
+ controller.enqueue(result.value);
347
+ } catch {
348
+ controller.error(
349
+ new S3FetchObjectStoreError(
350
+ operation,
351
+ "s3_response_body_read_failed",
352
+ ),
353
+ );
354
+ release();
355
+ }
356
+ },
357
+ async cancel() {
358
+ try {
359
+ // Do not forward an arbitrary caller reason into a provider-owned
360
+ // stream, and never retain a provider rejection as cause/text.
361
+ await reader.cancel("s3_response_body_cancelled");
362
+ } catch {
363
+ throw new S3FetchObjectStoreError(
364
+ operation,
365
+ "s3_response_body_cancel_failed",
366
+ );
367
+ } finally {
368
+ release();
369
+ }
370
+ },
371
+ },
372
+ { highWaterMark: 0 },
373
+ );
374
+ }
375
+
376
+ function assertMaxObjectBytes(value: number): void {
377
+ if (!Number.isSafeInteger(value) || value < 1) {
378
+ throw new TypeError("s3_object_response_limit_invalid");
379
+ }
380
+ }
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Row shape for every `drizzle-orm/sqlite-proxy` lane.
3
+ *
4
+ * `sqlite-proxy` maps `rows[i][j]` POSITIONALLY onto the fields Drizzle
5
+ * compiled, so a lane is only correct while the j-th cell it hands back is the
6
+ * j-th item of the projection list that went out. Two things break that, and
7
+ * both break it SILENTLY:
8
+ *
9
+ * 1. Duplicate result-column names. Drizzle's join SQL is
10
+ *
11
+ * select "inbox"."actor_ap_id", ..., "activities"."actor_ap_id", ...
12
+ * from "inbox" inner join "activities" on ...
13
+ *
14
+ * and SQLite names both result columns `actor_ap_id`. Any driver, facade, or
15
+ * Host hop that materializes a row as a RECORD keeps one of them and every
16
+ * later field shifts by one. (`created_at` collides in the same query, and
17
+ * JavaScript's key ordering adds a second trap: an integer-like name such as
18
+ * the `1` of `select 1` sorts ahead of every other key.) So this module
19
+ * rewrites the projection list before it is sent, giving every unaliased
20
+ * item a unique, non-numeric name (`__c0`, `__c1`, ...). Drizzle never looks
21
+ * at result-column names, so the rename is invisible to it. The rewrite is
22
+ * then BACKED BY A GUARD: a row that comes back with a different number of
23
+ * columns than the statement projected throws instead of being mapped.
24
+ *
25
+ * 2. Rows with no names at all. Drizzle reads a row positionally, but
26
+ * `db.get(sql`... AS matched`)` — a raw statement with no compiled fields —
27
+ * is handed the driver's row untouched, and on D1 that is a record: call
28
+ * sites read `row.matched`. A bare array answers `undefined` to those reads,
29
+ * and several of them gate block/mute enforcement, where `undefined` reads
30
+ * as "not blocked". So the row this module builds answers to BOTH access
31
+ * styles: an array Drizzle can index, carrying its column names as
32
+ * non-enumerable properties.
33
+ *
34
+ * Lanes differ only in how they obtain the (names, values) pair — `edge.sql`
35
+ * from a record per row, the managed relational runtime from a `columns` list
36
+ * beside positional rows — so both share everything below.
37
+ */
38
+
39
+ /** A row came back with a different column count than the statement projected. */
40
+ export class ProxyColumnMismatchError extends Error {
41
+ constructor(message: string) {
42
+ super(message);
43
+ this.name = "ProxyColumnMismatchError";
44
+ }
45
+ }
46
+
47
+ const PROJECTION_ALIAS_PREFIX = "__c";
48
+
49
+ const SELECT_LIST_TERMINATORS = new Set([
50
+ "from",
51
+ "where",
52
+ "group",
53
+ "having",
54
+ "window",
55
+ "order",
56
+ "limit",
57
+ "offset",
58
+ "union",
59
+ "except",
60
+ "intersect",
61
+ ]);
62
+
63
+ interface ScannedWord {
64
+ readonly word: string;
65
+ readonly start: number;
66
+ readonly end: number;
67
+ }
68
+
69
+ interface Scan {
70
+ readonly words: readonly ScannedWord[];
71
+ readonly commas: readonly number[];
72
+ }
73
+
74
+ function isWordChar(ch: string): boolean {
75
+ return /[A-Za-z0-9_$]/.test(ch);
76
+ }
77
+
78
+ /**
79
+ * Walk the statement recording the bare words and commas that sit at
80
+ * parenthesis depth zero. Quoted identifiers, string literals, and comments are
81
+ * skipped whole, so a `,` inside `'a,b'` or a `from` inside a subquery is never
82
+ * mistaken for structure.
83
+ */
84
+ function scanTopLevel(sql: string): Scan {
85
+ const words: ScannedWord[] = [];
86
+ const commas: number[] = [];
87
+ let depth = 0;
88
+ let index = 0;
89
+ while (index < sql.length) {
90
+ const ch = sql[index]!;
91
+ if (ch === "'" || ch === '"' || ch === "`") {
92
+ index += 1;
93
+ while (index < sql.length) {
94
+ if (sql[index] === ch) {
95
+ if (sql[index + 1] === ch) index += 2;
96
+ else {
97
+ index += 1;
98
+ break;
99
+ }
100
+ } else index += 1;
101
+ }
102
+ continue;
103
+ }
104
+ if (ch === "[") {
105
+ const close = sql.indexOf("]", index + 1);
106
+ index = close === -1 ? sql.length : close + 1;
107
+ continue;
108
+ }
109
+ if (ch === "-" && sql[index + 1] === "-") {
110
+ const newline = sql.indexOf("\n", index);
111
+ index = newline === -1 ? sql.length : newline + 1;
112
+ continue;
113
+ }
114
+ if (ch === "/" && sql[index + 1] === "*") {
115
+ const close = sql.indexOf("*/", index + 2);
116
+ index = close === -1 ? sql.length : close + 2;
117
+ continue;
118
+ }
119
+ if (ch === "(") {
120
+ depth += 1;
121
+ index += 1;
122
+ continue;
123
+ }
124
+ if (ch === ")") {
125
+ depth -= 1;
126
+ index += 1;
127
+ continue;
128
+ }
129
+ if (ch === ",") {
130
+ if (depth === 0) commas.push(index);
131
+ index += 1;
132
+ continue;
133
+ }
134
+ if (isWordChar(ch) && !/[0-9]/.test(ch)) {
135
+ const start = index;
136
+ while (index < sql.length && isWordChar(sql[index]!)) index += 1;
137
+ if (depth === 0) {
138
+ words.push({
139
+ word: sql.slice(start, index).toLowerCase(),
140
+ start,
141
+ end: index,
142
+ });
143
+ }
144
+ continue;
145
+ }
146
+ if (/[0-9]/.test(ch)) {
147
+ while (index < sql.length && isWordChar(sql[index]!)) index += 1;
148
+ continue;
149
+ }
150
+ index += 1;
151
+ }
152
+ return { words, commas };
153
+ }
154
+
155
+ /** Does this projection item already carry its own `as "name"` alias? */
156
+ function hasOwnAlias(item: string): boolean {
157
+ return scanTopLevel(item).words.some((word) => word.word === "as");
158
+ }
159
+
160
+ export interface RewrittenStatement {
161
+ readonly sql: string;
162
+ /**
163
+ * How many result columns the statement projects, when that could be
164
+ * determined. `undefined` means the guard cannot check this statement — a
165
+ * `select *`, or a shape with no projection list at all.
166
+ */
167
+ readonly columns: number | undefined;
168
+ }
169
+
170
+ /**
171
+ * Give every projected column a distinct, non-numeric name.
172
+ *
173
+ * Handles the two shapes Drizzle emits: a `select` list (including one that
174
+ * follows a `with` clause, whose CTE bodies are parenthesized and therefore
175
+ * invisible to a depth-zero scan) and a `returning` list on a write. For a
176
+ * compound `select ... union select ...` only the first arm is rewritten, which
177
+ * is sufficient: SQLite takes a compound select's result-column names from its
178
+ * first arm.
179
+ *
180
+ * Anything else — a `pragma`, a `create table`, a `select *` — is returned
181
+ * untouched with no column count, because there is nothing safe to rename.
182
+ */
183
+ export function rewriteProjection(sql: string): RewrittenStatement {
184
+ const scan = scanTopLevel(sql);
185
+ const first = scan.words[0];
186
+ if (!first) return { sql, columns: undefined };
187
+
188
+ let listStart: number;
189
+ let listEnd: number;
190
+
191
+ if (first.word === "select" || first.word === "with") {
192
+ const selectAt = scan.words.findIndex((word) => word.word === "select");
193
+ if (selectAt === -1) return { sql, columns: undefined };
194
+ const select = scan.words[selectAt]!;
195
+ // The list begins right after the keyword, NOT at the next bare word: an
196
+ // item usually opens with a quoted identifier, which the scanner skips.
197
+ listStart = select.end;
198
+ let cursor = selectAt + 1;
199
+ const next = scan.words[cursor];
200
+ if (
201
+ next &&
202
+ (next.word === "distinct" || next.word === "all") &&
203
+ sql.slice(select.end, next.start).trim() === ""
204
+ ) {
205
+ listStart = next.end;
206
+ cursor += 1;
207
+ }
208
+ const terminator = scan.words
209
+ .slice(cursor)
210
+ .find((word) => SELECT_LIST_TERMINATORS.has(word.word));
211
+ listEnd = terminator ? terminator.start : sql.length;
212
+ } else {
213
+ // insert / update / delete: only a `returning` list is projected.
214
+ const returning = [...scan.words]
215
+ .reverse()
216
+ .find((word) => word.word === "returning");
217
+ if (!returning) return { sql, columns: undefined };
218
+ listStart = returning.end;
219
+ listEnd = sql.length;
220
+ }
221
+
222
+ if (listEnd <= listStart) return { sql, columns: undefined };
223
+
224
+ const boundaries = [
225
+ listStart,
226
+ ...scan.commas.filter((at) => at > listStart && at < listEnd),
227
+ listEnd,
228
+ ];
229
+ const items: { text: string; start: number; end: number }[] = [];
230
+ for (let index = 0; index + 1 < boundaries.length; index += 1) {
231
+ const start = index === 0 ? boundaries[0]! : boundaries[index]! + 1;
232
+ const end = boundaries[index + 1]!;
233
+ items.push({ text: sql.slice(start, end), start, end });
234
+ }
235
+ if (items.length === 0) return { sql, columns: undefined };
236
+
237
+ // `select *` and `"t".*` cannot take an alias, and their column count is not
238
+ // knowable from the text. Leave the whole statement alone.
239
+ if (items.some((item) => /(^|\.)\s*\*\s*$/.test(item.text.trim()))) {
240
+ return { sql, columns: undefined };
241
+ }
242
+
243
+ let out = "";
244
+ let cursor = 0;
245
+ items.forEach((item, position) => {
246
+ out += sql.slice(cursor, item.end);
247
+ if (!hasOwnAlias(item.text)) {
248
+ const trailing = item.text.length - item.text.trimEnd().length;
249
+ // Insert before the item's own trailing whitespace so the statement keeps
250
+ // its shape.
251
+ out =
252
+ out.slice(0, out.length - trailing) +
253
+ ` as "${PROJECTION_ALIAS_PREFIX}${position}"` +
254
+ out.slice(out.length - trailing);
255
+ }
256
+ cursor = item.end;
257
+ });
258
+ out += sql.slice(cursor);
259
+ return { sql: out, columns: items.length };
260
+ }
261
+
262
+ /** What a lane must remember about a statement to shape its rows back. */
263
+ export interface ProjectedStatement {
264
+ /** Which lane is speaking, so a refusal names the surface that refused. */
265
+ readonly lane: string;
266
+ /** The statement as the caller wrote it, for the refusal message. */
267
+ readonly sql: string;
268
+ /** `rewriteProjection`'s column count, or `undefined` when unknowable. */
269
+ readonly columns: number | undefined;
270
+ }
271
+
272
+ /** Array indices and `length` are the only names an array cannot also carry. */
273
+ function canCarryName(key: string): boolean {
274
+ return key !== "length" && String(Number(key)) !== key;
275
+ }
276
+
277
+ /**
278
+ * Build the row `sqlite-proxy` indexes positionally, carrying its column names.
279
+ *
280
+ * Fails closed first: if the lane came back with a different number of columns
281
+ * than the statement projected, the cells no longer line up with the fields
282
+ * Drizzle compiled, and returning them would be a silent mis-read.
283
+ */
284
+ export function positionalRow(
285
+ statement: ProjectedStatement,
286
+ columns: readonly string[],
287
+ values: readonly unknown[],
288
+ ): unknown[] {
289
+ if (statement.columns !== undefined && columns.length !== statement.columns) {
290
+ throw new ProxyColumnMismatchError(
291
+ `${statement.lane} returned ${columns.length} columns for a statement ` +
292
+ `projecting ${statement.columns}; the row cannot be mapped ` +
293
+ `positionally. Statement: ${statement.sql}`,
294
+ );
295
+ }
296
+ const row = [...values];
297
+ for (let index = 0; index < columns.length; index += 1) {
298
+ const key = columns[index]!;
299
+ if (canCarryName(key)) {
300
+ Object.defineProperty(row, key, {
301
+ value: values[index],
302
+ enumerable: false,
303
+ configurable: true,
304
+ writable: true,
305
+ });
306
+ }
307
+ }
308
+ return row;
309
+ }