tradeblocks-mcp 3.3.1 → 3.5.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/{chunk-4HZLVUOZ.js → chunk-5HBZ3TZM.js} +2350 -336
- package/dist/chunk-5HBZ3TZM.js.map +1 -0
- package/dist/{chunk-NWNSKALN.js → chunk-DHHEGI3Q.js} +15 -8
- package/dist/chunk-DHHEGI3Q.js.map +1 -0
- package/dist/{chunk-2I3S2ZLT.js → chunk-GD3STARM.js} +278 -45
- package/dist/chunk-GD3STARM.js.map +1 -0
- package/dist/market/provenance/index.d.ts +644 -0
- package/dist/market/provenance/index.js +23067 -0
- package/dist/market/provenance/index.js.map +1 -0
- package/dist/{market-provider-6U33TQUT.js → market-provider-JJIJ7QMU.js} +3 -3
- package/dist/{sync-3UBC37VW.js → sync-AWI764HE.js} +3 -3
- package/dist/test-exports.js +4174 -549
- package/dist/test-exports.js.map +1 -1
- package/package.json +8 -2
- package/server/{chunk-RM4B2VKS.js → chunk-4BY4JOI3.js} +2300 -370
- package/server/chunk-4BY4JOI3.js.map +1 -0
- package/server/{chunk-RJDJHZ5Y.js → chunk-65OJTDFW.js} +276 -45
- package/server/chunk-65OJTDFW.js.map +1 -0
- package/server/{chunk-OMUDNJBJ.js → chunk-KP4NYKL5.js} +15 -8
- package/server/chunk-KP4NYKL5.js.map +1 -0
- package/server/index.d.ts +37 -21
- package/server/index.js +3780 -477
- package/server/index.js.map +1 -1
- package/server/{market-provider-L56RFHAC.js → market-provider-DIPCCFJU.js} +3 -3
- package/server/{sync-CENKDN53.js → sync-2SFFAZ67.js} +3 -3
- package/src/db/connection.ts +142 -3
- package/src/db/index.ts +1 -0
- package/src/db/market-datasets.ts +208 -24
- package/src/db/market-views.ts +28 -11
- package/src/db/parquet-writer.ts +234 -11
- package/src/market/ingestor/index.ts +1 -0
- package/src/market/ingestor/market-ingestor.ts +7 -0
- package/src/market/ingestor/types.ts +18 -0
- package/src/market/provenance/canonical-json.ts +119 -0
- package/src/market/provenance/canonical-market-resolver.ts +801 -0
- package/src/market/provenance/content-manifest.ts +1934 -0
- package/src/market/provenance/content-object-store.ts +179 -0
- package/src/market/provenance/dataset-registry.ts +144 -0
- package/src/market/provenance/index.ts +137 -0
- package/src/market/provenance/partition-commit-attempt.ts +89 -0
- package/src/market/provenance/partition-commit-store.ts +1847 -0
- package/src/market/provenance/rate-slices.ts +60 -0
- package/src/market/provenance/refresh-completion.ts +989 -0
- package/src/market/provenance/xnys-session-calendar.ts +170 -0
- package/src/market/stores/coverage.ts +48 -0
- package/src/market/stores/duckdb-enriched-store.ts +26 -5
- package/src/market/stores/duckdb-quote-store.ts +29 -18
- package/src/market/stores/enriched-legacy-migration.ts +297 -0
- package/src/market/stores/enriched-sql.ts +19 -3
- package/src/market/stores/enriched-store.ts +16 -2
- package/src/market/stores/index.ts +143 -14
- package/src/market/stores/parquet-chain-store.ts +4 -1
- package/src/market/stores/parquet-enriched-store.ts +162 -41
- package/src/market/stores/parquet-oi-daily-store.ts +3 -3
- package/src/market/stores/parquet-quote-store.ts +37 -24
- package/src/market/stores/parquet-spot-store.ts +14 -10
- package/src/market/stores/quote-store.ts +5 -0
- package/src/market/stores/rth-aggregation.ts +4 -2
- package/src/market/stores/spot-store.ts +75 -6
- package/src/market/stores/types.ts +27 -0
- package/src/test-exports.ts +154 -2
- package/src/tools/market-enrichment.ts +2 -4
- package/src/utils/market-enricher.ts +249 -66
- package/src/utils/option-quote-greeks.ts +20 -8
- package/src/utils/providers/thetadata/bulk-roots.ts +10 -0
- package/src/utils/providers/thetadata.ts +5 -17
- package/src/utils/quote-parquet-projection.ts +50 -1
- package/dist/chunk-2I3S2ZLT.js.map +0 -1
- package/dist/chunk-4HZLVUOZ.js.map +0 -1
- package/dist/chunk-NWNSKALN.js.map +0 -1
- package/server/chunk-OMUDNJBJ.js.map +0 -1
- package/server/chunk-RJDJHZ5Y.js.map +0 -1
- package/server/chunk-RM4B2VKS.js.map +0 -1
- /package/dist/{market-provider-6U33TQUT.js.map → market-provider-JJIJ7QMU.js.map} +0 -0
- /package/dist/{sync-3UBC37VW.js.map → sync-AWI764HE.js.map} +0 -0
- /package/server/{market-provider-L56RFHAC.js.map → market-provider-DIPCCFJU.js.map} +0 -0
- /package/server/{sync-CENKDN53.js.map → sync-2SFFAZ67.js.map} +0 -0
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
2
|
REPORTING_TRADE_COLUMN_ALIASES,
|
|
3
3
|
convertTatRowToReportingTrade,
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
describeReadParquetColumns,
|
|
5
|
+
isTatFormat,
|
|
6
|
+
quoteParquetCanonicalProjection,
|
|
7
|
+
readParquetGlobSql
|
|
8
|
+
} from "./chunk-GD3STARM.js";
|
|
6
9
|
|
|
7
10
|
// src/sync/index.ts
|
|
8
11
|
import { existsSync as existsSync2 } from "fs";
|
|
9
|
-
import * as
|
|
10
|
-
import * as
|
|
12
|
+
import * as fs11 from "fs/promises";
|
|
13
|
+
import * as path15 from "path";
|
|
11
14
|
|
|
12
15
|
// src/db/connection.ts
|
|
13
16
|
import { DuckDBInstance } from "@duckdb/node-api";
|
|
14
17
|
import { execFile } from "child_process";
|
|
15
|
-
import * as
|
|
18
|
+
import * as fs6 from "fs/promises";
|
|
16
19
|
import * as os from "os";
|
|
17
|
-
import * as
|
|
20
|
+
import * as path11 from "path";
|
|
18
21
|
import { promisify } from "util";
|
|
19
22
|
|
|
20
23
|
// src/db/schemas.ts
|
|
@@ -117,109 +120,1867 @@ async function ensureReportingDataTable(conn) {
|
|
|
117
120
|
await conn.run(`ALTER TABLE trades.reporting_data ADD COLUMN ticker VARCHAR`);
|
|
118
121
|
}
|
|
119
122
|
}
|
|
120
|
-
|
|
121
|
-
// src/db/market-schemas.ts
|
|
122
|
-
async function ensureMutableMarketTables(conn) {
|
|
123
|
-
await conn.run(`
|
|
124
|
-
CREATE TABLE IF NOT EXISTS market._sync_metadata (
|
|
125
|
-
source VARCHAR NOT NULL,
|
|
126
|
-
ticker VARCHAR NOT NULL,
|
|
127
|
-
target_table VARCHAR NOT NULL,
|
|
128
|
-
|
|
129
|
-
content_hash VARCHAR,
|
|
130
|
-
max_date VARCHAR,
|
|
131
|
-
wilder_state JSON,
|
|
132
|
-
synced_at TIMESTAMP NOT NULL,
|
|
133
|
-
|
|
134
|
-
PRIMARY KEY (source, ticker, target_table)
|
|
135
|
-
)
|
|
136
|
-
`);
|
|
137
|
-
}
|
|
138
|
-
async function ensureMarketDataTables(conn) {
|
|
139
|
-
for (const name of ["daily", "date_context", "intraday", "data_coverage"]) {
|
|
140
|
-
try {
|
|
141
|
-
await conn.run(`DROP VIEW IF EXISTS market.${name}`);
|
|
142
|
-
} catch {
|
|
143
|
-
}
|
|
144
|
-
try {
|
|
145
|
-
await conn.run(`DROP TABLE IF EXISTS market.${name}`);
|
|
146
|
-
} catch {
|
|
147
|
-
}
|
|
123
|
+
|
|
124
|
+
// src/db/market-schemas.ts
|
|
125
|
+
async function ensureMutableMarketTables(conn) {
|
|
126
|
+
await conn.run(`
|
|
127
|
+
CREATE TABLE IF NOT EXISTS market._sync_metadata (
|
|
128
|
+
source VARCHAR NOT NULL,
|
|
129
|
+
ticker VARCHAR NOT NULL,
|
|
130
|
+
target_table VARCHAR NOT NULL,
|
|
131
|
+
|
|
132
|
+
content_hash VARCHAR,
|
|
133
|
+
max_date VARCHAR,
|
|
134
|
+
wilder_state JSON,
|
|
135
|
+
synced_at TIMESTAMP NOT NULL,
|
|
136
|
+
|
|
137
|
+
PRIMARY KEY (source, ticker, target_table)
|
|
138
|
+
)
|
|
139
|
+
`);
|
|
140
|
+
}
|
|
141
|
+
async function ensureMarketDataTables(conn) {
|
|
142
|
+
for (const name of ["daily", "date_context", "intraday", "data_coverage"]) {
|
|
143
|
+
try {
|
|
144
|
+
await conn.run(`DROP VIEW IF EXISTS market.${name}`);
|
|
145
|
+
} catch {
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
await conn.run(`DROP TABLE IF EXISTS market.${name}`);
|
|
149
|
+
} catch {
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
await conn.run(`
|
|
153
|
+
CREATE TABLE IF NOT EXISTS market.spot (
|
|
154
|
+
ticker VARCHAR NOT NULL,
|
|
155
|
+
date VARCHAR NOT NULL,
|
|
156
|
+
time VARCHAR NOT NULL,
|
|
157
|
+
open DOUBLE,
|
|
158
|
+
high DOUBLE,
|
|
159
|
+
low DOUBLE,
|
|
160
|
+
close DOUBLE,
|
|
161
|
+
bid DOUBLE,
|
|
162
|
+
ask DOUBLE,
|
|
163
|
+
PRIMARY KEY (ticker, date, time)
|
|
164
|
+
)
|
|
165
|
+
`);
|
|
166
|
+
await conn.run(`
|
|
167
|
+
CREATE TABLE IF NOT EXISTS market.enriched (
|
|
168
|
+
ticker VARCHAR NOT NULL,
|
|
169
|
+
date VARCHAR NOT NULL,
|
|
170
|
+
Prior_Close DOUBLE,
|
|
171
|
+
Gap_Pct DOUBLE,
|
|
172
|
+
ATR_Pct DOUBLE,
|
|
173
|
+
RSI_14 DOUBLE,
|
|
174
|
+
Price_vs_EMA21_Pct DOUBLE,
|
|
175
|
+
Price_vs_SMA50_Pct DOUBLE,
|
|
176
|
+
Realized_Vol_5D DOUBLE,
|
|
177
|
+
Realized_Vol_20D DOUBLE,
|
|
178
|
+
Return_5D DOUBLE,
|
|
179
|
+
Return_20D DOUBLE,
|
|
180
|
+
Intraday_Range_Pct DOUBLE,
|
|
181
|
+
Intraday_Return_Pct DOUBLE,
|
|
182
|
+
Close_Position_In_Range DOUBLE,
|
|
183
|
+
Gap_Filled INTEGER,
|
|
184
|
+
Consecutive_Days INTEGER,
|
|
185
|
+
Prev_Return_Pct DOUBLE,
|
|
186
|
+
Prior_Range_vs_ATR DOUBLE,
|
|
187
|
+
High_Time DOUBLE,
|
|
188
|
+
Low_Time DOUBLE,
|
|
189
|
+
High_Before_Low INTEGER,
|
|
190
|
+
Reversal_Type INTEGER,
|
|
191
|
+
Opening_Drive_Strength DOUBLE,
|
|
192
|
+
Intraday_Realized_Vol DOUBLE,
|
|
193
|
+
Day_of_Week INTEGER,
|
|
194
|
+
Month INTEGER,
|
|
195
|
+
Is_Opex INTEGER,
|
|
196
|
+
ivr DOUBLE,
|
|
197
|
+
ivp DOUBLE,
|
|
198
|
+
PRIMARY KEY (ticker, date)
|
|
199
|
+
)
|
|
200
|
+
`);
|
|
201
|
+
await conn.run(`
|
|
202
|
+
CREATE TABLE IF NOT EXISTS market.enriched_context (
|
|
203
|
+
date VARCHAR NOT NULL,
|
|
204
|
+
Vol_Regime INTEGER,
|
|
205
|
+
Term_Structure_State INTEGER,
|
|
206
|
+
Trend_Direction VARCHAR,
|
|
207
|
+
VIX_Spike_Pct DOUBLE,
|
|
208
|
+
VIX_Gap_Pct DOUBLE,
|
|
209
|
+
PRIMARY KEY (date)
|
|
210
|
+
)
|
|
211
|
+
`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/db/parquet-writer.ts
|
|
215
|
+
import { createHash as createHash3 } from "crypto";
|
|
216
|
+
import { createReadStream } from "fs";
|
|
217
|
+
import * as fs3 from "fs/promises";
|
|
218
|
+
import * as path6 from "path";
|
|
219
|
+
|
|
220
|
+
// src/market/provenance/partition-commit-store.ts
|
|
221
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
222
|
+
import { constants, readFileSync } from "fs";
|
|
223
|
+
import * as fs2 from "fs/promises";
|
|
224
|
+
import { hostname } from "os";
|
|
225
|
+
import * as path3 from "path";
|
|
226
|
+
|
|
227
|
+
// src/market/provenance/canonical-json.ts
|
|
228
|
+
import { createHash } from "crypto";
|
|
229
|
+
var CANONICAL_JSON_VERSION = 1;
|
|
230
|
+
var SHA256_ADDRESS_RE = /^sha256:([0-9a-f]{64})$/;
|
|
231
|
+
function compareCodePoints(left, right) {
|
|
232
|
+
const leftPoints = Array.from(left, (char) => char.codePointAt(0));
|
|
233
|
+
const rightPoints = Array.from(right, (char) => char.codePointAt(0));
|
|
234
|
+
const length = Math.min(leftPoints.length, rightPoints.length);
|
|
235
|
+
for (let i = 0; i < length; i++) {
|
|
236
|
+
if (leftPoints[i] !== rightPoints[i]) return leftPoints[i] - rightPoints[i];
|
|
237
|
+
}
|
|
238
|
+
return leftPoints.length - rightPoints.length;
|
|
239
|
+
}
|
|
240
|
+
function encode(value, seen) {
|
|
241
|
+
if (value === null) return "null";
|
|
242
|
+
switch (typeof value) {
|
|
243
|
+
case "string":
|
|
244
|
+
return JSON.stringify(value.normalize("NFC"));
|
|
245
|
+
case "boolean":
|
|
246
|
+
return value ? "true" : "false";
|
|
247
|
+
case "number":
|
|
248
|
+
if (!Number.isSafeInteger(value)) {
|
|
249
|
+
throw new TypeError("Canonical JSON v1 supports only safe integers");
|
|
250
|
+
}
|
|
251
|
+
return JSON.stringify(value);
|
|
252
|
+
case "undefined":
|
|
253
|
+
case "function":
|
|
254
|
+
case "symbol":
|
|
255
|
+
case "bigint":
|
|
256
|
+
throw new TypeError(`Canonical JSON does not support ${typeof value}`);
|
|
257
|
+
case "object":
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
const object = value;
|
|
261
|
+
if (seen.has(object)) throw new TypeError("Canonical JSON does not support cyclic values");
|
|
262
|
+
seen.add(object);
|
|
263
|
+
try {
|
|
264
|
+
if (Array.isArray(value)) {
|
|
265
|
+
const elements = [];
|
|
266
|
+
for (let i = 0; i < value.length; i++) {
|
|
267
|
+
if (!(i in value)) throw new TypeError("Canonical JSON does not support sparse arrays");
|
|
268
|
+
elements.push(encode(value[i], seen));
|
|
269
|
+
}
|
|
270
|
+
return `[${elements.join(",")}]`;
|
|
271
|
+
}
|
|
272
|
+
const prototype = Object.getPrototypeOf(value);
|
|
273
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
274
|
+
throw new TypeError("Canonical JSON accepts only plain objects and arrays");
|
|
275
|
+
}
|
|
276
|
+
const normalizedKeys = /* @__PURE__ */ new Map();
|
|
277
|
+
for (const sourceKey of Object.keys(value)) {
|
|
278
|
+
const normalizedKey = sourceKey.normalize("NFC");
|
|
279
|
+
const collision = normalizedKeys.get(normalizedKey);
|
|
280
|
+
if (collision !== void 0 && collision !== sourceKey) {
|
|
281
|
+
throw new TypeError(
|
|
282
|
+
`Canonical JSON key collision after NFC normalization: ${JSON.stringify(collision)} and ${JSON.stringify(sourceKey)}`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
normalizedKeys.set(normalizedKey, sourceKey);
|
|
286
|
+
}
|
|
287
|
+
const entries = [...normalizedKeys.entries()].sort(([left], [right]) => compareCodePoints(left, right)).map(
|
|
288
|
+
([normalizedKey, sourceKey]) => `${JSON.stringify(normalizedKey)}:${encode(value[sourceKey], seen)}`
|
|
289
|
+
);
|
|
290
|
+
return `{${entries.join(",")}}`;
|
|
291
|
+
} finally {
|
|
292
|
+
seen.delete(object);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function canonicalJson(value) {
|
|
296
|
+
return encode(value, /* @__PURE__ */ new WeakSet());
|
|
297
|
+
}
|
|
298
|
+
function canonicalJsonBytes(value) {
|
|
299
|
+
return Buffer.from(canonicalJson(value), "utf8");
|
|
300
|
+
}
|
|
301
|
+
function addressCanonicalJson(value) {
|
|
302
|
+
return addressBytes(canonicalJsonBytes(value));
|
|
303
|
+
}
|
|
304
|
+
function addressBytes(bytes) {
|
|
305
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
306
|
+
return `sha256:${digest}`;
|
|
307
|
+
}
|
|
308
|
+
function parseCanonicalJsonAddress(address) {
|
|
309
|
+
return parseSha256Address(address);
|
|
310
|
+
}
|
|
311
|
+
function parseSha256Address(address) {
|
|
312
|
+
const match = SHA256_ADDRESS_RE.exec(address);
|
|
313
|
+
if (!match) throw new TypeError(`Invalid SHA-256 address: ${JSON.stringify(address)}`);
|
|
314
|
+
return match[1];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/market/provenance/content-object-store.ts
|
|
318
|
+
import * as fs from "fs/promises";
|
|
319
|
+
import * as path from "path";
|
|
320
|
+
import { randomUUID } from "crypto";
|
|
321
|
+
var ContentObjectCollisionError = class extends Error {
|
|
322
|
+
address;
|
|
323
|
+
objectPath;
|
|
324
|
+
constructor(address, objectPath) {
|
|
325
|
+
super(`Content object collision or corruption at ${address} (${objectPath})`);
|
|
326
|
+
this.name = "ContentObjectCollisionError";
|
|
327
|
+
this.address = address;
|
|
328
|
+
this.objectPath = objectPath;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
function deepFreeze(value) {
|
|
332
|
+
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
333
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
334
|
+
Object.freeze(value);
|
|
335
|
+
}
|
|
336
|
+
return value;
|
|
337
|
+
}
|
|
338
|
+
var ContentObjectStore = class {
|
|
339
|
+
rootDir;
|
|
340
|
+
constructor(rootDir) {
|
|
341
|
+
this.rootDir = rootDir;
|
|
342
|
+
}
|
|
343
|
+
objectPath(address) {
|
|
344
|
+
const digest = parseCanonicalJsonAddress(address);
|
|
345
|
+
return path.join(this.rootDir, "objects", "sha256", digest.slice(0, 2), `${digest}.json`);
|
|
346
|
+
}
|
|
347
|
+
async syncDirectory(directory) {
|
|
348
|
+
const handle = await fs.open(directory, "r");
|
|
349
|
+
try {
|
|
350
|
+
await handle.sync();
|
|
351
|
+
} finally {
|
|
352
|
+
await handle.close();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
async ensureDurableDirectory(directory) {
|
|
356
|
+
const parent = path.dirname(directory);
|
|
357
|
+
if (parent !== directory) await this.ensureDurableDirectory(parent);
|
|
358
|
+
try {
|
|
359
|
+
const handle = await fs.open(directory, "r");
|
|
360
|
+
try {
|
|
361
|
+
await handle.sync();
|
|
362
|
+
} finally {
|
|
363
|
+
await handle.close();
|
|
364
|
+
}
|
|
365
|
+
if (parent !== directory) await this.syncDirectory(parent);
|
|
366
|
+
return;
|
|
367
|
+
} catch (error) {
|
|
368
|
+
if (error.code !== "ENOENT") throw error;
|
|
369
|
+
}
|
|
370
|
+
if (parent === directory)
|
|
371
|
+
throw new Error(`Cannot create content-object directory ${directory}`);
|
|
372
|
+
let created = false;
|
|
373
|
+
try {
|
|
374
|
+
await fs.mkdir(directory);
|
|
375
|
+
created = true;
|
|
376
|
+
} catch (error) {
|
|
377
|
+
if (error.code !== "EEXIST") throw error;
|
|
378
|
+
}
|
|
379
|
+
if (created) {
|
|
380
|
+
await this.syncDirectory(directory);
|
|
381
|
+
await this.syncDirectory(parent);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
async syncExistingObject(objectPath) {
|
|
385
|
+
const handle = await fs.open(objectPath, "r");
|
|
386
|
+
try {
|
|
387
|
+
await handle.sync();
|
|
388
|
+
} finally {
|
|
389
|
+
await handle.close();
|
|
390
|
+
}
|
|
391
|
+
await this.syncDirectory(path.dirname(objectPath));
|
|
392
|
+
}
|
|
393
|
+
async put(value) {
|
|
394
|
+
const bytes = canonicalJsonBytes(value);
|
|
395
|
+
const address = addressBytes(bytes);
|
|
396
|
+
const materialized = deepFreeze(JSON.parse(bytes.toString("utf8")));
|
|
397
|
+
const objectPath = this.objectPath(address);
|
|
398
|
+
const objectDir = path.dirname(objectPath);
|
|
399
|
+
await this.ensureDurableDirectory(objectDir);
|
|
400
|
+
const tempPath = path.join(objectDir, `.${path.basename(objectPath)}.tmp-${randomUUID()}`);
|
|
401
|
+
let handle;
|
|
402
|
+
try {
|
|
403
|
+
handle = await fs.open(tempPath, "wx", 292);
|
|
404
|
+
await handle.writeFile(bytes);
|
|
405
|
+
await handle.sync();
|
|
406
|
+
await handle.close();
|
|
407
|
+
handle = void 0;
|
|
408
|
+
await fs.link(tempPath, objectPath);
|
|
409
|
+
await this.syncDirectory(objectDir);
|
|
410
|
+
await fs.unlink(tempPath);
|
|
411
|
+
await this.syncDirectory(objectDir);
|
|
412
|
+
return {
|
|
413
|
+
address,
|
|
414
|
+
value: materialized,
|
|
415
|
+
path: objectPath,
|
|
416
|
+
bytes: bytes.byteLength,
|
|
417
|
+
created: true
|
|
418
|
+
};
|
|
419
|
+
} catch (error) {
|
|
420
|
+
await handle?.close();
|
|
421
|
+
handle = void 0;
|
|
422
|
+
await fs.unlink(tempPath).catch(() => void 0);
|
|
423
|
+
if (error.code !== "EEXIST") throw error;
|
|
424
|
+
const existing = await fs.readFile(objectPath);
|
|
425
|
+
if (!existing.equals(bytes)) throw new ContentObjectCollisionError(address, objectPath);
|
|
426
|
+
await this.syncExistingObject(objectPath);
|
|
427
|
+
return {
|
|
428
|
+
address,
|
|
429
|
+
value: materialized,
|
|
430
|
+
path: objectPath,
|
|
431
|
+
bytes: bytes.byteLength,
|
|
432
|
+
created: false
|
|
433
|
+
};
|
|
434
|
+
} finally {
|
|
435
|
+
await handle?.close();
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
async get(address) {
|
|
439
|
+
const objectPath = this.objectPath(address);
|
|
440
|
+
const bytes = await fs.readFile(objectPath);
|
|
441
|
+
const digestAddress = addressBytes(bytes);
|
|
442
|
+
if (digestAddress !== address) throw new ContentObjectCollisionError(address, objectPath);
|
|
443
|
+
const value = JSON.parse(bytes.toString("utf8"));
|
|
444
|
+
if (!canonicalJsonBytes(value).equals(bytes)) {
|
|
445
|
+
throw new ContentObjectCollisionError(address, objectPath);
|
|
446
|
+
}
|
|
447
|
+
return deepFreeze(value);
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
// src/market/provenance/dataset-registry.ts
|
|
452
|
+
import * as path2 from "path";
|
|
453
|
+
var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
454
|
+
function deepFreezeDatasetRegistry(registry) {
|
|
455
|
+
for (const definition of Object.values(registry)) {
|
|
456
|
+
Object.freeze(definition.partitionKeys);
|
|
457
|
+
Object.freeze(definition.provenance);
|
|
458
|
+
Object.freeze(definition);
|
|
459
|
+
}
|
|
460
|
+
return Object.freeze(registry);
|
|
461
|
+
}
|
|
462
|
+
var MARKET_DATASETS = deepFreezeDatasetRegistry({
|
|
463
|
+
spot: {
|
|
464
|
+
subdir: "spot",
|
|
465
|
+
partitionKeys: ["ticker", "date"],
|
|
466
|
+
filename: "data.parquet",
|
|
467
|
+
schemaRevision: 1,
|
|
468
|
+
provenance: { kind: "bounded-session", sessionKey: "date" }
|
|
469
|
+
},
|
|
470
|
+
enriched: {
|
|
471
|
+
subdir: "enriched",
|
|
472
|
+
partitionKeys: ["ticker", "date"],
|
|
473
|
+
filename: "data.parquet",
|
|
474
|
+
schemaRevision: 1,
|
|
475
|
+
provenance: { kind: "bounded-session", sessionKey: "date" }
|
|
476
|
+
},
|
|
477
|
+
enriched_context: {
|
|
478
|
+
subdir: "enriched/context",
|
|
479
|
+
partitionKeys: ["date"],
|
|
480
|
+
filename: "data.parquet",
|
|
481
|
+
schemaRevision: 1,
|
|
482
|
+
provenance: { kind: "bounded-session", sessionKey: "date" }
|
|
483
|
+
},
|
|
484
|
+
option_chain: {
|
|
485
|
+
subdir: "option_chain",
|
|
486
|
+
partitionKeys: ["underlying", "date"],
|
|
487
|
+
filename: "data.parquet",
|
|
488
|
+
schemaRevision: 1,
|
|
489
|
+
provenance: { kind: "bounded-session", sessionKey: "date" }
|
|
490
|
+
},
|
|
491
|
+
option_quote_minutes: {
|
|
492
|
+
subdir: "option_quote_minutes",
|
|
493
|
+
partitionKeys: ["underlying", "date"],
|
|
494
|
+
filename: "data.parquet",
|
|
495
|
+
schemaRevision: 1,
|
|
496
|
+
provenance: { kind: "bounded-session", sessionKey: "date" }
|
|
497
|
+
},
|
|
498
|
+
option_oi_daily: {
|
|
499
|
+
subdir: "option_oi_daily",
|
|
500
|
+
partitionKeys: ["underlying", "date"],
|
|
501
|
+
filename: "data.parquet",
|
|
502
|
+
schemaRevision: 1,
|
|
503
|
+
provenance: { kind: "bounded-session", sessionKey: "date" }
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
function isRealMarketSessionDate(value) {
|
|
507
|
+
if (!ISO_DATE_RE.test(value)) return false;
|
|
508
|
+
const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
|
|
509
|
+
return Number.isFinite(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value;
|
|
510
|
+
}
|
|
511
|
+
function marketDatasetDefinition(dataset) {
|
|
512
|
+
return Object.hasOwn(MARKET_DATASETS, dataset) ? MARKET_DATASETS[dataset] : void 0;
|
|
513
|
+
}
|
|
514
|
+
function canonicalPartitionDataset(dataset) {
|
|
515
|
+
const definition = marketDatasetDefinition(dataset);
|
|
516
|
+
return definition?.provenance.kind === "bounded-session" ? definition : void 0;
|
|
517
|
+
}
|
|
518
|
+
function validatePartitionIdentity(identity) {
|
|
519
|
+
const definition = canonicalPartitionDataset(identity.dataset);
|
|
520
|
+
if (!definition) {
|
|
521
|
+
throw new TypeError(`Unregistered provenance dataset: ${JSON.stringify(identity.dataset)}`);
|
|
522
|
+
}
|
|
523
|
+
const expectedKeys = [...definition.partitionKeys].sort();
|
|
524
|
+
const observedKeys = Object.keys(identity.partition).sort();
|
|
525
|
+
if (observedKeys.length !== expectedKeys.length || observedKeys.some((key, index) => key !== expectedKeys[index])) {
|
|
526
|
+
throw new TypeError(
|
|
527
|
+
`Invalid provenance partition keys: ${JSON.stringify({ dataset: identity.dataset, observedKeys, expectedKeys })}`
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
for (const [key, value] of Object.entries(identity.partition)) {
|
|
531
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || !/^[A-Za-z0-9._-]+$/.test(value)) {
|
|
532
|
+
throw new TypeError(`Invalid provenance partition: ${JSON.stringify({ key, value })}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const session = identity.partition[definition.provenance.sessionKey];
|
|
536
|
+
if (!isRealMarketSessionDate(session)) {
|
|
537
|
+
throw new TypeError(`Invalid provenance partition date: ${JSON.stringify(session)}`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
function canonicalPartitionRelativePath(identity) {
|
|
541
|
+
validatePartitionIdentity(identity);
|
|
542
|
+
const definition = canonicalPartitionDataset(identity.dataset);
|
|
543
|
+
return path2.posix.join(
|
|
544
|
+
definition.subdir,
|
|
545
|
+
...definition.partitionKeys.map((key) => `${key}=${identity.partition[key]}`),
|
|
546
|
+
definition.filename
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/market/provenance/partition-commit-store.ts
|
|
551
|
+
var PARTITION_COMMIT_RECEIPT_KIND = "tradeblocks.market-data.partition-commit";
|
|
552
|
+
var PARTITION_COMMIT_RECEIPT_VERSION = 1;
|
|
553
|
+
var PARTITION_COMMIT_EVENT_KIND = "tradeblocks.market-data.partition-commit-event";
|
|
554
|
+
var PARTITION_COMMIT_EVENT_VERSION = 1;
|
|
555
|
+
var PARTITION_HEAD_KIND = "tradeblocks.market-data.partition-head";
|
|
556
|
+
var PARTITION_HEAD_VERSION = 1;
|
|
557
|
+
var INTERNAL_HISTORICAL_PARTITION_ADOPTION = /* @__PURE__ */ Symbol(
|
|
558
|
+
"tradeblocks.internal.historical-partition-adoption"
|
|
559
|
+
);
|
|
560
|
+
var partitionCommitTestFaults = /* @__PURE__ */ new WeakMap();
|
|
561
|
+
function setPartitionCommitTestFault(store, fault) {
|
|
562
|
+
if (fault) partitionCommitTestFaults.set(store, fault);
|
|
563
|
+
else partitionCommitTestFaults.delete(store);
|
|
564
|
+
}
|
|
565
|
+
function attachPartitionLockCleanupError(primary, cleanup) {
|
|
566
|
+
if (primary instanceof Error) {
|
|
567
|
+
try {
|
|
568
|
+
Object.defineProperty(primary, "cleanupError", {
|
|
569
|
+
value: cleanup,
|
|
570
|
+
enumerable: false,
|
|
571
|
+
configurable: true
|
|
572
|
+
});
|
|
573
|
+
return primary;
|
|
574
|
+
} catch {
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return new AggregateError(
|
|
578
|
+
[primary, cleanup],
|
|
579
|
+
"Partition operation failed and its lock claim could not be released",
|
|
580
|
+
{ cause: primary }
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
var PartitionFileIntegrityError = class extends Error {
|
|
584
|
+
filePath;
|
|
585
|
+
constructor(filePath, reason, cause) {
|
|
586
|
+
super(`Invalid partition file (${reason}): ${filePath}`, {
|
|
587
|
+
...cause === void 0 ? {} : { cause }
|
|
588
|
+
});
|
|
589
|
+
this.name = "PartitionFileIntegrityError";
|
|
590
|
+
this.filePath = filePath;
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
var TOKEN_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
594
|
+
var CURRENT_HOSTNAME = hostname();
|
|
595
|
+
var CURRENT_BOOT_ID = (() => {
|
|
596
|
+
try {
|
|
597
|
+
const value = readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
598
|
+
return value.length > 0 ? value : "unavailable";
|
|
599
|
+
} catch {
|
|
600
|
+
return "unavailable";
|
|
601
|
+
}
|
|
602
|
+
})();
|
|
603
|
+
function validateIdentity(identity) {
|
|
604
|
+
validatePartitionIdentity(identity);
|
|
605
|
+
}
|
|
606
|
+
function canonicalRelativePath(identity) {
|
|
607
|
+
return canonicalPartitionRelativePath(identity);
|
|
608
|
+
}
|
|
609
|
+
function validateRelativePath(relativePath) {
|
|
610
|
+
if (relativePath.length === 0 || relativePath.includes("\\") || path3.posix.isAbsolute(relativePath) || relativePath.split("/").some((part) => part === ".." || part === "." || part === "")) {
|
|
611
|
+
throw new TypeError(`Invalid provenance relative path: ${JSON.stringify(relativePath)}`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
function validateRegistryPath(identity, relativePath) {
|
|
615
|
+
validateRelativePath(relativePath);
|
|
616
|
+
const expected = canonicalRelativePath(identity);
|
|
617
|
+
if (relativePath !== expected) {
|
|
618
|
+
throw new TypeError(
|
|
619
|
+
`Provenance path does not match the registered partition: ${JSON.stringify({ expected, observed: relativePath })}`
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function validateFingerprint(file) {
|
|
624
|
+
parseSha256Address(file.address);
|
|
625
|
+
if (!Number.isSafeInteger(file.bytes) || file.bytes < 0) {
|
|
626
|
+
throw new TypeError(`Invalid provenance byte count: ${file.bytes}`);
|
|
627
|
+
}
|
|
628
|
+
if (!Number.isSafeInteger(file.rows) || file.rows < 0) {
|
|
629
|
+
throw new TypeError(`Invalid provenance row count: ${file.rows}`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
function validateCoverage(coverage, rows) {
|
|
633
|
+
if (coverage.kind === "empty") {
|
|
634
|
+
if (rows !== 0) throw new TypeError("Non-empty partition cannot have empty logical coverage");
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
if (!isRealMarketSessionDate(coverage.from) || !isRealMarketSessionDate(coverage.through)) {
|
|
638
|
+
throw new TypeError(`Invalid logical date coverage: ${JSON.stringify(coverage)}`);
|
|
639
|
+
}
|
|
640
|
+
if (coverage.from > coverage.through) {
|
|
641
|
+
throw new TypeError(`Logical coverage starts after it ends: ${JSON.stringify(coverage)}`);
|
|
642
|
+
}
|
|
643
|
+
if (rows === 0) throw new TypeError("Empty partition cannot have non-empty logical coverage");
|
|
644
|
+
}
|
|
645
|
+
function validateQuality(quality, file) {
|
|
646
|
+
for (const [name, count] of Object.entries(quality)) {
|
|
647
|
+
if (!Number.isSafeInteger(count) || count < 0) {
|
|
648
|
+
throw new TypeError(`Invalid provenance ${name}: ${count}`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
if (quality.writtenRows !== file.rows) {
|
|
652
|
+
throw new TypeError(
|
|
653
|
+
`Provenance writtenRows ${quality.writtenRows} does not match file rows ${file.rows}`
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
if (quality.inputRows !== quality.writtenRows + quality.droppedRows) {
|
|
657
|
+
throw new TypeError("Provenance inputRows must equal writtenRows + droppedRows");
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
function validateInput(input) {
|
|
661
|
+
validateIdentity(input);
|
|
662
|
+
if (!Number.isSafeInteger(input.schemaRevision) || input.schemaRevision < 1) {
|
|
663
|
+
throw new TypeError(`Invalid schema revision: ${input.schemaRevision}`);
|
|
664
|
+
}
|
|
665
|
+
validateRegistryPath(input, input.relativePath);
|
|
666
|
+
validateFingerprint(input.file);
|
|
667
|
+
validateCoverage(input.coverage, input.file.rows);
|
|
668
|
+
validateQuality(input.quality, input.file);
|
|
669
|
+
const date = input.partition[canonicalPartitionDataset(input.dataset).provenance.sessionKey];
|
|
670
|
+
if (input.coverage.kind === "date-range" && (input.coverage.from !== date || input.coverage.through !== date)) {
|
|
671
|
+
throw new TypeError(
|
|
672
|
+
`Partition logical coverage must equal its registered date: ${JSON.stringify({ date, coverage: input.coverage })}`
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
var CANONICAL_PARQUET_SCHEMAS = Object.freeze({
|
|
677
|
+
spot: [
|
|
678
|
+
["ticker", "VARCHAR"],
|
|
679
|
+
["date", "VARCHAR"],
|
|
680
|
+
["time", "VARCHAR"],
|
|
681
|
+
["open", "DOUBLE"],
|
|
682
|
+
["high", "DOUBLE"],
|
|
683
|
+
["low", "DOUBLE"],
|
|
684
|
+
["close", "DOUBLE"],
|
|
685
|
+
["bid", "DOUBLE"],
|
|
686
|
+
["ask", "DOUBLE"]
|
|
687
|
+
],
|
|
688
|
+
enriched: [
|
|
689
|
+
["ticker", "VARCHAR"],
|
|
690
|
+
["date", "VARCHAR"],
|
|
691
|
+
["Prior_Close", "DOUBLE"],
|
|
692
|
+
["Gap_Pct", "DOUBLE"],
|
|
693
|
+
["ATR_Pct", "DOUBLE"],
|
|
694
|
+
["RSI_14", "DOUBLE"],
|
|
695
|
+
["Price_vs_EMA21_Pct", "DOUBLE"],
|
|
696
|
+
["Price_vs_SMA50_Pct", "DOUBLE"],
|
|
697
|
+
["Realized_Vol_5D", "DOUBLE"],
|
|
698
|
+
["Realized_Vol_20D", "DOUBLE"],
|
|
699
|
+
["Return_5D", "DOUBLE"],
|
|
700
|
+
["Return_20D", "DOUBLE"],
|
|
701
|
+
["Intraday_Range_Pct", "DOUBLE"],
|
|
702
|
+
["Intraday_Return_Pct", "DOUBLE"],
|
|
703
|
+
["Close_Position_In_Range", "DOUBLE"],
|
|
704
|
+
["Gap_Filled", "INTEGER"],
|
|
705
|
+
["Consecutive_Days", "INTEGER"],
|
|
706
|
+
["Prev_Return_Pct", "DOUBLE"],
|
|
707
|
+
["Prior_Range_vs_ATR", "DOUBLE"],
|
|
708
|
+
["High_Time", "DOUBLE"],
|
|
709
|
+
["Low_Time", "DOUBLE"],
|
|
710
|
+
["High_Before_Low", "INTEGER"],
|
|
711
|
+
["Reversal_Type", "INTEGER"],
|
|
712
|
+
["Opening_Drive_Strength", "DOUBLE"],
|
|
713
|
+
["Intraday_Realized_Vol", "DOUBLE"],
|
|
714
|
+
["Day_of_Week", "INTEGER"],
|
|
715
|
+
["Month", "INTEGER"],
|
|
716
|
+
["Is_Opex", "INTEGER"],
|
|
717
|
+
["ivr", "DOUBLE"],
|
|
718
|
+
["ivp", "DOUBLE"]
|
|
719
|
+
],
|
|
720
|
+
enriched_context: [
|
|
721
|
+
["date", "VARCHAR"],
|
|
722
|
+
["Vol_Regime", "INTEGER"],
|
|
723
|
+
["Term_Structure_State", "INTEGER"],
|
|
724
|
+
["Trend_Direction", "VARCHAR"],
|
|
725
|
+
["VIX_Spike_Pct", "DOUBLE"],
|
|
726
|
+
["VIX_Gap_Pct", "DOUBLE"]
|
|
727
|
+
],
|
|
728
|
+
option_chain: [
|
|
729
|
+
["underlying", "VARCHAR"],
|
|
730
|
+
["date", "VARCHAR"],
|
|
731
|
+
["ticker", "VARCHAR"],
|
|
732
|
+
["contract_type", "VARCHAR"],
|
|
733
|
+
["strike", "DOUBLE"],
|
|
734
|
+
["expiration", "VARCHAR"],
|
|
735
|
+
["dte", "INTEGER"],
|
|
736
|
+
["exercise_style", "VARCHAR"]
|
|
737
|
+
],
|
|
738
|
+
option_quote_minutes: [
|
|
739
|
+
["underlying", "VARCHAR"],
|
|
740
|
+
["date", "VARCHAR"],
|
|
741
|
+
["ticker", "VARCHAR"],
|
|
742
|
+
["time", "VARCHAR"],
|
|
743
|
+
["bid", "DOUBLE"],
|
|
744
|
+
["ask", "DOUBLE"],
|
|
745
|
+
["mid", "DOUBLE"],
|
|
746
|
+
["last_updated_ns", "BIGINT"],
|
|
747
|
+
["source", "VARCHAR"],
|
|
748
|
+
["delta", "FLOAT"],
|
|
749
|
+
["gamma", "FLOAT"],
|
|
750
|
+
["theta", "FLOAT"],
|
|
751
|
+
["vega", "FLOAT"],
|
|
752
|
+
["iv", "FLOAT"],
|
|
753
|
+
["greeks_source", "VARCHAR"],
|
|
754
|
+
["greeks_revision", "INTEGER"],
|
|
755
|
+
["rate_type", "VARCHAR"],
|
|
756
|
+
["rate_value", "DOUBLE"],
|
|
757
|
+
["gamma_source", "VARCHAR"]
|
|
758
|
+
],
|
|
759
|
+
option_oi_daily: [
|
|
760
|
+
["underlying", "VARCHAR"],
|
|
761
|
+
["date", "VARCHAR"],
|
|
762
|
+
["ticker", "VARCHAR"],
|
|
763
|
+
["expiration", "VARCHAR"],
|
|
764
|
+
["strike", "DOUBLE"],
|
|
765
|
+
["right", "VARCHAR"],
|
|
766
|
+
["open_interest", "BIGINT"],
|
|
767
|
+
["source", "VARCHAR"]
|
|
768
|
+
]
|
|
769
|
+
});
|
|
770
|
+
function escapeSqlLiteral(value) {
|
|
771
|
+
return value.replaceAll("'", "''");
|
|
772
|
+
}
|
|
773
|
+
function canonicalDuckDbType(value) {
|
|
774
|
+
const normalized = String(value).toUpperCase();
|
|
775
|
+
return normalized === "REAL" ? "FLOAT" : normalized;
|
|
776
|
+
}
|
|
777
|
+
async function inspectCanonicalParquet(conn, filePath, identity) {
|
|
778
|
+
const expectedSchema = CANONICAL_PARQUET_SCHEMAS[identity.dataset];
|
|
779
|
+
if (!expectedSchema) throw new TypeError(`No canonical Parquet schema for ${identity.dataset}`);
|
|
780
|
+
const source = `read_parquet('${escapeSqlLiteral(filePath)}', hive_partitioning=false)`;
|
|
781
|
+
const described = await conn.runAndReadAll(`DESCRIBE SELECT * FROM ${source}`);
|
|
782
|
+
const schema = described.getRows().map((row2) => [String(row2[0]), canonicalDuckDbType(row2[1])]);
|
|
783
|
+
if (canonicalJsonBytes(schema).compare(canonicalJsonBytes(expectedSchema)) !== 0) {
|
|
784
|
+
throw new Error(
|
|
785
|
+
`Existing partition Parquet schema does not match revision 1: ${JSON.stringify({ observed: schema, expected: expectedSchema })}`
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
const definition = canonicalPartitionDataset(identity.dataset);
|
|
789
|
+
const sessionColumn = definition.provenance.sessionKey;
|
|
790
|
+
const predicates = Object.entries(identity.partition).map(
|
|
791
|
+
([key, value]) => `COUNT(*) FILTER (WHERE "${key}" IS NULL OR CAST("${key}" AS VARCHAR) <> '${escapeSqlLiteral(value)}')::BIGINT`
|
|
792
|
+
).join(", ");
|
|
793
|
+
const inspected = await conn.runAndReadAll(
|
|
794
|
+
`SELECT COUNT(*)::BIGINT,
|
|
795
|
+
COUNT("${sessionColumn}")::BIGINT,
|
|
796
|
+
MIN(CAST("${sessionColumn}" AS VARCHAR)),
|
|
797
|
+
MAX(CAST("${sessionColumn}" AS VARCHAR)),
|
|
798
|
+
${predicates}
|
|
799
|
+
FROM ${source}`
|
|
800
|
+
);
|
|
801
|
+
const row = inspected.getRows()[0];
|
|
802
|
+
const rows = Number(row[0]);
|
|
803
|
+
const coveredRows = Number(row[1]);
|
|
804
|
+
if (!Number.isSafeInteger(rows) || rows <= 0 || coveredRows !== rows) {
|
|
805
|
+
throw new Error("Existing canonical Parquet must contain non-null rows for its session");
|
|
806
|
+
}
|
|
807
|
+
for (const mismatches of row.slice(4)) {
|
|
808
|
+
if (Number(mismatches) !== 0) {
|
|
809
|
+
throw new Error("Existing canonical Parquet rows disagree with the registered partition");
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
const from = String(row[2]);
|
|
813
|
+
const through = String(row[3]);
|
|
814
|
+
const expectedSession = identity.partition[sessionColumn];
|
|
815
|
+
if (from !== expectedSession || through !== expectedSession) {
|
|
816
|
+
throw new Error("Existing canonical Parquet coverage disagrees with its partition session");
|
|
817
|
+
}
|
|
818
|
+
return { rows, coverage: { kind: "date-range", from, through } };
|
|
819
|
+
}
|
|
820
|
+
function validateReceipt(receipt, identity) {
|
|
821
|
+
if (receipt.kind !== PARTITION_COMMIT_RECEIPT_KIND || receipt.version !== PARTITION_COMMIT_RECEIPT_VERSION || receipt.dataset !== identity.dataset || addressCanonicalJson(receipt.partition) !== addressCanonicalJson(identity.partition)) {
|
|
822
|
+
throw new Error("Partition receipt does not match its identity");
|
|
823
|
+
}
|
|
824
|
+
validateInput(receipt);
|
|
825
|
+
if (receipt.classification !== "append" && receipt.classification !== "repair") {
|
|
826
|
+
throw new Error(`Invalid partition receipt classification: ${String(receipt.classification)}`);
|
|
827
|
+
}
|
|
828
|
+
if (receipt.classification === "append" && receipt.parent !== void 0) {
|
|
829
|
+
throw new Error("Append receipt must not have a parent");
|
|
830
|
+
}
|
|
831
|
+
if (receipt.classification === "repair" && receipt.parent === void 0) {
|
|
832
|
+
throw new Error("Repair receipt must have a parent");
|
|
833
|
+
}
|
|
834
|
+
if (receipt.parent !== void 0) parseCanonicalJsonAddress(receipt.parent);
|
|
835
|
+
}
|
|
836
|
+
function identityAddress(identity) {
|
|
837
|
+
return addressCanonicalJson({
|
|
838
|
+
kind: "tradeblocks.market-data.partition-identity",
|
|
839
|
+
version: 1,
|
|
840
|
+
dataset: identity.dataset,
|
|
841
|
+
partition: identity.partition
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
function sameInode(left, right) {
|
|
845
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
846
|
+
}
|
|
847
|
+
async function openRegularFileNoFollow(filePath) {
|
|
848
|
+
let handle;
|
|
849
|
+
try {
|
|
850
|
+
handle = await fs2.open(
|
|
851
|
+
filePath,
|
|
852
|
+
constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK
|
|
853
|
+
);
|
|
854
|
+
} catch (error) {
|
|
855
|
+
if (error.code === "ELOOP") {
|
|
856
|
+
throw new PartitionFileIntegrityError(filePath, "symbolic links are not allowed", error);
|
|
857
|
+
}
|
|
858
|
+
throw error;
|
|
859
|
+
}
|
|
860
|
+
try {
|
|
861
|
+
const stat2 = await handle.stat();
|
|
862
|
+
if (!stat2.isFile()) {
|
|
863
|
+
throw new PartitionFileIntegrityError(filePath, "expected a regular file");
|
|
864
|
+
}
|
|
865
|
+
if (stat2.nlink !== 1) {
|
|
866
|
+
throw new PartitionFileIntegrityError(filePath, "expected an unshared regular file inode");
|
|
867
|
+
}
|
|
868
|
+
return { handle, stat: stat2 };
|
|
869
|
+
} catch (error) {
|
|
870
|
+
await handle.close();
|
|
871
|
+
throw error;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
async function exactOpenFileAddress(handle) {
|
|
875
|
+
const hash = createHash2("sha256");
|
|
876
|
+
let bytes = 0;
|
|
877
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
878
|
+
while (true) {
|
|
879
|
+
const read = await handle.read(buffer, 0, buffer.byteLength, bytes);
|
|
880
|
+
if (read.bytesRead === 0) break;
|
|
881
|
+
hash.update(buffer.subarray(0, read.bytesRead));
|
|
882
|
+
bytes += read.bytesRead;
|
|
883
|
+
}
|
|
884
|
+
return { address: `sha256:${hash.digest("hex")}`, bytes };
|
|
885
|
+
}
|
|
886
|
+
async function requireNamedRegularInode(filePath, expected) {
|
|
887
|
+
let named;
|
|
888
|
+
try {
|
|
889
|
+
named = await fs2.lstat(filePath);
|
|
890
|
+
} catch (error) {
|
|
891
|
+
throw new PartitionFileIntegrityError(
|
|
892
|
+
filePath,
|
|
893
|
+
"verified inode is no longer named here",
|
|
894
|
+
error
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
if (!named.isFile()) {
|
|
898
|
+
throw new PartitionFileIntegrityError(filePath, "canonical entry is not a regular file");
|
|
899
|
+
}
|
|
900
|
+
if (named.nlink !== 1 || expected.nlink !== 1) {
|
|
901
|
+
throw new PartitionFileIntegrityError(filePath, "canonical inode has multiple hard links");
|
|
902
|
+
}
|
|
903
|
+
if (!sameInode(expected, named)) {
|
|
904
|
+
throw new PartitionFileIntegrityError(filePath, "canonical entry changed during verification");
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
async function exactFileAddress(filePath) {
|
|
908
|
+
const opened = await openRegularFileNoFollow(filePath);
|
|
909
|
+
try {
|
|
910
|
+
const fingerprint = await exactOpenFileAddress(opened.handle);
|
|
911
|
+
const after = await opened.handle.stat();
|
|
912
|
+
if (!after.isFile() || after.nlink !== 1 || !sameInode(opened.stat, after)) {
|
|
913
|
+
throw new PartitionFileIntegrityError(filePath, "open inode changed during verification");
|
|
914
|
+
}
|
|
915
|
+
await requireNamedRegularInode(filePath, opened.stat);
|
|
916
|
+
return fingerprint;
|
|
917
|
+
} finally {
|
|
918
|
+
await opened.handle.close();
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
async function snapshotOpenFile(source, snapshotPath) {
|
|
922
|
+
const snapshot = await fs2.open(snapshotPath, "wx", 256);
|
|
923
|
+
const hash = createHash2("sha256");
|
|
924
|
+
let bytes = 0;
|
|
925
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
926
|
+
try {
|
|
927
|
+
while (true) {
|
|
928
|
+
const read = await source.read(buffer, 0, buffer.byteLength, bytes);
|
|
929
|
+
if (read.bytesRead === 0) break;
|
|
930
|
+
hash.update(buffer.subarray(0, read.bytesRead));
|
|
931
|
+
let written = 0;
|
|
932
|
+
while (written < read.bytesRead) {
|
|
933
|
+
const result = await snapshot.write(
|
|
934
|
+
buffer,
|
|
935
|
+
written,
|
|
936
|
+
read.bytesRead - written,
|
|
937
|
+
bytes + written
|
|
938
|
+
);
|
|
939
|
+
if (result.bytesWritten === 0) throw new Error("Unable to write inspection snapshot");
|
|
940
|
+
written += result.bytesWritten;
|
|
941
|
+
}
|
|
942
|
+
bytes += read.bytesRead;
|
|
943
|
+
}
|
|
944
|
+
await snapshot.sync();
|
|
945
|
+
} finally {
|
|
946
|
+
await snapshot.close();
|
|
947
|
+
}
|
|
948
|
+
return { address: `sha256:${hash.digest("hex")}`, bytes };
|
|
949
|
+
}
|
|
950
|
+
function sameCommitContent(previous, input) {
|
|
951
|
+
return previous.schemaRevision === input.schemaRevision && previous.relativePath === input.relativePath && addressCanonicalJson(previous.coverage) === addressCanonicalJson(input.coverage) && addressCanonicalJson(previous.quality) === addressCanonicalJson(input.quality) && previous.file.address === input.file.address && previous.file.bytes === input.file.bytes && previous.file.rows === input.file.rows;
|
|
952
|
+
}
|
|
953
|
+
function captureIdentity(identity) {
|
|
954
|
+
const captured = JSON.parse(
|
|
955
|
+
canonicalJsonBytes({ dataset: identity.dataset, partition: identity.partition }).toString(
|
|
956
|
+
"utf8"
|
|
957
|
+
)
|
|
958
|
+
);
|
|
959
|
+
validateIdentity(captured);
|
|
960
|
+
return Object.freeze({ ...captured, partition: Object.freeze(captured.partition) });
|
|
961
|
+
}
|
|
962
|
+
function captureInput(input) {
|
|
963
|
+
const captured = JSON.parse(
|
|
964
|
+
canonicalJsonBytes({
|
|
965
|
+
dataset: input.dataset,
|
|
966
|
+
partition: input.partition,
|
|
967
|
+
schemaRevision: input.schemaRevision,
|
|
968
|
+
relativePath: input.relativePath,
|
|
969
|
+
coverage: input.coverage,
|
|
970
|
+
quality: input.quality,
|
|
971
|
+
file: input.file
|
|
972
|
+
}).toString("utf8")
|
|
973
|
+
);
|
|
974
|
+
validateInput(captured);
|
|
975
|
+
Object.freeze(captured.partition);
|
|
976
|
+
Object.freeze(captured.coverage);
|
|
977
|
+
Object.freeze(captured.quality);
|
|
978
|
+
Object.freeze(captured.file);
|
|
979
|
+
return Object.freeze(captured);
|
|
980
|
+
}
|
|
981
|
+
var FilePartitionCommitStore = class {
|
|
982
|
+
marketRootDir;
|
|
983
|
+
objects;
|
|
984
|
+
provenanceRootDir;
|
|
985
|
+
staleLockMs;
|
|
986
|
+
lockWaitMs;
|
|
987
|
+
constructor(marketRootDir, options = {}) {
|
|
988
|
+
this.marketRootDir = marketRootDir;
|
|
989
|
+
this.provenanceRootDir = path3.join(marketRootDir, ".provenance");
|
|
990
|
+
this.objects = new ContentObjectStore(this.provenanceRootDir);
|
|
991
|
+
this.staleLockMs = options.staleLockMs ?? 3e4;
|
|
992
|
+
this.lockWaitMs = options.lockWaitMs ?? 5e3;
|
|
993
|
+
}
|
|
994
|
+
identityDigest(identity) {
|
|
995
|
+
return parseCanonicalJsonAddress(identityAddress(identity));
|
|
996
|
+
}
|
|
997
|
+
headPath(identity) {
|
|
998
|
+
const digest = this.identityDigest(identity);
|
|
999
|
+
return path3.join(this.provenanceRootDir, "heads", digest.slice(0, 2), `${digest}.json`);
|
|
1000
|
+
}
|
|
1001
|
+
eventIndexDir(identity) {
|
|
1002
|
+
const digest = this.identityDigest(identity);
|
|
1003
|
+
return path3.join(this.provenanceRootDir, "events", digest.slice(0, 2), digest);
|
|
1004
|
+
}
|
|
1005
|
+
lockRoot(identity) {
|
|
1006
|
+
const digest = this.identityDigest(identity);
|
|
1007
|
+
return path3.join(this.provenanceRootDir, "locks", digest.slice(0, 2), digest);
|
|
1008
|
+
}
|
|
1009
|
+
targetPath(relativePath) {
|
|
1010
|
+
validateRelativePath(relativePath);
|
|
1011
|
+
return path3.join(this.marketRootDir, ...relativePath.split("/"));
|
|
1012
|
+
}
|
|
1013
|
+
async validateMarketRoot() {
|
|
1014
|
+
const root = path3.resolve(this.marketRootDir);
|
|
1015
|
+
const rootStat = await fs2.lstat(root);
|
|
1016
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
|
|
1017
|
+
throw new PartitionFileIntegrityError(root, "market root must be a real directory");
|
|
1018
|
+
}
|
|
1019
|
+
return fs2.realpath(root);
|
|
1020
|
+
}
|
|
1021
|
+
async validatedTargetPath(relativePath, expectedRealRoot) {
|
|
1022
|
+
validateRelativePath(relativePath);
|
|
1023
|
+
const root = path3.resolve(this.marketRootDir);
|
|
1024
|
+
const realRoot = await this.validateMarketRoot();
|
|
1025
|
+
if (realRoot !== expectedRealRoot) {
|
|
1026
|
+
throw new PartitionFileIntegrityError(root, "market root changed during publication");
|
|
1027
|
+
}
|
|
1028
|
+
const components = relativePath.split("/");
|
|
1029
|
+
let current = root;
|
|
1030
|
+
for (const component of components.slice(0, -1)) {
|
|
1031
|
+
current = path3.join(current, component);
|
|
1032
|
+
const stat2 = await fs2.lstat(current);
|
|
1033
|
+
if (!stat2.isDirectory() || stat2.isSymbolicLink()) {
|
|
1034
|
+
throw new PartitionFileIntegrityError(
|
|
1035
|
+
current,
|
|
1036
|
+
"target path components must be real directories"
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
const realParent = await fs2.realpath(current);
|
|
1041
|
+
const containment = path3.relative(realRoot, realParent);
|
|
1042
|
+
if (containment === ".." || containment.startsWith(`..${path3.sep}`) || path3.isAbsolute(containment)) {
|
|
1043
|
+
throw new PartitionFileIntegrityError(
|
|
1044
|
+
current,
|
|
1045
|
+
"target parent escapes the configured market root"
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
return path3.join(current, components.at(-1));
|
|
1049
|
+
}
|
|
1050
|
+
async quarantineClaim(claimedPath) {
|
|
1051
|
+
try {
|
|
1052
|
+
await fs2.lstat(claimedPath);
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
if (error.code === "ENOENT") return;
|
|
1055
|
+
throw error;
|
|
1056
|
+
}
|
|
1057
|
+
const quarantineDir = path3.join(this.provenanceRootDir, "rejected-prepared");
|
|
1058
|
+
await this.ensureDurableDirectory(quarantineDir);
|
|
1059
|
+
const quarantinePath = path3.join(quarantineDir, randomUUID2());
|
|
1060
|
+
await fs2.rename(claimedPath, quarantinePath);
|
|
1061
|
+
await this.syncDirectory(path3.dirname(claimedPath));
|
|
1062
|
+
await this.syncDirectory(quarantineDir);
|
|
1063
|
+
}
|
|
1064
|
+
async restoreOrQuarantineClaim(claimedPath, preparedPath) {
|
|
1065
|
+
let restored = false;
|
|
1066
|
+
let claimedRemoved = false;
|
|
1067
|
+
try {
|
|
1068
|
+
await fs2.link(claimedPath, preparedPath);
|
|
1069
|
+
restored = true;
|
|
1070
|
+
await fs2.unlink(claimedPath);
|
|
1071
|
+
claimedRemoved = true;
|
|
1072
|
+
await this.syncDirectory(path3.dirname(preparedPath));
|
|
1073
|
+
return;
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
if (claimedRemoved) throw error;
|
|
1076
|
+
if (restored) await fs2.unlink(preparedPath).catch(() => void 0);
|
|
1077
|
+
await this.quarantineClaim(claimedPath);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
async syncDirectory(directory) {
|
|
1081
|
+
const handle = await fs2.open(directory, "r");
|
|
1082
|
+
try {
|
|
1083
|
+
await handle.sync();
|
|
1084
|
+
} finally {
|
|
1085
|
+
await handle.close();
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
async ensureDurableDirectory(directory) {
|
|
1089
|
+
const parent = path3.dirname(directory);
|
|
1090
|
+
if (parent !== directory) await this.ensureDurableDirectory(parent);
|
|
1091
|
+
try {
|
|
1092
|
+
const handle = await fs2.open(directory, "r");
|
|
1093
|
+
try {
|
|
1094
|
+
await handle.sync();
|
|
1095
|
+
} finally {
|
|
1096
|
+
await handle.close();
|
|
1097
|
+
}
|
|
1098
|
+
if (parent !== directory) await this.syncDirectory(parent);
|
|
1099
|
+
return;
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
if (error.code !== "ENOENT") throw error;
|
|
1102
|
+
}
|
|
1103
|
+
if (parent === directory) throw new Error(`Cannot create provenance directory ${directory}`);
|
|
1104
|
+
let created = false;
|
|
1105
|
+
try {
|
|
1106
|
+
await fs2.mkdir(directory);
|
|
1107
|
+
created = true;
|
|
1108
|
+
} catch (error) {
|
|
1109
|
+
if (error.code !== "EEXIST") throw error;
|
|
1110
|
+
}
|
|
1111
|
+
if (created) {
|
|
1112
|
+
await this.syncDirectory(directory);
|
|
1113
|
+
await this.syncDirectory(parent);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
isProcessAlive(pid) {
|
|
1117
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return false;
|
|
1118
|
+
try {
|
|
1119
|
+
process.kill(pid, 0);
|
|
1120
|
+
return true;
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
return error.code === "EPERM";
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
async readCanonicalFile(filePath) {
|
|
1126
|
+
try {
|
|
1127
|
+
const bytes = await fs2.readFile(filePath);
|
|
1128
|
+
const value = JSON.parse(bytes.toString("utf8"));
|
|
1129
|
+
if (!canonicalJsonBytes(value).equals(bytes)) return null;
|
|
1130
|
+
return value;
|
|
1131
|
+
} catch {
|
|
1132
|
+
return null;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
validOwner(owner, token) {
|
|
1136
|
+
return Boolean(
|
|
1137
|
+
owner && owner.kind === "tradeblocks.market-data.partition-lock-owner" && owner.version === 1 && owner.token === token && TOKEN_RE.test(owner.token) && Number.isSafeInteger(owner.pid) && owner.pid > 0 && typeof owner.hostname === "string" && owner.hostname.length > 0 && typeof owner.bootId === "string" && owner.bootId.length > 0 && Number.isSafeInteger(owner.createdAtMs) && owner.createdAtMs >= 0
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
validTicket(ticket, token) {
|
|
1141
|
+
return Boolean(
|
|
1142
|
+
ticket && ticket.kind === "tradeblocks.market-data.partition-lock-ticket" && ticket.version === 1 && ticket.token === token && Number.isSafeInteger(ticket.number) && ticket.number >= 1
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
ownerProvablyDead(owner) {
|
|
1146
|
+
if (owner.hostname !== CURRENT_HOSTNAME) return false;
|
|
1147
|
+
if (owner.bootId !== "unavailable" && CURRENT_BOOT_ID !== "unavailable" && owner.bootId !== CURRENT_BOOT_ID) {
|
|
1148
|
+
return true;
|
|
1149
|
+
}
|
|
1150
|
+
return !this.isProcessAlive(owner.pid);
|
|
1151
|
+
}
|
|
1152
|
+
async recoverClaim(claimsDir, token) {
|
|
1153
|
+
if (!TOKEN_RE.test(token)) return false;
|
|
1154
|
+
const claimPath = path3.join(claimsDir, token);
|
|
1155
|
+
let claimStat;
|
|
1156
|
+
try {
|
|
1157
|
+
claimStat = await fs2.stat(claimPath);
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
if (error.code === "ENOENT") return true;
|
|
1160
|
+
throw error;
|
|
1161
|
+
}
|
|
1162
|
+
const owner = await this.readCanonicalFile(
|
|
1163
|
+
path3.join(claimPath, "owner.json")
|
|
1164
|
+
);
|
|
1165
|
+
const validOwner = this.validOwner(owner, token);
|
|
1166
|
+
if (!validOwner) return false;
|
|
1167
|
+
const createdAtMs = owner.createdAtMs;
|
|
1168
|
+
if (Date.now() - createdAtMs < this.staleLockMs) return false;
|
|
1169
|
+
if (!this.ownerProvablyDead(owner)) return false;
|
|
1170
|
+
const quarantineDir = path3.join(path3.dirname(claimsDir), "quarantine");
|
|
1171
|
+
await this.ensureDurableDirectory(quarantineDir);
|
|
1172
|
+
const quarantinePath = path3.join(
|
|
1173
|
+
quarantineDir,
|
|
1174
|
+
`${token}-${claimStat.ino}-${Math.floor(claimStat.birthtimeMs)}`
|
|
1175
|
+
);
|
|
1176
|
+
try {
|
|
1177
|
+
await fs2.rename(claimPath, quarantinePath);
|
|
1178
|
+
await this.syncDirectory(claimsDir);
|
|
1179
|
+
await this.syncDirectory(quarantineDir);
|
|
1180
|
+
return true;
|
|
1181
|
+
} catch (error) {
|
|
1182
|
+
const code = error.code;
|
|
1183
|
+
if (code === "ENOENT" || code === "EEXIST" || code === "ENOTEMPTY") return false;
|
|
1184
|
+
throw error;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
async publishClaim(claimsDir, token) {
|
|
1188
|
+
const owner = {
|
|
1189
|
+
kind: "tradeblocks.market-data.partition-lock-owner",
|
|
1190
|
+
version: 1,
|
|
1191
|
+
token,
|
|
1192
|
+
pid: process.pid,
|
|
1193
|
+
hostname: CURRENT_HOSTNAME,
|
|
1194
|
+
bootId: CURRENT_BOOT_ID,
|
|
1195
|
+
createdAtMs: Date.now()
|
|
1196
|
+
};
|
|
1197
|
+
const publishingPath = path3.join(claimsDir, `.publishing-${token}`);
|
|
1198
|
+
const claimPath = path3.join(claimsDir, token);
|
|
1199
|
+
await fs2.mkdir(publishingPath);
|
|
1200
|
+
try {
|
|
1201
|
+
const ownerHandle = await fs2.open(path3.join(publishingPath, "owner.json"), "wx", 292);
|
|
1202
|
+
try {
|
|
1203
|
+
await ownerHandle.writeFile(canonicalJsonBytes(owner));
|
|
1204
|
+
await ownerHandle.sync();
|
|
1205
|
+
} finally {
|
|
1206
|
+
await ownerHandle.close();
|
|
1207
|
+
}
|
|
1208
|
+
await this.syncDirectory(publishingPath);
|
|
1209
|
+
await fs2.rename(publishingPath, claimPath);
|
|
1210
|
+
await this.syncDirectory(claimsDir);
|
|
1211
|
+
return claimPath;
|
|
1212
|
+
} catch (error) {
|
|
1213
|
+
await fs2.rm(publishingPath, { recursive: true, force: true });
|
|
1214
|
+
throw error;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
async assignTicket(claimsDir, claimPath, token) {
|
|
1218
|
+
let maximum = 0;
|
|
1219
|
+
for (const entry of await fs2.readdir(claimsDir, { withFileTypes: true })) {
|
|
1220
|
+
if (!entry.isDirectory() || !TOKEN_RE.test(entry.name)) continue;
|
|
1221
|
+
const ticket2 = await this.readCanonicalFile(
|
|
1222
|
+
path3.join(claimsDir, entry.name, "ticket.json")
|
|
1223
|
+
);
|
|
1224
|
+
if (this.validTicket(ticket2, entry.name)) maximum = Math.max(maximum, ticket2.number);
|
|
1225
|
+
}
|
|
1226
|
+
if (!Number.isSafeInteger(maximum + 1)) throw new Error("Partition lock ticket overflow");
|
|
1227
|
+
const ticket = {
|
|
1228
|
+
kind: "tradeblocks.market-data.partition-lock-ticket",
|
|
1229
|
+
version: 1,
|
|
1230
|
+
token,
|
|
1231
|
+
number: maximum + 1
|
|
1232
|
+
};
|
|
1233
|
+
const handle = await fs2.open(path3.join(claimPath, "ticket.json"), "wx", 292);
|
|
1234
|
+
try {
|
|
1235
|
+
await handle.writeFile(canonicalJsonBytes(ticket));
|
|
1236
|
+
await handle.sync();
|
|
1237
|
+
} finally {
|
|
1238
|
+
await handle.close();
|
|
1239
|
+
}
|
|
1240
|
+
await this.syncDirectory(claimPath);
|
|
1241
|
+
return ticket.number;
|
|
1242
|
+
}
|
|
1243
|
+
async releaseClaim(claimsDir, claimPath, token) {
|
|
1244
|
+
await partitionCommitTestFaults.get(this)?.("before-release-claim");
|
|
1245
|
+
const owner = await this.readCanonicalFile(
|
|
1246
|
+
path3.join(claimPath, "owner.json")
|
|
1247
|
+
);
|
|
1248
|
+
if (!this.validOwner(owner, token) || owner.pid !== process.pid) {
|
|
1249
|
+
throw new Error(
|
|
1250
|
+
`Refusing to release a partition claim owned by another writer: ${claimPath}`
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
const releasedDir = path3.join(path3.dirname(claimsDir), "released");
|
|
1254
|
+
await this.ensureDurableDirectory(releasedDir);
|
|
1255
|
+
const releasedPath = path3.join(releasedDir, token);
|
|
1256
|
+
await fs2.rename(claimPath, releasedPath);
|
|
1257
|
+
await this.syncDirectory(claimsDir);
|
|
1258
|
+
await this.syncDirectory(releasedDir);
|
|
1259
|
+
await fs2.rm(releasedPath, { recursive: true });
|
|
1260
|
+
await this.syncDirectory(releasedDir);
|
|
1261
|
+
}
|
|
1262
|
+
async withPartitionLock(identity, operation) {
|
|
1263
|
+
validateIdentity(identity);
|
|
1264
|
+
const lockRoot = this.lockRoot(identity);
|
|
1265
|
+
const claimsDir = path3.join(lockRoot, "claims");
|
|
1266
|
+
await this.ensureDurableDirectory(claimsDir);
|
|
1267
|
+
const token = randomUUID2();
|
|
1268
|
+
const claimPath = await this.publishClaim(claimsDir, token);
|
|
1269
|
+
let ownTicket;
|
|
1270
|
+
try {
|
|
1271
|
+
ownTicket = await this.assignTicket(claimsDir, claimPath, token);
|
|
1272
|
+
} catch (error) {
|
|
1273
|
+
try {
|
|
1274
|
+
await this.releaseClaim(claimsDir, claimPath, token);
|
|
1275
|
+
} catch (cleanupError) {
|
|
1276
|
+
throw attachPartitionLockCleanupError(error, cleanupError);
|
|
1277
|
+
}
|
|
1278
|
+
throw error;
|
|
1279
|
+
}
|
|
1280
|
+
const deadline = Date.now() + this.lockWaitMs;
|
|
1281
|
+
let acquired = false;
|
|
1282
|
+
let operationFailed = false;
|
|
1283
|
+
let operationError;
|
|
1284
|
+
try {
|
|
1285
|
+
while (Date.now() <= deadline) {
|
|
1286
|
+
let blocked = false;
|
|
1287
|
+
const entries = await fs2.readdir(claimsDir, { withFileTypes: true });
|
|
1288
|
+
for (const entry of entries) {
|
|
1289
|
+
if (!entry.isDirectory() || entry.name === token) continue;
|
|
1290
|
+
if (!TOKEN_RE.test(entry.name)) {
|
|
1291
|
+
if (!entry.name.startsWith(".publishing-")) blocked = true;
|
|
1292
|
+
continue;
|
|
1293
|
+
}
|
|
1294
|
+
const otherPath = path3.join(claimsDir, entry.name);
|
|
1295
|
+
const owner = await this.readCanonicalFile(
|
|
1296
|
+
path3.join(otherPath, "owner.json")
|
|
1297
|
+
);
|
|
1298
|
+
if (!this.validOwner(owner, entry.name)) {
|
|
1299
|
+
blocked = true;
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
const ticket = await this.readCanonicalFile(
|
|
1303
|
+
path3.join(otherPath, "ticket.json")
|
|
1304
|
+
);
|
|
1305
|
+
if (!this.validTicket(ticket, entry.name)) {
|
|
1306
|
+
try {
|
|
1307
|
+
await fs2.stat(path3.join(otherPath, "ticket.json"));
|
|
1308
|
+
blocked = true;
|
|
1309
|
+
} catch (error) {
|
|
1310
|
+
if (error.code !== "ENOENT") throw error;
|
|
1311
|
+
if (!await this.recoverClaim(claimsDir, entry.name)) blocked = true;
|
|
1312
|
+
}
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
if (ticket.number < ownTicket || ticket.number === ownTicket && entry.name < token) {
|
|
1316
|
+
if (!await this.recoverClaim(claimsDir, entry.name)) blocked = true;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
if (!blocked) {
|
|
1320
|
+
acquired = true;
|
|
1321
|
+
break;
|
|
1322
|
+
}
|
|
1323
|
+
await new Promise((resolve3) => setTimeout(resolve3, 10));
|
|
1324
|
+
}
|
|
1325
|
+
if (!acquired) throw new Error(`Timed out acquiring partition lock: ${lockRoot}`);
|
|
1326
|
+
return await operation();
|
|
1327
|
+
} catch (error) {
|
|
1328
|
+
operationFailed = true;
|
|
1329
|
+
operationError = error;
|
|
1330
|
+
throw error;
|
|
1331
|
+
} finally {
|
|
1332
|
+
try {
|
|
1333
|
+
await this.releaseClaim(claimsDir, claimPath, token);
|
|
1334
|
+
} catch (cleanupError) {
|
|
1335
|
+
if (operationFailed) {
|
|
1336
|
+
throw attachPartitionLockCleanupError(operationError, cleanupError);
|
|
1337
|
+
}
|
|
1338
|
+
throw cleanupError;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
async readIndexedEvent(identity, eventPath, address) {
|
|
1343
|
+
const bytes = await fs2.readFile(eventPath);
|
|
1344
|
+
if (addressBytes(bytes) !== address) {
|
|
1345
|
+
throw new ContentObjectCollisionError(address, eventPath);
|
|
1346
|
+
}
|
|
1347
|
+
const event = JSON.parse(bytes.toString("utf8"));
|
|
1348
|
+
if (!canonicalJsonBytes(event).equals(bytes)) {
|
|
1349
|
+
throw new ContentObjectCollisionError(address, eventPath);
|
|
1350
|
+
}
|
|
1351
|
+
if (event.kind !== PARTITION_COMMIT_EVENT_KIND || event.version !== PARTITION_COMMIT_EVENT_VERSION || event.dataset !== identity.dataset || addressCanonicalJson(event.partition) !== addressCanonicalJson(identity.partition)) {
|
|
1352
|
+
throw new Error(`Invalid partition commit event at ${eventPath}`);
|
|
1353
|
+
}
|
|
1354
|
+
parseCanonicalJsonAddress(event.receipt);
|
|
1355
|
+
if (event.previous !== void 0) parseCanonicalJsonAddress(event.previous);
|
|
1356
|
+
const commit = await this.readCommit(event.receipt);
|
|
1357
|
+
validateReceipt(commit.receipt, identity);
|
|
1358
|
+
return { event, commit };
|
|
1359
|
+
}
|
|
1360
|
+
async readAuthorityTip(identity) {
|
|
1361
|
+
const indexDir = this.eventIndexDir(identity);
|
|
1362
|
+
let entries;
|
|
1363
|
+
try {
|
|
1364
|
+
entries = await fs2.readdir(indexDir);
|
|
1365
|
+
} catch (error) {
|
|
1366
|
+
if (error.code === "ENOENT") return null;
|
|
1367
|
+
throw error;
|
|
1368
|
+
}
|
|
1369
|
+
const events = /* @__PURE__ */ new Map();
|
|
1370
|
+
for (const entry of entries) {
|
|
1371
|
+
const match = /^([0-9a-f]{64})\.json$/.exec(entry);
|
|
1372
|
+
if (!match) throw new Error(`Unexpected partition event index entry: ${entry}`);
|
|
1373
|
+
const address = `sha256:${match[1]}`;
|
|
1374
|
+
events.set(
|
|
1375
|
+
address,
|
|
1376
|
+
await this.readIndexedEvent(identity, path3.join(indexDir, entry), address)
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
if (events.size === 0) return null;
|
|
1380
|
+
const roots = [...events.entries()].filter(([, value]) => value.event.previous === void 0);
|
|
1381
|
+
if (roots.length !== 1) throw new Error("Partition event authority must have exactly one root");
|
|
1382
|
+
let [currentAddress, current] = roots[0];
|
|
1383
|
+
if (current.commit.receipt.classification !== "append") {
|
|
1384
|
+
throw new Error("Partition event root must reference an append receipt");
|
|
1385
|
+
}
|
|
1386
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1387
|
+
while (true) {
|
|
1388
|
+
if (visited.has(currentAddress))
|
|
1389
|
+
throw new Error("Partition event authority contains a cycle");
|
|
1390
|
+
visited.add(currentAddress);
|
|
1391
|
+
const children = [...events.entries()].filter(
|
|
1392
|
+
([, value]) => value.event.previous === currentAddress
|
|
1393
|
+
);
|
|
1394
|
+
if (children.length === 0) break;
|
|
1395
|
+
if (children.length !== 1)
|
|
1396
|
+
throw new Error("Partition event authority contains ambiguous tips");
|
|
1397
|
+
const [childAddress, child] = children[0];
|
|
1398
|
+
if (child.commit.receipt.classification !== "repair" || child.commit.receipt.parent !== current.commit.address) {
|
|
1399
|
+
throw new Error("Partition event authority receipt ancestry is inconsistent");
|
|
1400
|
+
}
|
|
1401
|
+
currentAddress = childAddress;
|
|
1402
|
+
current = child;
|
|
1403
|
+
}
|
|
1404
|
+
if (visited.size !== events.size) {
|
|
1405
|
+
throw new Error("Partition event authority contains a disconnected or missing-parent event");
|
|
1406
|
+
}
|
|
1407
|
+
return { eventAddress: currentAddress, commit: current.commit };
|
|
1408
|
+
}
|
|
1409
|
+
async readProjectedHead(identity) {
|
|
1410
|
+
const headPath = this.headPath(identity);
|
|
1411
|
+
let bytes;
|
|
1412
|
+
try {
|
|
1413
|
+
bytes = await fs2.readFile(headPath);
|
|
1414
|
+
} catch (error) {
|
|
1415
|
+
if (error.code === "ENOENT") return null;
|
|
1416
|
+
throw error;
|
|
1417
|
+
}
|
|
1418
|
+
try {
|
|
1419
|
+
const head = JSON.parse(bytes.toString("utf8"));
|
|
1420
|
+
if (!canonicalJsonBytes(head).equals(bytes)) return null;
|
|
1421
|
+
if (head.kind !== PARTITION_HEAD_KIND || head.version !== PARTITION_HEAD_VERSION || head.dataset !== identity.dataset || addressCanonicalJson(head.partition) !== addressCanonicalJson(identity.partition)) {
|
|
1422
|
+
return null;
|
|
1423
|
+
}
|
|
1424
|
+
parseCanonicalJsonAddress(head.receipt);
|
|
1425
|
+
parseCanonicalJsonAddress(head.event);
|
|
1426
|
+
return head;
|
|
1427
|
+
} catch {
|
|
1428
|
+
return null;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
async writeHead(identity, tip) {
|
|
1432
|
+
const headPath = this.headPath(identity);
|
|
1433
|
+
await this.ensureDurableDirectory(path3.dirname(headPath));
|
|
1434
|
+
const tempPath = `${headPath}.tmp-${randomUUID2()}`;
|
|
1435
|
+
const head = {
|
|
1436
|
+
kind: PARTITION_HEAD_KIND,
|
|
1437
|
+
version: PARTITION_HEAD_VERSION,
|
|
1438
|
+
dataset: identity.dataset,
|
|
1439
|
+
partition: identity.partition,
|
|
1440
|
+
receipt: tip.commit.address,
|
|
1441
|
+
event: tip.eventAddress
|
|
1442
|
+
};
|
|
1443
|
+
let handle;
|
|
1444
|
+
try {
|
|
1445
|
+
handle = await fs2.open(tempPath, "wx", 420);
|
|
1446
|
+
await handle.writeFile(canonicalJsonBytes(head));
|
|
1447
|
+
await handle.sync();
|
|
1448
|
+
await handle.close();
|
|
1449
|
+
handle = void 0;
|
|
1450
|
+
await fs2.rename(tempPath, headPath);
|
|
1451
|
+
await this.syncDirectory(path3.dirname(headPath));
|
|
1452
|
+
} catch (error) {
|
|
1453
|
+
await handle?.close();
|
|
1454
|
+
await fs2.unlink(tempPath).catch(() => void 0);
|
|
1455
|
+
throw error;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
async authorityWithRebuiltHead(identity) {
|
|
1459
|
+
const tip = await this.readAuthorityTip(identity);
|
|
1460
|
+
const head = await this.readProjectedHead(identity);
|
|
1461
|
+
if (!tip) {
|
|
1462
|
+
if (head) throw new Error("Partition head exists without immutable event authority");
|
|
1463
|
+
return null;
|
|
1464
|
+
}
|
|
1465
|
+
if (head?.receipt !== tip.commit.address || head.event !== tip.eventAddress) {
|
|
1466
|
+
await this.writeHead(identity, tip);
|
|
1467
|
+
}
|
|
1468
|
+
return tip;
|
|
1469
|
+
}
|
|
1470
|
+
async appendEvent(identity, receipt, previous) {
|
|
1471
|
+
const event = {
|
|
1472
|
+
kind: PARTITION_COMMIT_EVENT_KIND,
|
|
1473
|
+
version: PARTITION_COMMIT_EVENT_VERSION,
|
|
1474
|
+
dataset: identity.dataset,
|
|
1475
|
+
partition: identity.partition,
|
|
1476
|
+
receipt,
|
|
1477
|
+
...previous ? { previous } : {}
|
|
1478
|
+
};
|
|
1479
|
+
const stored = await this.objects.put(event);
|
|
1480
|
+
const indexDir = this.eventIndexDir(identity);
|
|
1481
|
+
await this.ensureDurableDirectory(indexDir);
|
|
1482
|
+
const digest = parseCanonicalJsonAddress(stored.address);
|
|
1483
|
+
const eventPath = path3.join(indexDir, `${digest}.json`);
|
|
1484
|
+
try {
|
|
1485
|
+
await fs2.link(stored.path, eventPath);
|
|
1486
|
+
await this.syncDirectory(indexDir);
|
|
1487
|
+
} catch (error) {
|
|
1488
|
+
if (error.code !== "EEXIST") throw error;
|
|
1489
|
+
const existing = await fs2.readFile(eventPath);
|
|
1490
|
+
if (addressBytes(existing) !== stored.address || !existing.equals(canonicalJsonBytes(event))) {
|
|
1491
|
+
throw new ContentObjectCollisionError(stored.address, eventPath);
|
|
1492
|
+
}
|
|
1493
|
+
const handle = await fs2.open(eventPath, "r");
|
|
1494
|
+
try {
|
|
1495
|
+
await handle.sync();
|
|
1496
|
+
} finally {
|
|
1497
|
+
await handle.close();
|
|
1498
|
+
}
|
|
1499
|
+
await this.syncDirectory(indexDir);
|
|
1500
|
+
}
|
|
1501
|
+
return stored.address;
|
|
1502
|
+
}
|
|
1503
|
+
async readCommit(address) {
|
|
1504
|
+
parseCanonicalJsonAddress(address);
|
|
1505
|
+
const receipt = await this.objects.get(address);
|
|
1506
|
+
validateReceipt(receipt, receipt);
|
|
1507
|
+
return Object.freeze({ address, receipt, created: false });
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Adopt an existing canonical Parquet partition after producer-owned schema,
|
|
1511
|
+
* identity, coverage, row-count, and exact-byte inspection.
|
|
1512
|
+
*/
|
|
1513
|
+
async [INTERNAL_HISTORICAL_PARTITION_ADOPTION](conn, identity) {
|
|
1514
|
+
const captured = captureIdentity(identity);
|
|
1515
|
+
const definition = canonicalPartitionDataset(captured.dataset);
|
|
1516
|
+
const relativePath = canonicalRelativePath(captured);
|
|
1517
|
+
const targetPath = path3.resolve(this.targetPath(relativePath));
|
|
1518
|
+
return this.withPartitionLock(captured, async () => {
|
|
1519
|
+
const realMarketRoot = await this.validateMarketRoot();
|
|
1520
|
+
const validatedTargetPath = await this.validatedTargetPath(relativePath, realMarketRoot);
|
|
1521
|
+
if (validatedTargetPath !== targetPath) {
|
|
1522
|
+
throw new PartitionFileIntegrityError(
|
|
1523
|
+
validatedTargetPath,
|
|
1524
|
+
"validated target differs from the registered path"
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1527
|
+
const opened = await openRegularFileNoFollow(targetPath);
|
|
1528
|
+
const snapshotRoot = path3.join(this.provenanceRootDir, "inspection-snapshots");
|
|
1529
|
+
await this.ensureDurableDirectory(snapshotRoot);
|
|
1530
|
+
const snapshotDir = await fs2.mkdtemp(path3.join(snapshotRoot, "adopt-"));
|
|
1531
|
+
const snapshotPath = path3.join(snapshotDir, "partition.parquet");
|
|
1532
|
+
try {
|
|
1533
|
+
const snapshotFingerprint = await snapshotOpenFile(opened.handle, snapshotPath);
|
|
1534
|
+
const snapshotObserved = await exactFileAddress(snapshotPath);
|
|
1535
|
+
if (snapshotObserved.address !== snapshotFingerprint.address || snapshotObserved.bytes !== snapshotFingerprint.bytes) {
|
|
1536
|
+
throw new PartitionFileIntegrityError(
|
|
1537
|
+
snapshotPath,
|
|
1538
|
+
"inspection snapshot bytes disagree with the pinned canonical inode"
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
await partitionCommitTestFaults.get(this)?.("historical-after-snapshot");
|
|
1542
|
+
const inspected = await inspectCanonicalParquet(conn, snapshotPath, captured);
|
|
1543
|
+
const snapshotAfter = await exactFileAddress(snapshotPath);
|
|
1544
|
+
if (snapshotAfter.address !== snapshotFingerprint.address || snapshotAfter.bytes !== snapshotFingerprint.bytes) {
|
|
1545
|
+
throw new PartitionFileIntegrityError(
|
|
1546
|
+
snapshotPath,
|
|
1547
|
+
"inspection snapshot changed during semantic inspection"
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
const canonicalAfter = await exactOpenFileAddress(opened.handle);
|
|
1551
|
+
const afterStat = await opened.handle.stat();
|
|
1552
|
+
if (!afterStat.isFile() || afterStat.nlink !== 1 || !sameInode(opened.stat, afterStat) || canonicalAfter.address !== snapshotFingerprint.address || canonicalAfter.bytes !== snapshotFingerprint.bytes) {
|
|
1553
|
+
throw new PartitionFileIntegrityError(
|
|
1554
|
+
targetPath,
|
|
1555
|
+
"canonical inode changed during historical semantic inspection"
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
await requireNamedRegularInode(targetPath, opened.stat);
|
|
1559
|
+
return this.adoptExistingFileCommit(
|
|
1560
|
+
{
|
|
1561
|
+
dataset: captured.dataset,
|
|
1562
|
+
partition: captured.partition,
|
|
1563
|
+
schemaRevision: definition.schemaRevision,
|
|
1564
|
+
relativePath,
|
|
1565
|
+
coverage: inspected.coverage,
|
|
1566
|
+
quality: { inputRows: inspected.rows, writtenRows: inspected.rows, droppedRows: 0 },
|
|
1567
|
+
file: { ...snapshotFingerprint, rows: inspected.rows }
|
|
1568
|
+
},
|
|
1569
|
+
true
|
|
1570
|
+
);
|
|
1571
|
+
} finally {
|
|
1572
|
+
await opened.handle.close();
|
|
1573
|
+
await fs2.rm(snapshotDir, { recursive: true });
|
|
1574
|
+
}
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
/**
|
|
1578
|
+
* Establish immutable authority for a canonical file that predates receipt
|
|
1579
|
+
* publication. This never replaces the data file: it pins and hashes the
|
|
1580
|
+
* existing regular inode under the partition lock, then appends authority
|
|
1581
|
+
* only when the caller-supplied rows/coverage/fingerprint agree exactly.
|
|
1582
|
+
*/
|
|
1583
|
+
async adoptExistingFileCommit(input, lockHeld = false) {
|
|
1584
|
+
const captured = captureInput(input);
|
|
1585
|
+
const targetPath = path3.resolve(this.targetPath(captured.relativePath));
|
|
1586
|
+
const realMarketRoot = await this.validateMarketRoot();
|
|
1587
|
+
const adopt = async () => {
|
|
1588
|
+
const validatedTargetPath = await this.validatedTargetPath(
|
|
1589
|
+
captured.relativePath,
|
|
1590
|
+
realMarketRoot
|
|
1591
|
+
);
|
|
1592
|
+
if (validatedTargetPath !== targetPath) {
|
|
1593
|
+
throw new PartitionFileIntegrityError(
|
|
1594
|
+
validatedTargetPath,
|
|
1595
|
+
"validated target differs from the registered path"
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
const opened = await openRegularFileNoFollow(targetPath);
|
|
1599
|
+
const handle = opened.handle;
|
|
1600
|
+
const pinnedStat = opened.stat;
|
|
1601
|
+
try {
|
|
1602
|
+
if (await this.validatedTargetPath(captured.relativePath, realMarketRoot) !== targetPath) {
|
|
1603
|
+
throw new PartitionFileIntegrityError(
|
|
1604
|
+
targetPath,
|
|
1605
|
+
"target parent changed before existing-file adoption"
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1608
|
+
const observed = await exactOpenFileAddress(handle);
|
|
1609
|
+
const afterHash = await handle.stat();
|
|
1610
|
+
if (!afterHash.isFile() || afterHash.nlink !== 1 || !sameInode(pinnedStat, afterHash)) {
|
|
1611
|
+
throw new PartitionFileIntegrityError(targetPath, "existing inode changed while hashing");
|
|
1612
|
+
}
|
|
1613
|
+
await requireNamedRegularInode(targetPath, pinnedStat);
|
|
1614
|
+
if (observed.address !== captured.file.address || observed.bytes !== captured.file.bytes) {
|
|
1615
|
+
throw new Error("Existing partition bytes do not match the supplied fingerprint");
|
|
1616
|
+
}
|
|
1617
|
+
const previous = await this.authorityWithRebuiltHead(captured);
|
|
1618
|
+
if (previous && sameCommitContent(previous.commit.receipt, captured)) {
|
|
1619
|
+
return previous.commit;
|
|
1620
|
+
}
|
|
1621
|
+
const receipt = {
|
|
1622
|
+
kind: PARTITION_COMMIT_RECEIPT_KIND,
|
|
1623
|
+
version: PARTITION_COMMIT_RECEIPT_VERSION,
|
|
1624
|
+
schemaRevision: captured.schemaRevision,
|
|
1625
|
+
dataset: captured.dataset,
|
|
1626
|
+
partition: captured.partition,
|
|
1627
|
+
relativePath: captured.relativePath,
|
|
1628
|
+
coverage: captured.coverage,
|
|
1629
|
+
quality: captured.quality,
|
|
1630
|
+
file: captured.file,
|
|
1631
|
+
classification: previous ? "repair" : "append",
|
|
1632
|
+
...previous ? { parent: previous.commit.address } : {}
|
|
1633
|
+
};
|
|
1634
|
+
const published = await this.objects.put(receipt);
|
|
1635
|
+
const stored = Object.freeze({
|
|
1636
|
+
address: published.address,
|
|
1637
|
+
receipt: published.value,
|
|
1638
|
+
created: published.created
|
|
1639
|
+
});
|
|
1640
|
+
if (await this.validatedTargetPath(captured.relativePath, realMarketRoot) !== targetPath) {
|
|
1641
|
+
throw new PartitionFileIntegrityError(
|
|
1642
|
+
targetPath,
|
|
1643
|
+
"target parent changed before authority publication"
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
await requireNamedRegularInode(targetPath, pinnedStat);
|
|
1647
|
+
const beforeEvent = await exactOpenFileAddress(handle);
|
|
1648
|
+
if (beforeEvent.address !== stored.receipt.file.address || beforeEvent.bytes !== stored.receipt.file.bytes) {
|
|
1649
|
+
throw new Error("Existing partition changed before authority publication");
|
|
1650
|
+
}
|
|
1651
|
+
const eventAddress = await this.appendEvent(
|
|
1652
|
+
captured,
|
|
1653
|
+
stored.address,
|
|
1654
|
+
previous?.eventAddress
|
|
1655
|
+
);
|
|
1656
|
+
await this.writeHead(captured, { eventAddress, commit: stored });
|
|
1657
|
+
if (await this.validatedTargetPath(captured.relativePath, realMarketRoot) !== targetPath) {
|
|
1658
|
+
throw new PartitionFileIntegrityError(
|
|
1659
|
+
targetPath,
|
|
1660
|
+
"target parent changed during authority publication"
|
|
1661
|
+
);
|
|
1662
|
+
}
|
|
1663
|
+
await requireNamedRegularInode(targetPath, pinnedStat);
|
|
1664
|
+
const finalObserved = await exactOpenFileAddress(handle);
|
|
1665
|
+
const afterFinalHash = await handle.stat();
|
|
1666
|
+
if (!afterFinalHash.isFile() || afterFinalHash.nlink !== 1 || !sameInode(pinnedStat, afterFinalHash)) {
|
|
1667
|
+
throw new PartitionFileIntegrityError(
|
|
1668
|
+
targetPath,
|
|
1669
|
+
"existing inode changed during final verification"
|
|
1670
|
+
);
|
|
1671
|
+
}
|
|
1672
|
+
await requireNamedRegularInode(targetPath, pinnedStat);
|
|
1673
|
+
if (finalObserved.address !== stored.receipt.file.address || finalObserved.bytes !== stored.receipt.file.bytes) {
|
|
1674
|
+
throw new Error("Existing partition changed during authority publication");
|
|
1675
|
+
}
|
|
1676
|
+
return stored;
|
|
1677
|
+
} finally {
|
|
1678
|
+
await handle.close();
|
|
1679
|
+
}
|
|
1680
|
+
};
|
|
1681
|
+
return lockHeld ? adopt() : this.withPartitionLock(captured, adopt);
|
|
1682
|
+
}
|
|
1683
|
+
async publishFileCommit(input) {
|
|
1684
|
+
const captured = captureInput(input);
|
|
1685
|
+
const preparedPath = path3.resolve(String(input.preparedPath));
|
|
1686
|
+
const expectedTargetPath = path3.resolve(String(input.expectedTargetPath));
|
|
1687
|
+
const targetPath = path3.resolve(this.targetPath(captured.relativePath));
|
|
1688
|
+
if (expectedTargetPath !== targetPath) {
|
|
1689
|
+
throw new TypeError(
|
|
1690
|
+
`Provenance target does not match the store-owned market path: ${JSON.stringify({ expected: targetPath, observed: expectedTargetPath })}`
|
|
1691
|
+
);
|
|
1692
|
+
}
|
|
1693
|
+
if (preparedPath === targetPath || path3.dirname(preparedPath) !== path3.dirname(targetPath)) {
|
|
1694
|
+
throw new TypeError("Prepared partition file must be a distinct sibling of its target");
|
|
1695
|
+
}
|
|
1696
|
+
const realMarketRoot = await this.validateMarketRoot();
|
|
1697
|
+
return this.withPartitionLock(captured, async () => {
|
|
1698
|
+
const validatedTargetPath = await this.validatedTargetPath(
|
|
1699
|
+
captured.relativePath,
|
|
1700
|
+
realMarketRoot
|
|
1701
|
+
);
|
|
1702
|
+
if (validatedTargetPath !== targetPath) {
|
|
1703
|
+
throw new PartitionFileIntegrityError(
|
|
1704
|
+
validatedTargetPath,
|
|
1705
|
+
"validated target differs from the registered path"
|
|
1706
|
+
);
|
|
1707
|
+
}
|
|
1708
|
+
const targetDirectory = path3.dirname(targetPath);
|
|
1709
|
+
const claimedPath = path3.join(targetDirectory, `.provenance-claim-${randomUUID2()}`);
|
|
1710
|
+
let claimed = false;
|
|
1711
|
+
let targetTouched = false;
|
|
1712
|
+
let claimedHandle;
|
|
1713
|
+
let claimedStat;
|
|
1714
|
+
try {
|
|
1715
|
+
await fs2.rename(preparedPath, claimedPath);
|
|
1716
|
+
claimed = true;
|
|
1717
|
+
await this.syncDirectory(targetDirectory);
|
|
1718
|
+
const opened = await openRegularFileNoFollow(claimedPath);
|
|
1719
|
+
claimedHandle = opened.handle;
|
|
1720
|
+
claimedStat = opened.stat;
|
|
1721
|
+
await partitionCommitTestFaults.get(this)?.("after-claim-open");
|
|
1722
|
+
const prepared = await exactOpenFileAddress(claimedHandle);
|
|
1723
|
+
const afterPreparedHash = await claimedHandle.stat();
|
|
1724
|
+
if (!afterPreparedHash.isFile() || afterPreparedHash.nlink !== 1 || !sameInode(claimedStat, afterPreparedHash)) {
|
|
1725
|
+
throw new PartitionFileIntegrityError(claimedPath, "claimed inode changed while hashing");
|
|
1726
|
+
}
|
|
1727
|
+
await requireNamedRegularInode(claimedPath, claimedStat);
|
|
1728
|
+
if (prepared.address !== captured.file.address || prepared.bytes !== captured.file.bytes) {
|
|
1729
|
+
throw new Error("Prepared partition bytes do not match the supplied fingerprint");
|
|
1730
|
+
}
|
|
1731
|
+
await claimedHandle.sync();
|
|
1732
|
+
await this.syncDirectory(targetDirectory);
|
|
1733
|
+
const previous = await this.authorityWithRebuiltHead(captured);
|
|
1734
|
+
let stored;
|
|
1735
|
+
let pendingEvent;
|
|
1736
|
+
if (previous && sameCommitContent(previous.commit.receipt, captured)) {
|
|
1737
|
+
stored = previous.commit;
|
|
1738
|
+
} else {
|
|
1739
|
+
const receipt = {
|
|
1740
|
+
kind: PARTITION_COMMIT_RECEIPT_KIND,
|
|
1741
|
+
version: PARTITION_COMMIT_RECEIPT_VERSION,
|
|
1742
|
+
schemaRevision: captured.schemaRevision,
|
|
1743
|
+
dataset: captured.dataset,
|
|
1744
|
+
partition: captured.partition,
|
|
1745
|
+
relativePath: captured.relativePath,
|
|
1746
|
+
coverage: captured.coverage,
|
|
1747
|
+
quality: captured.quality,
|
|
1748
|
+
file: captured.file,
|
|
1749
|
+
classification: previous ? "repair" : "append",
|
|
1750
|
+
...previous ? { parent: previous.commit.address } : {}
|
|
1751
|
+
};
|
|
1752
|
+
const publishedReceipt = await this.objects.put(receipt);
|
|
1753
|
+
stored = {
|
|
1754
|
+
address: publishedReceipt.address,
|
|
1755
|
+
receipt: publishedReceipt.value,
|
|
1756
|
+
created: publishedReceipt.created
|
|
1757
|
+
};
|
|
1758
|
+
Object.freeze(stored);
|
|
1759
|
+
pendingEvent = { receipt: stored, previousEvent: previous?.eventAddress };
|
|
1760
|
+
}
|
|
1761
|
+
if (await this.validatedTargetPath(captured.relativePath, realMarketRoot) !== targetPath) {
|
|
1762
|
+
throw new PartitionFileIntegrityError(targetPath, "target parent changed before install");
|
|
1763
|
+
}
|
|
1764
|
+
const beforeInstall = await claimedHandle.stat();
|
|
1765
|
+
if (!beforeInstall.isFile() || beforeInstall.nlink !== 1 || !sameInode(claimedStat, beforeInstall)) {
|
|
1766
|
+
throw new PartitionFileIntegrityError(
|
|
1767
|
+
claimedPath,
|
|
1768
|
+
"claimed inode changed before install"
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
await requireNamedRegularInode(claimedPath, claimedStat);
|
|
1772
|
+
await fs2.rename(claimedPath, targetPath);
|
|
1773
|
+
claimed = false;
|
|
1774
|
+
targetTouched = true;
|
|
1775
|
+
await this.syncDirectory(targetDirectory);
|
|
1776
|
+
await requireNamedRegularInode(targetPath, claimedStat);
|
|
1777
|
+
const afterInstall = await claimedHandle.stat();
|
|
1778
|
+
if (!afterInstall.isFile() || afterInstall.nlink !== 1 || !sameInode(claimedStat, afterInstall)) {
|
|
1779
|
+
throw new PartitionFileIntegrityError(targetPath, "installed inode changed");
|
|
1780
|
+
}
|
|
1781
|
+
const observed = await exactOpenFileAddress(claimedHandle);
|
|
1782
|
+
const afterInstalledHash = await claimedHandle.stat();
|
|
1783
|
+
if (!afterInstalledHash.isFile() || afterInstalledHash.nlink !== 1 || !sameInode(claimedStat, afterInstalledHash)) {
|
|
1784
|
+
throw new PartitionFileIntegrityError(
|
|
1785
|
+
targetPath,
|
|
1786
|
+
"installed inode changed while hashing"
|
|
1787
|
+
);
|
|
1788
|
+
}
|
|
1789
|
+
await requireNamedRegularInode(targetPath, claimedStat);
|
|
1790
|
+
await claimedHandle.sync();
|
|
1791
|
+
if (observed.address !== stored.receipt.file.address || observed.bytes !== stored.receipt.file.bytes) {
|
|
1792
|
+
throw new Error("Installed partition bytes disagree with immutable commit authority");
|
|
1793
|
+
}
|
|
1794
|
+
if (pendingEvent) {
|
|
1795
|
+
const eventAddress = await this.appendEvent(
|
|
1796
|
+
captured,
|
|
1797
|
+
pendingEvent.receipt.address,
|
|
1798
|
+
pendingEvent.previousEvent
|
|
1799
|
+
);
|
|
1800
|
+
await partitionCommitTestFaults.get(this)?.("after-event-before-head");
|
|
1801
|
+
await this.writeHead(captured, { eventAddress, commit: pendingEvent.receipt });
|
|
1802
|
+
}
|
|
1803
|
+
const tip = await this.authorityWithRebuiltHead(captured);
|
|
1804
|
+
if (!tip || tip.commit.address !== stored.address) {
|
|
1805
|
+
throw new Error("Installed partition and authoritative head disagree");
|
|
1806
|
+
}
|
|
1807
|
+
if (await this.validatedTargetPath(captured.relativePath, realMarketRoot) !== targetPath) {
|
|
1808
|
+
throw new PartitionFileIntegrityError(
|
|
1809
|
+
targetPath,
|
|
1810
|
+
"target parent changed before publication completed"
|
|
1811
|
+
);
|
|
1812
|
+
}
|
|
1813
|
+
const beforeReturn = await claimedHandle.stat();
|
|
1814
|
+
if (!beforeReturn.isFile() || beforeReturn.nlink !== 1 || !sameInode(claimedStat, beforeReturn)) {
|
|
1815
|
+
throw new PartitionFileIntegrityError(
|
|
1816
|
+
targetPath,
|
|
1817
|
+
"installed inode changed before publication completed"
|
|
1818
|
+
);
|
|
1819
|
+
}
|
|
1820
|
+
await requireNamedRegularInode(targetPath, claimedStat);
|
|
1821
|
+
const finalObserved = await exactOpenFileAddress(claimedHandle);
|
|
1822
|
+
const afterFinalHash = await claimedHandle.stat();
|
|
1823
|
+
if (!afterFinalHash.isFile() || afterFinalHash.nlink !== 1 || !sameInode(claimedStat, afterFinalHash)) {
|
|
1824
|
+
throw new PartitionFileIntegrityError(
|
|
1825
|
+
targetPath,
|
|
1826
|
+
"installed inode changed during final verification"
|
|
1827
|
+
);
|
|
1828
|
+
}
|
|
1829
|
+
await requireNamedRegularInode(targetPath, claimedStat);
|
|
1830
|
+
if (finalObserved.address !== stored.receipt.file.address || finalObserved.bytes !== stored.receipt.file.bytes) {
|
|
1831
|
+
throw new Error("Installed partition changed before publication completed");
|
|
1832
|
+
}
|
|
1833
|
+
return stored;
|
|
1834
|
+
} catch (error) {
|
|
1835
|
+
if (targetTouched) {
|
|
1836
|
+
throw new PartitionFilePublicationError(targetPath, captured.file, error);
|
|
1837
|
+
}
|
|
1838
|
+
throw error;
|
|
1839
|
+
} finally {
|
|
1840
|
+
await claimedHandle?.close();
|
|
1841
|
+
if (claimed) await this.restoreOrQuarantineClaim(claimedPath, preparedPath);
|
|
1842
|
+
}
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
async inspectPartition(identity) {
|
|
1846
|
+
const captured = captureIdentity(identity);
|
|
1847
|
+
const realMarketRoot = await this.validateMarketRoot();
|
|
1848
|
+
return this.withPartitionLock(captured, async () => {
|
|
1849
|
+
const tip = await this.authorityWithRebuiltHead(captured);
|
|
1850
|
+
const relativePath = tip?.commit.receipt.relativePath ?? canonicalRelativePath(captured);
|
|
1851
|
+
validateRegistryPath(captured, relativePath);
|
|
1852
|
+
let observed;
|
|
1853
|
+
try {
|
|
1854
|
+
const targetPath = await this.validatedTargetPath(relativePath, realMarketRoot);
|
|
1855
|
+
observed = await exactFileAddress(targetPath);
|
|
1856
|
+
} catch (error) {
|
|
1857
|
+
if (error.code === "ENOENT") {
|
|
1858
|
+
return tip ? { status: "missing", receipt: tip.commit } : { status: "absent" };
|
|
1859
|
+
}
|
|
1860
|
+
throw error;
|
|
1861
|
+
}
|
|
1862
|
+
if (!tip) return { status: "orphan", observed };
|
|
1863
|
+
if (tip.commit.receipt.file.address !== observed.address || tip.commit.receipt.file.bytes !== observed.bytes) {
|
|
1864
|
+
return { status: "mismatch", receipt: tip.commit, observed };
|
|
1865
|
+
}
|
|
1866
|
+
return { status: "match", receipt: tip.commit };
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
};
|
|
1870
|
+
async function publishRefreshCompletionAuthority(store, completion) {
|
|
1871
|
+
const digest = parseCanonicalJsonAddress(completion);
|
|
1872
|
+
const source = store.objects.objectPath(completion);
|
|
1873
|
+
const directory = path3.join(store.provenanceRootDir, "refresh-completions", digest.slice(0, 2));
|
|
1874
|
+
const marker = path3.join(directory, `${digest}.json`);
|
|
1875
|
+
await fs2.mkdir(directory, { recursive: true });
|
|
1876
|
+
try {
|
|
1877
|
+
await fs2.link(source, marker);
|
|
1878
|
+
} catch (error) {
|
|
1879
|
+
if (error.code !== "EEXIST") throw error;
|
|
1880
|
+
}
|
|
1881
|
+
const bytes = await fs2.readFile(marker);
|
|
1882
|
+
if (addressBytes(bytes) !== completion) {
|
|
1883
|
+
throw new ContentObjectCollisionError(completion, marker);
|
|
1884
|
+
}
|
|
1885
|
+
const handle = await fs2.open(marker, "r");
|
|
1886
|
+
try {
|
|
1887
|
+
await handle.sync();
|
|
1888
|
+
} finally {
|
|
1889
|
+
await handle.close();
|
|
1890
|
+
}
|
|
1891
|
+
const directoryHandle = await fs2.open(directory, "r");
|
|
1892
|
+
try {
|
|
1893
|
+
await directoryHandle.sync();
|
|
1894
|
+
} finally {
|
|
1895
|
+
await directoryHandle.close();
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
async function verifyRefreshCompletionAuthority(store, completion) {
|
|
1899
|
+
const digest = parseCanonicalJsonAddress(completion);
|
|
1900
|
+
const marker = path3.join(
|
|
1901
|
+
store.provenanceRootDir,
|
|
1902
|
+
"refresh-completions",
|
|
1903
|
+
digest.slice(0, 2),
|
|
1904
|
+
`${digest}.json`
|
|
1905
|
+
);
|
|
1906
|
+
const stat2 = await fs2.lstat(marker);
|
|
1907
|
+
if (!stat2.isFile() || stat2.isSymbolicLink()) {
|
|
1908
|
+
throw new Error("Canonical refresh completion has no producer authority marker");
|
|
1909
|
+
}
|
|
1910
|
+
const bytes = await fs2.readFile(marker);
|
|
1911
|
+
const object = await fs2.readFile(store.objects.objectPath(completion));
|
|
1912
|
+
if (addressBytes(bytes) !== completion || !bytes.equals(object)) {
|
|
1913
|
+
throw new Error("Canonical refresh completion authority marker is corrupt");
|
|
148
1914
|
}
|
|
149
|
-
await conn.run(`
|
|
150
|
-
CREATE TABLE IF NOT EXISTS market.spot (
|
|
151
|
-
ticker VARCHAR NOT NULL,
|
|
152
|
-
date VARCHAR NOT NULL,
|
|
153
|
-
time VARCHAR NOT NULL,
|
|
154
|
-
open DOUBLE,
|
|
155
|
-
high DOUBLE,
|
|
156
|
-
low DOUBLE,
|
|
157
|
-
close DOUBLE,
|
|
158
|
-
bid DOUBLE,
|
|
159
|
-
ask DOUBLE,
|
|
160
|
-
PRIMARY KEY (ticker, date, time)
|
|
161
|
-
)
|
|
162
|
-
`);
|
|
163
|
-
await conn.run(`
|
|
164
|
-
CREATE TABLE IF NOT EXISTS market.enriched (
|
|
165
|
-
ticker VARCHAR NOT NULL,
|
|
166
|
-
date VARCHAR NOT NULL,
|
|
167
|
-
Prior_Close DOUBLE,
|
|
168
|
-
Gap_Pct DOUBLE,
|
|
169
|
-
ATR_Pct DOUBLE,
|
|
170
|
-
RSI_14 DOUBLE,
|
|
171
|
-
Price_vs_EMA21_Pct DOUBLE,
|
|
172
|
-
Price_vs_SMA50_Pct DOUBLE,
|
|
173
|
-
Realized_Vol_5D DOUBLE,
|
|
174
|
-
Realized_Vol_20D DOUBLE,
|
|
175
|
-
Return_5D DOUBLE,
|
|
176
|
-
Return_20D DOUBLE,
|
|
177
|
-
Intraday_Range_Pct DOUBLE,
|
|
178
|
-
Intraday_Return_Pct DOUBLE,
|
|
179
|
-
Close_Position_In_Range DOUBLE,
|
|
180
|
-
Gap_Filled INTEGER,
|
|
181
|
-
Consecutive_Days INTEGER,
|
|
182
|
-
Prev_Return_Pct DOUBLE,
|
|
183
|
-
Prior_Range_vs_ATR DOUBLE,
|
|
184
|
-
High_Time DOUBLE,
|
|
185
|
-
Low_Time DOUBLE,
|
|
186
|
-
High_Before_Low INTEGER,
|
|
187
|
-
Reversal_Type INTEGER,
|
|
188
|
-
Opening_Drive_Strength DOUBLE,
|
|
189
|
-
Intraday_Realized_Vol DOUBLE,
|
|
190
|
-
Day_of_Week INTEGER,
|
|
191
|
-
Month INTEGER,
|
|
192
|
-
Is_Opex INTEGER,
|
|
193
|
-
ivr DOUBLE,
|
|
194
|
-
ivp DOUBLE,
|
|
195
|
-
PRIMARY KEY (ticker, date)
|
|
196
|
-
)
|
|
197
|
-
`);
|
|
198
|
-
await conn.run(`
|
|
199
|
-
CREATE TABLE IF NOT EXISTS market.enriched_context (
|
|
200
|
-
date VARCHAR NOT NULL,
|
|
201
|
-
Vol_Regime INTEGER,
|
|
202
|
-
Term_Structure_State INTEGER,
|
|
203
|
-
Trend_Direction VARCHAR,
|
|
204
|
-
VIX_Spike_Pct DOUBLE,
|
|
205
|
-
VIX_Gap_Pct DOUBLE,
|
|
206
|
-
PRIMARY KEY (date)
|
|
207
|
-
)
|
|
208
|
-
`);
|
|
209
1915
|
}
|
|
1916
|
+
var PartitionFilePublicationError = class extends Error {
|
|
1917
|
+
targetPath;
|
|
1918
|
+
file;
|
|
1919
|
+
constructor(targetPath, file, cause) {
|
|
1920
|
+
super(`Partition file installed without a complete projected commit: ${targetPath}`, { cause });
|
|
1921
|
+
this.name = "PartitionFilePublicationError";
|
|
1922
|
+
this.targetPath = targetPath;
|
|
1923
|
+
this.file = file;
|
|
1924
|
+
}
|
|
1925
|
+
};
|
|
210
1926
|
|
|
211
|
-
// src/
|
|
212
|
-
import
|
|
213
|
-
|
|
1927
|
+
// src/market/provenance/partition-commit-attempt.ts
|
|
1928
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
1929
|
+
var attempts = new AsyncLocalStorage();
|
|
1930
|
+
function compareCodePoints2(left, right) {
|
|
1931
|
+
const leftPoints = Array.from(left, (character) => character.codePointAt(0));
|
|
1932
|
+
const rightPoints = Array.from(right, (character) => character.codePointAt(0));
|
|
1933
|
+
const length = Math.min(leftPoints.length, rightPoints.length);
|
|
1934
|
+
for (let index = 0; index < length; index++) {
|
|
1935
|
+
if (leftPoints[index] !== rightPoints[index]) return leftPoints[index] - rightPoints[index];
|
|
1936
|
+
}
|
|
1937
|
+
return leftPoints.length - rightPoints.length;
|
|
1938
|
+
}
|
|
1939
|
+
function commitTuple(commit) {
|
|
1940
|
+
const partition = Object.entries(commit.receipt.partition).sort(([left], [right]) => compareCodePoints2(left, right)).flatMap(([key, value]) => [key, value]);
|
|
1941
|
+
return [commit.receipt.dataset, ...partition, commit.address];
|
|
1942
|
+
}
|
|
1943
|
+
function compareTuple(left, right) {
|
|
1944
|
+
const length = Math.min(left.length, right.length);
|
|
1945
|
+
for (let index = 0; index < length; index++) {
|
|
1946
|
+
const compared = compareCodePoints2(left[index], right[index]);
|
|
1947
|
+
if (compared !== 0) return compared;
|
|
1948
|
+
}
|
|
1949
|
+
return left.length - right.length;
|
|
1950
|
+
}
|
|
1951
|
+
async function runPartitionCommitAttempt(options, operation) {
|
|
1952
|
+
if (options.attemptId.trim().length === 0) {
|
|
1953
|
+
throw new TypeError("Partition commit attemptId must not be empty");
|
|
1954
|
+
}
|
|
1955
|
+
const active = { ...options, receiptAddresses: [] };
|
|
1956
|
+
const value = await attempts.run(active, operation);
|
|
1957
|
+
const receipts = await Promise.all(
|
|
1958
|
+
[...new Set(active.receiptAddresses)].map((address) => active.recorder.readCommit(address))
|
|
1959
|
+
);
|
|
1960
|
+
receipts.sort((left, right) => compareTuple(commitTuple(left), commitTuple(right)));
|
|
1961
|
+
return {
|
|
1962
|
+
attemptId: active.attemptId,
|
|
1963
|
+
value,
|
|
1964
|
+
receipts: Object.freeze(receipts)
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
function activePartitionCommitAttempt() {
|
|
1968
|
+
return attempts.getStore();
|
|
1969
|
+
}
|
|
1970
|
+
function capturePartitionCommitReceipt(receipt) {
|
|
1971
|
+
const active = attempts.getStore();
|
|
1972
|
+
if (!active) return;
|
|
1973
|
+
active.receiptAddresses.push(receipt.address);
|
|
1974
|
+
}
|
|
214
1975
|
|
|
215
1976
|
// src/db/market-datasets.ts
|
|
216
|
-
import * as
|
|
1977
|
+
import * as path5 from "path";
|
|
217
1978
|
|
|
218
1979
|
// src/db/data-root.ts
|
|
219
|
-
import
|
|
1980
|
+
import path4 from "path";
|
|
220
1981
|
var KEY = "__tradeblocks_data_root__";
|
|
221
1982
|
function resolveDbPath(dataRoot, kind) {
|
|
222
|
-
return
|
|
1983
|
+
return path4.join(dataRoot, "database", `${kind}.duckdb`);
|
|
223
1984
|
}
|
|
224
1985
|
function setDataRoot(dir) {
|
|
225
1986
|
globalThis[KEY] = dir;
|
|
@@ -242,23 +2003,26 @@ var PARTITIONED_DATASETS = {
|
|
|
242
2003
|
option_quote_minutes: "option_quote_minutes"
|
|
243
2004
|
};
|
|
244
2005
|
function resolveMarketDir(dataDir) {
|
|
245
|
-
return
|
|
2006
|
+
return path5.join(getDataRoot(dataDir), "market");
|
|
246
2007
|
}
|
|
247
2008
|
function resolveCanonicalMarketFile(dataDir, dataset) {
|
|
248
|
-
return
|
|
2009
|
+
return path5.join(resolveMarketDir(dataDir), SINGLE_FILE_DATASETS[dataset]);
|
|
249
2010
|
}
|
|
250
2011
|
function resolveCanonicalMarketPartitionDir(dataDir, dataset) {
|
|
251
|
-
return
|
|
2012
|
+
return path5.join(resolveMarketDir(dataDir), PARTITIONED_DATASETS[dataset]);
|
|
252
2013
|
}
|
|
253
2014
|
function resolveCanonicalMarketPartitionPath(dataDir, dataset, date) {
|
|
254
|
-
return
|
|
2015
|
+
return path5.join(resolveCanonicalMarketPartitionDir(dataDir, dataset), `date=${date}`);
|
|
255
2016
|
}
|
|
256
2017
|
function resolveCanonicalMarketPartitionFile(dataDir, dataset, date) {
|
|
257
|
-
return
|
|
2018
|
+
return path5.join(resolveCanonicalMarketPartitionPath(dataDir, dataset, date), "data.parquet");
|
|
258
2019
|
}
|
|
259
2020
|
function canonicalMarketTableName(dataset) {
|
|
260
2021
|
return `market.${dataset}`;
|
|
261
2022
|
}
|
|
2023
|
+
function provenanceOptions(dataset, partition, schemaRevision, relativePath, coverage, quality) {
|
|
2024
|
+
return { dataset, partition, schemaRevision, relativePath, coverage, quality };
|
|
2025
|
+
}
|
|
262
2026
|
var DATASETS_V3 = {
|
|
263
2027
|
spot: { subdir: "spot", partitionKeys: ["ticker", "date"], filename: "data.parquet" },
|
|
264
2028
|
enriched: { subdir: "enriched", partitionKeys: ["ticker"], filename: "data.parquet" },
|
|
@@ -279,96 +2043,359 @@ var DATASETS_V3 = {
|
|
|
279
2043
|
filename: "data.parquet"
|
|
280
2044
|
}
|
|
281
2045
|
};
|
|
2046
|
+
var BOUNDED_PROVENANCE_DATASETS = MARKET_DATASETS;
|
|
2047
|
+
function assertCanonicalPartitionDate(value, helper) {
|
|
2048
|
+
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
2049
|
+
throw new TypeError(
|
|
2050
|
+
`${helper}: date must be an ISO calendar date (YYYY-MM-DD): ${JSON.stringify(value)}`
|
|
2051
|
+
);
|
|
2052
|
+
}
|
|
2053
|
+
const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
|
|
2054
|
+
if (!Number.isFinite(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== value) {
|
|
2055
|
+
throw new TypeError(`${helper}: date is not a real calendar date: ${JSON.stringify(value)}`);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
282
2058
|
async function writeSpotPartition(conn, args) {
|
|
283
|
-
|
|
2059
|
+
assertCanonicalPartitionDate(args.date, "writeSpotPartition");
|
|
2060
|
+
const def = BOUNDED_PROVENANCE_DATASETS.spot;
|
|
284
2061
|
return writeParquetPartition(conn, {
|
|
285
|
-
baseDir:
|
|
2062
|
+
baseDir: path5.join(resolveMarketDir(args.dataDir), def.subdir),
|
|
286
2063
|
partitions: { ticker: args.ticker, date: args.date },
|
|
287
2064
|
// order matches def.partitionKeys
|
|
288
2065
|
selectQuery: args.selectQuery,
|
|
289
2066
|
compression: args.compression,
|
|
290
|
-
filename: def.filename
|
|
2067
|
+
filename: def.filename,
|
|
2068
|
+
provenance: provenanceOptions(
|
|
2069
|
+
"spot",
|
|
2070
|
+
{ ticker: args.ticker, date: args.date },
|
|
2071
|
+
def.schemaRevision,
|
|
2072
|
+
path5.posix.join(def.subdir, `ticker=${args.ticker}`, `date=${args.date}`, def.filename),
|
|
2073
|
+
{ kind: "prepared-date-range", column: "date" },
|
|
2074
|
+
args.quality
|
|
2075
|
+
)
|
|
291
2076
|
});
|
|
292
2077
|
}
|
|
293
2078
|
async function writeChainPartition(conn, args) {
|
|
294
|
-
|
|
2079
|
+
assertCanonicalPartitionDate(args.date, "writeChainPartition");
|
|
2080
|
+
const def = BOUNDED_PROVENANCE_DATASETS.option_chain;
|
|
295
2081
|
return writeParquetPartition(conn, {
|
|
296
|
-
baseDir:
|
|
2082
|
+
baseDir: path5.join(resolveMarketDir(args.dataDir), def.subdir),
|
|
297
2083
|
partitions: { underlying: args.underlying, date: args.date },
|
|
298
2084
|
selectQuery: args.selectQuery,
|
|
299
2085
|
compression: args.compression,
|
|
300
|
-
filename: def.filename
|
|
2086
|
+
filename: def.filename,
|
|
2087
|
+
provenance: provenanceOptions(
|
|
2088
|
+
"option_chain",
|
|
2089
|
+
{ underlying: args.underlying, date: args.date },
|
|
2090
|
+
def.schemaRevision,
|
|
2091
|
+
path5.posix.join(
|
|
2092
|
+
def.subdir,
|
|
2093
|
+
`underlying=${args.underlying}`,
|
|
2094
|
+
`date=${args.date}`,
|
|
2095
|
+
def.filename
|
|
2096
|
+
),
|
|
2097
|
+
{ kind: "prepared-date-range", column: "date" },
|
|
2098
|
+
args.quality
|
|
2099
|
+
)
|
|
301
2100
|
});
|
|
302
2101
|
}
|
|
303
2102
|
async function writeQuoteMinutesPartition(conn, args) {
|
|
304
|
-
|
|
2103
|
+
assertCanonicalPartitionDate(args.date, "writeQuoteMinutesPartition");
|
|
2104
|
+
const def = BOUNDED_PROVENANCE_DATASETS.option_quote_minutes;
|
|
305
2105
|
const sortedSelect = `SELECT * FROM (${args.selectQuery}) AS q ORDER BY q.ticker, q.time`;
|
|
306
2106
|
return writeParquetPartition(conn, {
|
|
307
|
-
baseDir:
|
|
2107
|
+
baseDir: path5.join(resolveMarketDir(args.dataDir), def.subdir),
|
|
308
2108
|
partitions: { underlying: args.underlying, date: args.date },
|
|
309
2109
|
selectQuery: sortedSelect,
|
|
310
2110
|
compression: args.compression,
|
|
311
|
-
filename: def.filename
|
|
2111
|
+
filename: def.filename,
|
|
2112
|
+
provenance: provenanceOptions(
|
|
2113
|
+
"option_quote_minutes",
|
|
2114
|
+
{ underlying: args.underlying, date: args.date },
|
|
2115
|
+
def.schemaRevision,
|
|
2116
|
+
path5.posix.join(
|
|
2117
|
+
def.subdir,
|
|
2118
|
+
`underlying=${args.underlying}`,
|
|
2119
|
+
`date=${args.date}`,
|
|
2120
|
+
def.filename
|
|
2121
|
+
),
|
|
2122
|
+
{ kind: "prepared-date-range", column: "date" },
|
|
2123
|
+
args.quality
|
|
2124
|
+
)
|
|
312
2125
|
});
|
|
313
2126
|
}
|
|
314
2127
|
async function writeOiDailyPartition(conn, args) {
|
|
315
|
-
|
|
2128
|
+
assertCanonicalPartitionDate(args.date, "writeOiDailyPartition");
|
|
2129
|
+
const def = BOUNDED_PROVENANCE_DATASETS.option_oi_daily;
|
|
316
2130
|
const sortedSelect = `SELECT * FROM (${args.selectQuery}) AS q ORDER BY q.ticker`;
|
|
317
2131
|
return writeParquetPartition(conn, {
|
|
318
|
-
baseDir:
|
|
2132
|
+
baseDir: path5.join(resolveMarketDir(args.dataDir), def.subdir),
|
|
319
2133
|
partitions: { underlying: args.underlying, date: args.date },
|
|
320
2134
|
selectQuery: sortedSelect,
|
|
321
2135
|
compression: args.compression,
|
|
322
|
-
filename: def.filename
|
|
2136
|
+
filename: def.filename,
|
|
2137
|
+
provenance: provenanceOptions(
|
|
2138
|
+
"option_oi_daily",
|
|
2139
|
+
{ underlying: args.underlying, date: args.date },
|
|
2140
|
+
def.schemaRevision,
|
|
2141
|
+
path5.posix.join(
|
|
2142
|
+
def.subdir,
|
|
2143
|
+
`underlying=${args.underlying}`,
|
|
2144
|
+
`date=${args.date}`,
|
|
2145
|
+
def.filename
|
|
2146
|
+
),
|
|
2147
|
+
{ kind: "prepared-date-range", column: "date" },
|
|
2148
|
+
args.quality
|
|
2149
|
+
)
|
|
323
2150
|
});
|
|
324
2151
|
}
|
|
325
2152
|
async function writeEnrichedTickerFile(conn, args) {
|
|
326
2153
|
const def = DATASETS_V3.enriched;
|
|
327
2154
|
return writeParquetPartition(conn, {
|
|
328
|
-
baseDir:
|
|
2155
|
+
baseDir: path5.join(resolveMarketDir(args.dataDir), def.subdir),
|
|
329
2156
|
partitions: { ticker: args.ticker },
|
|
330
2157
|
selectQuery: args.selectQuery,
|
|
331
2158
|
compression: args.compression,
|
|
332
2159
|
filename: def.filename
|
|
333
2160
|
});
|
|
334
2161
|
}
|
|
2162
|
+
async function writeEnrichedTickerPartition(conn, args) {
|
|
2163
|
+
assertCanonicalPartitionDate(args.date, "writeEnrichedTickerPartition");
|
|
2164
|
+
const def = BOUNDED_PROVENANCE_DATASETS.enriched;
|
|
2165
|
+
return writeParquetPartition(conn, {
|
|
2166
|
+
baseDir: path5.join(resolveMarketDir(args.dataDir), def.subdir),
|
|
2167
|
+
partitions: { ticker: args.ticker, date: args.date },
|
|
2168
|
+
selectQuery: args.selectQuery,
|
|
2169
|
+
compression: args.compression,
|
|
2170
|
+
filename: def.filename,
|
|
2171
|
+
provenance: provenanceOptions(
|
|
2172
|
+
"enriched",
|
|
2173
|
+
{ ticker: args.ticker, date: args.date },
|
|
2174
|
+
def.schemaRevision,
|
|
2175
|
+
path5.posix.join(def.subdir, `ticker=${args.ticker}`, `date=${args.date}`, def.filename),
|
|
2176
|
+
{ kind: "prepared-date-range", column: "date" },
|
|
2177
|
+
args.quality
|
|
2178
|
+
)
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
335
2181
|
async function writeEnrichedContext(conn, args) {
|
|
336
2182
|
const def = DATASETS_V3.enriched_context;
|
|
337
|
-
const targetPath = path2.join(resolveMarketDir(args.dataDir), def.subdir, def.filename);
|
|
338
2183
|
return writeParquetAtomic(conn, {
|
|
339
|
-
targetPath,
|
|
2184
|
+
targetPath: path5.join(resolveMarketDir(args.dataDir), def.subdir, def.filename),
|
|
340
2185
|
selectQuery: args.selectQuery,
|
|
341
2186
|
compression: args.compression
|
|
342
2187
|
});
|
|
343
2188
|
}
|
|
2189
|
+
async function writeEnrichedContextPartition(conn, args) {
|
|
2190
|
+
assertCanonicalPartitionDate(args.date, "writeEnrichedContextPartition");
|
|
2191
|
+
const completeness = await conn.runAndReadAll(
|
|
2192
|
+
`SELECT date, Vol_Regime, Term_Structure_State, Trend_Direction,
|
|
2193
|
+
VIX_Spike_Pct, VIX_Gap_Pct
|
|
2194
|
+
FROM (${args.selectQuery}) AS bounded_enriched_context`
|
|
2195
|
+
);
|
|
2196
|
+
const rows = completeness.getRows();
|
|
2197
|
+
if (rows.length !== 1) {
|
|
2198
|
+
throw new Error(
|
|
2199
|
+
`Enriched context partition ${args.date} must contain exactly one logical session row`
|
|
2200
|
+
);
|
|
2201
|
+
}
|
|
2202
|
+
const [date, volRegime, termStructure, trendDirection, vixSpikePct, vixGapPct] = rows[0];
|
|
2203
|
+
if (String(date) !== args.date) {
|
|
2204
|
+
throw new Error(`Enriched context partition ${args.date} contains a different logical date`);
|
|
2205
|
+
}
|
|
2206
|
+
if (!Number.isInteger(volRegime) || Number(volRegime) < 1 || Number(volRegime) > 6 || !Number.isInteger(termStructure) || ![-1, 0, 1].includes(Number(termStructure)) || typeof trendDirection !== "string" || !["up", "down", "flat"].includes(trendDirection) || typeof vixSpikePct !== "number" || !Number.isFinite(vixSpikePct) || typeof vixGapPct !== "number" || !Number.isFinite(vixGapPct)) {
|
|
2207
|
+
throw new Error(
|
|
2208
|
+
`Enriched context partition ${args.date} is missing required VIX completeness fields`
|
|
2209
|
+
);
|
|
2210
|
+
}
|
|
2211
|
+
const def = BOUNDED_PROVENANCE_DATASETS.enriched_context;
|
|
2212
|
+
return writeParquetPartition(conn, {
|
|
2213
|
+
baseDir: path5.join(resolveMarketDir(args.dataDir), def.subdir),
|
|
2214
|
+
partitions: { date: args.date },
|
|
2215
|
+
selectQuery: args.selectQuery,
|
|
2216
|
+
compression: args.compression,
|
|
2217
|
+
filename: def.filename,
|
|
2218
|
+
provenance: provenanceOptions(
|
|
2219
|
+
"enriched_context",
|
|
2220
|
+
{ date: args.date },
|
|
2221
|
+
def.schemaRevision,
|
|
2222
|
+
path5.posix.join(def.subdir, `date=${args.date}`, def.filename),
|
|
2223
|
+
{ kind: "prepared-date-range", column: "date" },
|
|
2224
|
+
args.quality
|
|
2225
|
+
)
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
344
2228
|
|
|
345
2229
|
// src/db/parquet-writer.ts
|
|
346
2230
|
function isParquetMode() {
|
|
347
2231
|
return process.env.TRADEBLOCKS_PARQUET === "true";
|
|
348
2232
|
}
|
|
2233
|
+
function captureWriteProvenance(provenance) {
|
|
2234
|
+
if (!provenance) return void 0;
|
|
2235
|
+
const captured = {
|
|
2236
|
+
dataset: provenance.dataset,
|
|
2237
|
+
partition: provenance.partition,
|
|
2238
|
+
schemaRevision: provenance.schemaRevision,
|
|
2239
|
+
relativePath: provenance.relativePath,
|
|
2240
|
+
coverage: provenance.coverage,
|
|
2241
|
+
...provenance.quality === void 0 ? {} : { quality: provenance.quality }
|
|
2242
|
+
};
|
|
2243
|
+
return JSON.parse(canonicalJsonBytes(captured).toString("utf8"));
|
|
2244
|
+
}
|
|
2245
|
+
var ParquetProvenanceOrphanError = class extends Error {
|
|
2246
|
+
targetPath;
|
|
2247
|
+
file;
|
|
2248
|
+
constructor(targetPath, file, cause) {
|
|
2249
|
+
super(`Parquet data committed without provenance receipt: ${targetPath}`, { cause });
|
|
2250
|
+
this.name = "ParquetProvenanceOrphanError";
|
|
2251
|
+
this.targetPath = targetPath;
|
|
2252
|
+
this.file = file;
|
|
2253
|
+
}
|
|
2254
|
+
};
|
|
2255
|
+
var UnmanifestedParquetWriteError = class extends Error {
|
|
2256
|
+
targetPath;
|
|
2257
|
+
constructor(targetPath) {
|
|
2258
|
+
super(`Active partition commit attempt refuses an unregistered Parquet write: ${targetPath}`);
|
|
2259
|
+
this.name = "UnmanifestedParquetWriteError";
|
|
2260
|
+
this.targetPath = targetPath;
|
|
2261
|
+
}
|
|
2262
|
+
};
|
|
2263
|
+
async function exactFileFingerprint(filePath, rows) {
|
|
2264
|
+
const hash = createHash3("sha256");
|
|
2265
|
+
let bytes = 0;
|
|
2266
|
+
for await (const chunk of createReadStream(filePath)) {
|
|
2267
|
+
const buffer = chunk;
|
|
2268
|
+
bytes += buffer.byteLength;
|
|
2269
|
+
hash.update(buffer);
|
|
2270
|
+
}
|
|
2271
|
+
return { address: `sha256:${hash.digest("hex")}`, bytes, rows };
|
|
2272
|
+
}
|
|
2273
|
+
function escapeSqlLiteral2(value) {
|
|
2274
|
+
return value.replaceAll("'", "''");
|
|
2275
|
+
}
|
|
2276
|
+
async function inspectPreparedParquet(conn, preparedPath, coverage) {
|
|
2277
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(coverage.column)) {
|
|
2278
|
+
throw new TypeError(`Unsafe provenance coverage column: ${JSON.stringify(coverage.column)}`);
|
|
2279
|
+
}
|
|
2280
|
+
const source = `read_parquet('${escapeSqlLiteral2(preparedPath)}', hive_partitioning=false)`;
|
|
2281
|
+
const reader = await conn.runAndReadAll(
|
|
2282
|
+
`SELECT COUNT(*)::BIGINT,
|
|
2283
|
+
COUNT("${coverage.column}")::BIGINT,
|
|
2284
|
+
MIN(CAST("${coverage.column}" AS VARCHAR)),
|
|
2285
|
+
MAX(CAST("${coverage.column}" AS VARCHAR))
|
|
2286
|
+
FROM ${source}`
|
|
2287
|
+
);
|
|
2288
|
+
const row = reader.getRows()[0];
|
|
2289
|
+
const rowCount = Number(row[0]);
|
|
2290
|
+
const coveredRows = Number(row[1]);
|
|
2291
|
+
if (!Number.isSafeInteger(rowCount) || rowCount < 0 || !Number.isSafeInteger(coveredRows)) {
|
|
2292
|
+
throw new Error("Prepared Parquet returned an invalid row count");
|
|
2293
|
+
}
|
|
2294
|
+
if (rowCount === 0) {
|
|
2295
|
+
if (coveredRows !== 0 || row[2] != null || row[3] != null) {
|
|
2296
|
+
throw new Error("Empty prepared Parquet returned inconsistent logical coverage");
|
|
2297
|
+
}
|
|
2298
|
+
return { rowCount, coverage: { kind: "empty" } };
|
|
2299
|
+
}
|
|
2300
|
+
if (coveredRows !== rowCount || row[2] == null || row[3] == null) {
|
|
2301
|
+
throw new Error(
|
|
2302
|
+
`Prepared Parquet contains NULL or incomplete logical coverage in ${coverage.column}`
|
|
2303
|
+
);
|
|
2304
|
+
}
|
|
2305
|
+
return {
|
|
2306
|
+
rowCount,
|
|
2307
|
+
coverage: { kind: "date-range", from: String(row[2]), through: String(row[3]) }
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
async function syncFile(filePath) {
|
|
2311
|
+
const handle = await fs3.open(filePath, "r");
|
|
2312
|
+
try {
|
|
2313
|
+
await handle.sync();
|
|
2314
|
+
} finally {
|
|
2315
|
+
await handle.close();
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
async function syncDirectory(directory) {
|
|
2319
|
+
const handle = await fs3.open(directory, "r");
|
|
2320
|
+
try {
|
|
2321
|
+
await handle.sync();
|
|
2322
|
+
} finally {
|
|
2323
|
+
await handle.close();
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
function resolveQuality(rowCount, quality) {
|
|
2327
|
+
if (!quality) {
|
|
2328
|
+
throw new Error("Partition provenance requires explicit inputRows and droppedRows");
|
|
2329
|
+
}
|
|
2330
|
+
if ("kind" in quality) {
|
|
2331
|
+
return { inputRows: rowCount, writtenRows: rowCount, droppedRows: 0 };
|
|
2332
|
+
}
|
|
2333
|
+
if (!Number.isSafeInteger(quality.inputRows) || !Number.isSafeInteger(quality.droppedRows) || quality.inputRows < 0 || quality.droppedRows < 0 || quality.inputRows !== rowCount + quality.droppedRows) {
|
|
2334
|
+
throw new Error(
|
|
2335
|
+
`Partition provenance quality must satisfy inputRows (${quality.inputRows}) = writtenRows (${rowCount}) + droppedRows (${quality.droppedRows})`
|
|
2336
|
+
);
|
|
2337
|
+
}
|
|
2338
|
+
return {
|
|
2339
|
+
inputRows: quality.inputRows,
|
|
2340
|
+
writtenRows: rowCount,
|
|
2341
|
+
droppedRows: quality.droppedRows
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
349
2344
|
async function writeParquetAtomic(conn, opts) {
|
|
350
2345
|
const {
|
|
351
2346
|
targetPath,
|
|
352
2347
|
selectQuery,
|
|
353
2348
|
stagingName = `_staging_${Date.now()}`,
|
|
354
|
-
compression = "ZSTD"
|
|
2349
|
+
compression = "ZSTD",
|
|
2350
|
+
provenance: callerProvenance
|
|
355
2351
|
} = opts;
|
|
2352
|
+
const provenance = captureWriteProvenance(callerProvenance);
|
|
2353
|
+
const provenanceAttempt = activePartitionCommitAttempt();
|
|
2354
|
+
if (provenanceAttempt && !provenance) throw new UnmanifestedParquetWriteError(targetPath);
|
|
356
2355
|
const tempPath = `${targetPath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
357
2356
|
try {
|
|
358
2357
|
await conn.run(`CREATE TEMP TABLE "${stagingName}" AS ${selectQuery}`);
|
|
359
|
-
await
|
|
2358
|
+
await fs3.mkdir(path6.dirname(targetPath), { recursive: true });
|
|
360
2359
|
await conn.run(
|
|
361
2360
|
`COPY "${stagingName}" TO '${tempPath}' (FORMAT PARQUET, COMPRESSION ${compression})`
|
|
362
2361
|
);
|
|
363
|
-
await
|
|
364
|
-
const
|
|
365
|
-
`SELECT COUNT(*)::INTEGER AS cnt FROM "${stagingName}"`
|
|
2362
|
+
const inspected = provenanceAttempt ? await inspectPreparedParquet(conn, tempPath, provenance.coverage) : void 0;
|
|
2363
|
+
const rowCount = inspected ? inspected.rowCount : Number(
|
|
2364
|
+
(await conn.runAndReadAll(`SELECT COUNT(*)::INTEGER AS cnt FROM "${stagingName}"`)).getRows()[0][0]
|
|
366
2365
|
);
|
|
367
|
-
const
|
|
2366
|
+
const logicalCoverage = inspected?.coverage;
|
|
2367
|
+
const quality = provenanceAttempt ? resolveQuality(rowCount, provenance.quality) : void 0;
|
|
2368
|
+
const fingerprint = provenanceAttempt ? await exactFileFingerprint(tempPath, rowCount) : void 0;
|
|
2369
|
+
if (provenance && provenanceAttempt && fingerprint && logicalCoverage && quality) {
|
|
2370
|
+
try {
|
|
2371
|
+
const stored = await provenanceAttempt.recorder.publishFileCommit({
|
|
2372
|
+
dataset: provenance.dataset,
|
|
2373
|
+
partition: provenance.partition,
|
|
2374
|
+
schemaRevision: provenance.schemaRevision,
|
|
2375
|
+
relativePath: provenance.relativePath,
|
|
2376
|
+
coverage: logicalCoverage,
|
|
2377
|
+
quality,
|
|
2378
|
+
file: fingerprint,
|
|
2379
|
+
preparedPath: tempPath,
|
|
2380
|
+
expectedTargetPath: targetPath
|
|
2381
|
+
});
|
|
2382
|
+
capturePartitionCommitReceipt(stored);
|
|
2383
|
+
return { rowCount, provenance: stored };
|
|
2384
|
+
} catch (error) {
|
|
2385
|
+
if (error instanceof PartitionFilePublicationError) {
|
|
2386
|
+
throw new ParquetProvenanceOrphanError(targetPath, fingerprint, error);
|
|
2387
|
+
}
|
|
2388
|
+
throw error;
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
await syncFile(tempPath);
|
|
2392
|
+
await syncDirectory(path6.dirname(tempPath));
|
|
2393
|
+
await fs3.rename(tempPath, targetPath);
|
|
2394
|
+
await syncDirectory(path6.dirname(targetPath));
|
|
368
2395
|
return { rowCount };
|
|
369
2396
|
} catch (writeErr) {
|
|
370
2397
|
try {
|
|
371
|
-
await
|
|
2398
|
+
await fs3.unlink(tempPath);
|
|
372
2399
|
} catch {
|
|
373
2400
|
}
|
|
374
2401
|
throw writeErr;
|
|
@@ -396,34 +2423,35 @@ async function writeParquetPartition(conn, opts) {
|
|
|
396
2423
|
}
|
|
397
2424
|
}
|
|
398
2425
|
const partitionSegments = Object.entries(partitions).map(([k, v]) => `${k}=${v}`);
|
|
399
|
-
const targetPath =
|
|
2426
|
+
const targetPath = path6.join(opts.baseDir, ...partitionSegments, filename);
|
|
400
2427
|
return writeParquetAtomic(conn, {
|
|
401
2428
|
targetPath,
|
|
402
2429
|
selectQuery: opts.selectQuery,
|
|
403
|
-
compression: opts.compression
|
|
2430
|
+
compression: opts.compression,
|
|
2431
|
+
provenance: opts.provenance
|
|
404
2432
|
});
|
|
405
2433
|
}
|
|
406
2434
|
|
|
407
2435
|
// src/db/json-adapters.ts
|
|
408
|
-
import * as
|
|
409
|
-
import * as
|
|
2436
|
+
import * as fs5 from "fs/promises";
|
|
2437
|
+
import * as path8 from "path";
|
|
410
2438
|
import { z } from "zod";
|
|
411
2439
|
|
|
412
2440
|
// src/db/json-store.ts
|
|
413
|
-
import * as
|
|
414
|
-
import * as
|
|
2441
|
+
import * as fs4 from "fs/promises";
|
|
2442
|
+
import * as path7 from "path";
|
|
415
2443
|
function isEnoent(err) {
|
|
416
2444
|
return typeof err === "object" && err !== null && err.code === "ENOENT";
|
|
417
2445
|
}
|
|
418
2446
|
async function writeJsonFile(filePath, data) {
|
|
419
2447
|
const tmpPath = filePath + ".tmp";
|
|
420
|
-
await
|
|
421
|
-
await
|
|
422
|
-
await
|
|
2448
|
+
await fs4.mkdir(path7.dirname(filePath), { recursive: true });
|
|
2449
|
+
await fs4.writeFile(tmpPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
2450
|
+
await fs4.rename(tmpPath, filePath);
|
|
423
2451
|
}
|
|
424
2452
|
async function readJsonFile(filePath) {
|
|
425
2453
|
try {
|
|
426
|
-
const content = await
|
|
2454
|
+
const content = await fs4.readFile(filePath, "utf-8");
|
|
427
2455
|
return JSON.parse(content);
|
|
428
2456
|
} catch (err) {
|
|
429
2457
|
if (isEnoent(err)) {
|
|
@@ -434,7 +2462,7 @@ async function readJsonFile(filePath) {
|
|
|
434
2462
|
}
|
|
435
2463
|
async function deleteJsonFile(filePath) {
|
|
436
2464
|
try {
|
|
437
|
-
await
|
|
2465
|
+
await fs4.unlink(filePath);
|
|
438
2466
|
return true;
|
|
439
2467
|
} catch (err) {
|
|
440
2468
|
if (isEnoent(err)) {
|
|
@@ -445,8 +2473,8 @@ async function deleteJsonFile(filePath) {
|
|
|
445
2473
|
}
|
|
446
2474
|
async function listJsonFiles(dirPath, suffix = ".json") {
|
|
447
2475
|
try {
|
|
448
|
-
const entries = await
|
|
449
|
-
return entries.filter((e) => e.isFile() && e.name.endsWith(suffix)).map((e) =>
|
|
2476
|
+
const entries = await fs4.readdir(dirPath, { withFileTypes: true });
|
|
2477
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(suffix)).map((e) => path7.join(dirPath, e.name)).sort();
|
|
450
2478
|
} catch (err) {
|
|
451
2479
|
if (isEnoent(err)) {
|
|
452
2480
|
return [];
|
|
@@ -474,7 +2502,7 @@ function jsonToProfile(json) {
|
|
|
474
2502
|
};
|
|
475
2503
|
}
|
|
476
2504
|
function profilePath(blockId, strategyName, blocksDir) {
|
|
477
|
-
return
|
|
2505
|
+
return path8.join(blocksDir, blockId, "profiles", toFileSlug(strategyName) + ".json");
|
|
478
2506
|
}
|
|
479
2507
|
async function upsertProfileJson(profile, blocksDir) {
|
|
480
2508
|
const filePath = profilePath(profile.blockId, profile.strategyName, blocksDir);
|
|
@@ -496,7 +2524,7 @@ async function getProfileJson(blockId, strategyName, blocksDir) {
|
|
|
496
2524
|
}
|
|
497
2525
|
async function listProfilesJson(blocksDir, blockId) {
|
|
498
2526
|
if (blockId) {
|
|
499
|
-
const profileDir =
|
|
2527
|
+
const profileDir = path8.join(blocksDir, blockId, "profiles");
|
|
500
2528
|
const files = await listJsonFiles(profileDir);
|
|
501
2529
|
const profiles2 = [];
|
|
502
2530
|
for (const file of files) {
|
|
@@ -508,7 +2536,7 @@ async function listProfilesJson(blocksDir, blockId) {
|
|
|
508
2536
|
const profiles = [];
|
|
509
2537
|
let entries;
|
|
510
2538
|
try {
|
|
511
|
-
entries = await
|
|
2539
|
+
entries = await fs5.readdir(blocksDir, { withFileTypes: true });
|
|
512
2540
|
} catch {
|
|
513
2541
|
return [];
|
|
514
2542
|
}
|
|
@@ -536,7 +2564,7 @@ function jsonToSyncMeta(json) {
|
|
|
536
2564
|
};
|
|
537
2565
|
}
|
|
538
2566
|
function syncMetaPath(blockId, blocksDir) {
|
|
539
|
-
return
|
|
2567
|
+
return path8.join(blocksDir, blockId, ".sync-meta.json");
|
|
540
2568
|
}
|
|
541
2569
|
async function getSyncMetadataJson(blockId, blocksDir) {
|
|
542
2570
|
const filePath = syncMetaPath(blockId, blocksDir);
|
|
@@ -554,7 +2582,7 @@ async function deleteSyncMetadataJson(blockId, blocksDir) {
|
|
|
554
2582
|
async function getAllSyncedBlockIdsJson(blocksDir) {
|
|
555
2583
|
let entries;
|
|
556
2584
|
try {
|
|
557
|
-
entries = await
|
|
2585
|
+
entries = await fs5.readdir(blocksDir, { withFileTypes: true });
|
|
558
2586
|
} catch {
|
|
559
2587
|
return [];
|
|
560
2588
|
}
|
|
@@ -563,7 +2591,7 @@ async function getAllSyncedBlockIdsJson(blocksDir) {
|
|
|
563
2591
|
if (!entry.isDirectory()) continue;
|
|
564
2592
|
const metaPath = syncMetaPath(entry.name, blocksDir);
|
|
565
2593
|
try {
|
|
566
|
-
await
|
|
2594
|
+
await fs5.access(metaPath);
|
|
567
2595
|
ids.push(entry.name);
|
|
568
2596
|
} catch {
|
|
569
2597
|
}
|
|
@@ -574,7 +2602,7 @@ function marketMetaKey(source, ticker, targetTable) {
|
|
|
574
2602
|
return `${source}|${ticker}|${targetTable}`;
|
|
575
2603
|
}
|
|
576
2604
|
function marketMetaFilePath(dataDir) {
|
|
577
|
-
return
|
|
2605
|
+
return path8.join(getDataRoot(dataDir), "market-meta", "sync-metadata.json");
|
|
578
2606
|
}
|
|
579
2607
|
async function getMarketImportMetadataJson(source, ticker, targetTable, dataDir) {
|
|
580
2608
|
const filePath = marketMetaFilePath(dataDir);
|
|
@@ -602,7 +2630,7 @@ function flatLogKey(entry) {
|
|
|
602
2630
|
return `${entry.date}|${entry.asset_class}|${entry.underlying}`;
|
|
603
2631
|
}
|
|
604
2632
|
function flatLogFilePath(dataDir) {
|
|
605
|
-
return
|
|
2633
|
+
return path8.join(getDataRoot(dataDir), "market-meta", "flat-import-log.json");
|
|
606
2634
|
}
|
|
607
2635
|
async function getFlatImportLogJson(assetClass, underlying, from, to, dataDir) {
|
|
608
2636
|
const filePath = flatLogFilePath(dataDir);
|
|
@@ -640,7 +2668,7 @@ var EnrichmentWatermarksSchema = z.object({
|
|
|
640
2668
|
watermarks: z.record(z.string().regex(/^[A-Z0-9._-]+$/), TickerEntrySchema)
|
|
641
2669
|
});
|
|
642
2670
|
function watermarksFilePath(dataDir) {
|
|
643
|
-
return
|
|
2671
|
+
return path8.join(getDataRoot(dataDir), "market-meta", "enrichment-watermarks.json");
|
|
644
2672
|
}
|
|
645
2673
|
async function loadEnrichmentWatermarks(dataDir) {
|
|
646
2674
|
const raw = await readJsonFile(watermarksFilePath(dataDir));
|
|
@@ -947,121 +2975,19 @@ async function deleteProfile(conn, blockId, strategyName, baseDir) {
|
|
|
947
2975
|
|
|
948
2976
|
// src/db/market-views.ts
|
|
949
2977
|
import { existsSync, readdirSync } from "fs";
|
|
950
|
-
import * as
|
|
951
|
-
|
|
952
|
-
// src/utils/quote-parquet-projection.ts
|
|
953
|
-
var describeCache = /* @__PURE__ */ new Map();
|
|
954
|
-
function escapeSqlLiteral(value) {
|
|
955
|
-
return value.replace(/'/g, "''");
|
|
956
|
-
}
|
|
957
|
-
function readParquetGlobSql(glob) {
|
|
958
|
-
return `read_parquet('${escapeSqlLiteral(glob)}', hive_partitioning=true, union_by_name=true)`;
|
|
959
|
-
}
|
|
960
|
-
function readParquetFilesSql(files) {
|
|
961
|
-
if (files.length === 0) {
|
|
962
|
-
throw new Error("readParquetFilesSql: files must not be empty");
|
|
963
|
-
}
|
|
964
|
-
const fileList = files.map((filePath) => `'${escapeSqlLiteral(filePath)}'`).join(", ");
|
|
965
|
-
return `read_parquet([${fileList}], hive_partitioning=true, union_by_name=true)`;
|
|
966
|
-
}
|
|
967
|
-
async function describeReadParquetColumns(conn, readParquetSql) {
|
|
968
|
-
let pending = describeCache.get(readParquetSql);
|
|
969
|
-
if (!pending) {
|
|
970
|
-
pending = (async () => {
|
|
971
|
-
const reader = await conn.runAndReadAll(`DESCRIBE SELECT * FROM ${readParquetSql}`);
|
|
972
|
-
return new Set(reader.getRows().map((row) => String(row[0]).toLowerCase()));
|
|
973
|
-
})();
|
|
974
|
-
describeCache.set(readParquetSql, pending);
|
|
975
|
-
}
|
|
976
|
-
return pending;
|
|
977
|
-
}
|
|
978
|
-
async function describeQueryColumns(conn, querySql) {
|
|
979
|
-
const reader = await conn.runAndReadAll(`DESCRIBE ${querySql}`);
|
|
980
|
-
return new Set(reader.getRows().map((row) => String(row[0]).toLowerCase()));
|
|
981
|
-
}
|
|
982
|
-
function hasColumn2(columns, name) {
|
|
983
|
-
return columns.has(name.toLowerCase());
|
|
984
|
-
}
|
|
985
|
-
function quoteParquetColumnExpr(columns, alias, name, fallbackType) {
|
|
986
|
-
return hasColumn2(columns, name) ? `${alias}.${name}` : `NULL::${fallbackType}`;
|
|
987
|
-
}
|
|
988
|
-
function quoteParquetMidExpr(columns, alias) {
|
|
989
|
-
return hasColumn2(columns, "mid") ? `${alias}.mid` : `((CAST(${alias}.bid AS DOUBLE) + CAST(${alias}.ask AS DOUBLE)) / 2)`;
|
|
990
|
-
}
|
|
991
|
-
var ALL_GREEKS = ["delta", "gamma", "theta", "vega", "iv"];
|
|
992
|
-
function quoteParquetGreekProjection(columns, alias = "q", needed = ALL_GREEKS) {
|
|
993
|
-
const wantSet = new Set(needed);
|
|
994
|
-
const projectGreek = (name) => wantSet.has(name) ? `${quoteParquetColumnExpr(columns, alias, name, "DOUBLE")} AS ${name}` : `NULL::DOUBLE AS ${name}`;
|
|
995
|
-
return [
|
|
996
|
-
projectGreek("delta"),
|
|
997
|
-
projectGreek("gamma"),
|
|
998
|
-
projectGreek("theta"),
|
|
999
|
-
projectGreek("vega"),
|
|
1000
|
-
projectGreek("iv"),
|
|
1001
|
-
`${quoteParquetColumnExpr(columns, alias, "greeks_source", "VARCHAR")} AS greeks_source`,
|
|
1002
|
-
`${quoteParquetColumnExpr(columns, alias, "greeks_revision", "INTEGER")} AS greeks_revision`,
|
|
1003
|
-
`${quoteParquetColumnExpr(columns, alias, "rate_type", "VARCHAR")} AS rate_type`,
|
|
1004
|
-
`${quoteParquetColumnExpr(columns, alias, "rate_value", "DOUBLE")} AS rate_value`,
|
|
1005
|
-
`${quoteParquetColumnExpr(columns, alias, "gamma_source", "VARCHAR")} AS gamma_source`
|
|
1006
|
-
].join(",\n ");
|
|
1007
|
-
}
|
|
1008
|
-
function quoteParquetGreekWriteProjection(columns, alias = "q") {
|
|
1009
|
-
const castReal = (name) => hasColumn2(columns, name) ? `CAST(${alias}.${name} AS REAL)` : `NULL::REAL`;
|
|
1010
|
-
return [
|
|
1011
|
-
`${castReal("delta")} AS delta`,
|
|
1012
|
-
`${castReal("gamma")} AS gamma`,
|
|
1013
|
-
`${castReal("theta")} AS theta`,
|
|
1014
|
-
`${castReal("vega")} AS vega`,
|
|
1015
|
-
`${castReal("iv")} AS iv`,
|
|
1016
|
-
`${quoteParquetColumnExpr(columns, alias, "greeks_source", "VARCHAR")} AS greeks_source`,
|
|
1017
|
-
`${quoteParquetColumnExpr(columns, alias, "greeks_revision", "INTEGER")} AS greeks_revision`,
|
|
1018
|
-
`${quoteParquetColumnExpr(columns, alias, "rate_type", "VARCHAR")} AS rate_type`,
|
|
1019
|
-
`${quoteParquetColumnExpr(columns, alias, "rate_value", "DOUBLE")} AS rate_value`,
|
|
1020
|
-
`${quoteParquetColumnExpr(columns, alias, "gamma_source", "VARCHAR")} AS gamma_source`
|
|
1021
|
-
].join(",\n ");
|
|
1022
|
-
}
|
|
1023
|
-
function quoteParquetCanonicalProjection(columns, alias = "q") {
|
|
1024
|
-
return [
|
|
1025
|
-
`${quoteParquetColumnExpr(columns, alias, "underlying", "VARCHAR")} AS underlying`,
|
|
1026
|
-
`${quoteParquetColumnExpr(columns, alias, "date", "VARCHAR")} AS date`,
|
|
1027
|
-
`${quoteParquetColumnExpr(columns, alias, "ticker", "VARCHAR")} AS ticker`,
|
|
1028
|
-
`${quoteParquetColumnExpr(columns, alias, "time", "VARCHAR")} AS time`,
|
|
1029
|
-
`${quoteParquetColumnExpr(columns, alias, "bid", "DOUBLE")} AS bid`,
|
|
1030
|
-
`${quoteParquetColumnExpr(columns, alias, "ask", "DOUBLE")} AS ask`,
|
|
1031
|
-
`${quoteParquetMidExpr(columns, alias)} AS mid`,
|
|
1032
|
-
`${quoteParquetColumnExpr(columns, alias, "last_updated_ns", "BIGINT")} AS last_updated_ns`,
|
|
1033
|
-
`${quoteParquetColumnExpr(columns, alias, "source", "VARCHAR")} AS source`,
|
|
1034
|
-
quoteParquetGreekProjection(columns, alias)
|
|
1035
|
-
].join(",\n ");
|
|
1036
|
-
}
|
|
1037
|
-
function quoteParquetCanonicalWriteProjection(columns, alias = "q") {
|
|
1038
|
-
return [
|
|
1039
|
-
`${quoteParquetColumnExpr(columns, alias, "underlying", "VARCHAR")} AS underlying`,
|
|
1040
|
-
`${quoteParquetColumnExpr(columns, alias, "date", "VARCHAR")} AS date`,
|
|
1041
|
-
`${quoteParquetColumnExpr(columns, alias, "ticker", "VARCHAR")} AS ticker`,
|
|
1042
|
-
`${quoteParquetColumnExpr(columns, alias, "time", "VARCHAR")} AS time`,
|
|
1043
|
-
`${quoteParquetColumnExpr(columns, alias, "bid", "DOUBLE")} AS bid`,
|
|
1044
|
-
`${quoteParquetColumnExpr(columns, alias, "ask", "DOUBLE")} AS ask`,
|
|
1045
|
-
`${quoteParquetMidExpr(columns, alias)} AS mid`,
|
|
1046
|
-
`${quoteParquetColumnExpr(columns, alias, "last_updated_ns", "BIGINT")} AS last_updated_ns`,
|
|
1047
|
-
`${quoteParquetColumnExpr(columns, alias, "source", "VARCHAR")} AS source`,
|
|
1048
|
-
quoteParquetGreekWriteProjection(columns, alias)
|
|
1049
|
-
].join(",\n ");
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
// src/db/market-views.ts
|
|
2978
|
+
import * as path9 from "path";
|
|
1053
2979
|
function hasParquetPartitions(dir, partitionKey = "date") {
|
|
1054
2980
|
if (!existsSync(dir)) return false;
|
|
1055
2981
|
try {
|
|
1056
2982
|
const prefix = `${partitionKey}=`;
|
|
1057
2983
|
return readdirSync(dir).some((entry) => {
|
|
1058
2984
|
if (!entry.startsWith(prefix)) return false;
|
|
1059
|
-
const partDir =
|
|
2985
|
+
const partDir = path9.join(dir, entry);
|
|
1060
2986
|
try {
|
|
1061
2987
|
const entries = readdirSync(partDir);
|
|
1062
2988
|
for (const sub of entries) {
|
|
1063
2989
|
if (sub.endsWith(".parquet")) return true;
|
|
1064
|
-
const nested =
|
|
2990
|
+
const nested = path9.join(partDir, sub);
|
|
1065
2991
|
try {
|
|
1066
2992
|
if (readdirSync(nested).some((f) => f.endsWith(".parquet"))) return true;
|
|
1067
2993
|
} catch {
|
|
@@ -1081,14 +3007,29 @@ function hasEnrichedTickerFiles(dir) {
|
|
|
1081
3007
|
try {
|
|
1082
3008
|
return readdirSync(dir).some((entry) => {
|
|
1083
3009
|
if (!entry.startsWith("ticker=")) return false;
|
|
1084
|
-
|
|
3010
|
+
const tickerDir = path9.join(dir, entry);
|
|
3011
|
+
try {
|
|
3012
|
+
return readdirSync(tickerDir).some(
|
|
3013
|
+
(dateEntry) => dateEntry.startsWith("date=") && existsSync(path9.join(tickerDir, dateEntry, "data.parquet"))
|
|
3014
|
+
);
|
|
3015
|
+
} catch {
|
|
3016
|
+
return false;
|
|
3017
|
+
}
|
|
1085
3018
|
});
|
|
1086
3019
|
} catch {
|
|
1087
3020
|
return false;
|
|
1088
3021
|
}
|
|
1089
3022
|
}
|
|
1090
3023
|
function hasEnrichedContextFile(dir) {
|
|
1091
|
-
|
|
3024
|
+
const contextDir = path9.join(dir, "context");
|
|
3025
|
+
if (!existsSync(contextDir)) return false;
|
|
3026
|
+
try {
|
|
3027
|
+
return readdirSync(contextDir).some(
|
|
3028
|
+
(entry) => entry.startsWith("date=") && existsSync(path9.join(contextDir, entry, "data.parquet"))
|
|
3029
|
+
);
|
|
3030
|
+
} catch {
|
|
3031
|
+
return false;
|
|
3032
|
+
}
|
|
1092
3033
|
}
|
|
1093
3034
|
async function createMarketParquetViews(conn, dataDir) {
|
|
1094
3035
|
const viewsCreated = [];
|
|
@@ -1169,7 +3110,7 @@ async function createMarketParquetViews(conn, dataDir) {
|
|
|
1169
3110
|
await conn.run("DROP TABLE IF EXISTS market.option_delta_index");
|
|
1170
3111
|
} catch {
|
|
1171
3112
|
}
|
|
1172
|
-
const spotDir =
|
|
3113
|
+
const spotDir = path9.join(resolveMarketDir(dataDir), "spot");
|
|
1173
3114
|
if (hasParquetPartitions(spotDir, "ticker")) {
|
|
1174
3115
|
try {
|
|
1175
3116
|
await conn.run("DROP VIEW IF EXISTS market.spot");
|
|
@@ -1191,7 +3132,7 @@ async function createMarketParquetViews(conn, dataDir) {
|
|
|
1191
3132
|
}
|
|
1192
3133
|
tablesKept.push("spot");
|
|
1193
3134
|
}
|
|
1194
|
-
const enrichedDir =
|
|
3135
|
+
const enrichedDir = path9.join(resolveMarketDir(dataDir), "enriched");
|
|
1195
3136
|
if (hasEnrichedTickerFiles(enrichedDir)) {
|
|
1196
3137
|
try {
|
|
1197
3138
|
await conn.run("DROP VIEW IF EXISTS market.enriched");
|
|
@@ -1203,7 +3144,7 @@ async function createMarketParquetViews(conn, dataDir) {
|
|
|
1203
3144
|
}
|
|
1204
3145
|
await conn.run(
|
|
1205
3146
|
`CREATE OR REPLACE VIEW market.enriched AS
|
|
1206
|
-
SELECT * FROM read_parquet('${enrichedDir}/ticker=*/data.parquet', hive_partitioning=true)`
|
|
3147
|
+
SELECT * FROM read_parquet('${enrichedDir}/ticker=*/date=*/data.parquet', hive_partitioning=true)`
|
|
1207
3148
|
);
|
|
1208
3149
|
viewsCreated.push("enriched");
|
|
1209
3150
|
} else {
|
|
@@ -1224,7 +3165,7 @@ async function createMarketParquetViews(conn, dataDir) {
|
|
|
1224
3165
|
}
|
|
1225
3166
|
await conn.run(
|
|
1226
3167
|
`CREATE OR REPLACE VIEW market.enriched_context AS
|
|
1227
|
-
SELECT * FROM read_parquet('${
|
|
3168
|
+
SELECT * FROM read_parquet('${path9.join(enrichedDir, "context", "date=*", "data.parquet")}', hive_partitioning=true)`
|
|
1228
3169
|
);
|
|
1229
3170
|
viewsCreated.push("enriched_context");
|
|
1230
3171
|
} else {
|
|
@@ -1289,7 +3230,7 @@ async function createMarketParquetViews(conn, dataDir) {
|
|
|
1289
3230
|
}
|
|
1290
3231
|
|
|
1291
3232
|
// src/db/json-migration.ts
|
|
1292
|
-
import * as
|
|
3233
|
+
import * as path10 from "path";
|
|
1293
3234
|
function toDate2(value) {
|
|
1294
3235
|
if (value instanceof Date) return value;
|
|
1295
3236
|
if (typeof value === "object" && value !== null && "micros" in value) {
|
|
@@ -1366,7 +3307,7 @@ async function migrateProfiles(conn, blocksDir) {
|
|
|
1366
3307
|
createdAt: createdAt.toISOString(),
|
|
1367
3308
|
updatedAt: updatedAt.toISOString()
|
|
1368
3309
|
};
|
|
1369
|
-
const filePath =
|
|
3310
|
+
const filePath = path10.join(blocksDir, blockId, "profiles", toFileSlug(strategyName) + ".json");
|
|
1370
3311
|
await writeJsonFile(filePath, profileJson);
|
|
1371
3312
|
}
|
|
1372
3313
|
return rows.length;
|
|
@@ -1399,7 +3340,7 @@ async function migrateSyncMetadata(conn, blocksDir) {
|
|
|
1399
3340
|
return rows.length;
|
|
1400
3341
|
}
|
|
1401
3342
|
async function migrateMarketImportMetadata(conn, dataDir) {
|
|
1402
|
-
const existingFile = await readJsonFile(
|
|
3343
|
+
const existingFile = await readJsonFile(path10.join(dataDir, "market-meta", "sync-metadata.json"));
|
|
1403
3344
|
if (existingFile !== null) return 0;
|
|
1404
3345
|
let rows;
|
|
1405
3346
|
try {
|
|
@@ -1426,7 +3367,7 @@ async function migrateMarketImportMetadata(conn, dataDir) {
|
|
|
1426
3367
|
}
|
|
1427
3368
|
async function migrateFlatImportLog(conn, dataDir) {
|
|
1428
3369
|
const existingFile = await readJsonFile(
|
|
1429
|
-
|
|
3370
|
+
path10.join(dataDir, "market-meta", "flat-import-log.json")
|
|
1430
3371
|
);
|
|
1431
3372
|
if (existingFile !== null) return 0;
|
|
1432
3373
|
let rows;
|
|
@@ -1557,7 +3498,7 @@ async function waitForProcessExit(pid, timeoutMs) {
|
|
|
1557
3498
|
if (!isProcessAlive(pid)) {
|
|
1558
3499
|
return true;
|
|
1559
3500
|
}
|
|
1560
|
-
await new Promise((
|
|
3501
|
+
await new Promise((resolve3) => setTimeout(resolve3, 100));
|
|
1561
3502
|
}
|
|
1562
3503
|
return !isProcessAlive(pid);
|
|
1563
3504
|
}
|
|
@@ -1571,7 +3512,7 @@ async function tryRecoverLockByTerminatingStaleProcess(errorMessage, dbPath, for
|
|
|
1571
3512
|
return false;
|
|
1572
3513
|
}
|
|
1573
3514
|
const normalizedDbPath = dbPath.replace(/\\/g, "/");
|
|
1574
|
-
const normalizedDbDir =
|
|
3515
|
+
const normalizedDbDir = path11.dirname(normalizedDbPath);
|
|
1575
3516
|
const isTradeblocksProcess = command.includes("tradeblocks-mcp") || command.includes("/mcp-server/server/index.js") || command.includes("packages/mcp-server/server/index.js") || command.includes("\\mcp-server\\server\\index.js") || command.includes("packages\\mcp-server\\server\\index.js");
|
|
1576
3517
|
const normalizedCommand = command.replace(/\\/g, "/");
|
|
1577
3518
|
const targetsSameDb = normalizedCommand.includes(normalizedDbPath) || normalizedCommand.includes(normalizedDbDir);
|
|
@@ -1605,16 +3546,16 @@ function resolveMarketDbPath(dataDir) {
|
|
|
1605
3546
|
const args = process.argv;
|
|
1606
3547
|
for (let i = 0; i < args.length; i++) {
|
|
1607
3548
|
if (args[i] === "--market-db" && args[i + 1]) {
|
|
1608
|
-
return
|
|
3549
|
+
return path11.resolve(args[i + 1]);
|
|
1609
3550
|
}
|
|
1610
3551
|
}
|
|
1611
3552
|
if (process.env.MARKET_DB_PATH) {
|
|
1612
|
-
return
|
|
3553
|
+
return path11.resolve(process.env.MARKET_DB_PATH);
|
|
1613
3554
|
}
|
|
1614
|
-
return
|
|
3555
|
+
return path11.join(dataDir, "market.duckdb");
|
|
1615
3556
|
}
|
|
1616
3557
|
async function attachMarketDb(conn, marketDbPath, mode) {
|
|
1617
|
-
await
|
|
3558
|
+
await fs6.mkdir(path11.dirname(marketDbPath), { recursive: true });
|
|
1618
3559
|
const readOnlyClause = mode === "read_only" ? " (READ_ONLY)" : "";
|
|
1619
3560
|
const escapedPath = marketDbPath.replace(/'/g, "''");
|
|
1620
3561
|
try {
|
|
@@ -1624,11 +3565,11 @@ async function attachMarketDb(conn, marketDbPath, mode) {
|
|
|
1624
3565
|
if (msg.includes("corrupt") || msg.includes("Invalid") || msg.includes("cannot open")) {
|
|
1625
3566
|
console.error(`market.duckdb appears corrupted at ${marketDbPath}. Recreating.`);
|
|
1626
3567
|
try {
|
|
1627
|
-
await
|
|
3568
|
+
await fs6.unlink(marketDbPath);
|
|
1628
3569
|
} catch {
|
|
1629
3570
|
}
|
|
1630
3571
|
try {
|
|
1631
|
-
await
|
|
3572
|
+
await fs6.unlink(marketDbPath + ".wal");
|
|
1632
3573
|
} catch {
|
|
1633
3574
|
}
|
|
1634
3575
|
await conn.run(`ATTACH '${escapedPath}' AS market${readOnlyClause}`);
|
|
@@ -1661,12 +3602,12 @@ async function openReadWriteConnection(dbPath, threads, memoryLimit) {
|
|
|
1661
3602
|
await ensureReportingDataTable(connection);
|
|
1662
3603
|
await ensureMutableMarketTables(connection);
|
|
1663
3604
|
await ensureProfilesSchema(connection);
|
|
1664
|
-
const dataDir =
|
|
3605
|
+
const dataDir = path11.dirname(dbPath);
|
|
1665
3606
|
const dataRoot = getDataRoot(dataDir);
|
|
1666
3607
|
await createMarketParquetViews(connection, dataRoot);
|
|
1667
3608
|
await ensureMarketDataTables(connection);
|
|
1668
3609
|
try {
|
|
1669
|
-
const blocksDir = (await import("./sync-
|
|
3610
|
+
const blocksDir = (await import("./sync-AWI764HE.js")).getBlocksDir(dataRoot);
|
|
1670
3611
|
await migrateMetadataToJson(connection, dataRoot, blocksDir);
|
|
1671
3612
|
} catch (err) {
|
|
1672
3613
|
console.warn(
|
|
@@ -1712,7 +3653,7 @@ async function getConnection(dataDir) {
|
|
|
1712
3653
|
if (connection) {
|
|
1713
3654
|
return connection;
|
|
1714
3655
|
}
|
|
1715
|
-
const dbPath =
|
|
3656
|
+
const dbPath = path11.join(dataDir, "analytics.duckdb");
|
|
1716
3657
|
const threads = process.env.DUCKDB_THREADS || defaultThreads();
|
|
1717
3658
|
const memoryLimit = process.env.DUCKDB_MEMORY_LIMIT || defaultMemoryLimit();
|
|
1718
3659
|
storedDbPath = dbPath;
|
|
@@ -1789,7 +3730,7 @@ async function closeConnection() {
|
|
|
1789
3730
|
async function upgradeToReadWrite(dataDir, options) {
|
|
1790
3731
|
if (connectionMode === "read_write" && connection) return connection;
|
|
1791
3732
|
await closeConnection();
|
|
1792
|
-
const dbPath = storedDbPath ||
|
|
3733
|
+
const dbPath = storedDbPath || path11.join(dataDir, "analytics.duckdb");
|
|
1793
3734
|
const threads = storedThreads || process.env.DUCKDB_THREADS || defaultThreads();
|
|
1794
3735
|
const memoryLimit = storedMemoryLimit || process.env.DUCKDB_MEMORY_LIMIT || defaultMemoryLimit();
|
|
1795
3736
|
if (!storedMarketDbPath) {
|
|
@@ -1861,11 +3802,57 @@ async function openMarketOnlyConnection(baseDir) {
|
|
|
1861
3802
|
};
|
|
1862
3803
|
return { conn, marketDbPath, close };
|
|
1863
3804
|
}
|
|
1864
|
-
|
|
3805
|
+
var DUCKDB_MEMORY_LIMIT_PATTERN = /^\d+(\.\d+)?(KB|MB|GB|TB|KiB|MiB|GiB|TiB)$/i;
|
|
3806
|
+
function validateMemoryLimit(value) {
|
|
3807
|
+
const trimmed = value.trim();
|
|
3808
|
+
if (!DUCKDB_MEMORY_LIMIT_PATTERN.test(trimmed)) {
|
|
3809
|
+
throw new Error(
|
|
3810
|
+
`Invalid DuckDB memory_limit ${JSON.stringify(value)}. Expected a number followed by a unit, e.g. "4GB", "512MB", or "8GiB" (units: KB, MB, GB, TB, KiB, MiB, GiB, TiB).`
|
|
3811
|
+
);
|
|
3812
|
+
}
|
|
3813
|
+
return trimmed;
|
|
3814
|
+
}
|
|
3815
|
+
function validateThreads(value) {
|
|
3816
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
3817
|
+
throw new Error(
|
|
3818
|
+
`Invalid DuckDB threads ${JSON.stringify(value)}. Expected a positive integer.`
|
|
3819
|
+
);
|
|
3820
|
+
}
|
|
3821
|
+
return value;
|
|
3822
|
+
}
|
|
3823
|
+
function resolveResourceBounds(options) {
|
|
3824
|
+
const resolved = {};
|
|
3825
|
+
const memoryLimitRaw = options?.memoryLimit ?? envValue("TRADEBLOCKS_DUCKDB_MEMORY_LIMIT");
|
|
3826
|
+
if (memoryLimitRaw !== void 0) {
|
|
3827
|
+
resolved.memoryLimit = validateMemoryLimit(memoryLimitRaw);
|
|
3828
|
+
}
|
|
3829
|
+
const threadsEnv = envValue("TRADEBLOCKS_DUCKDB_THREADS");
|
|
3830
|
+
const threadsRaw = options?.threads ?? (threadsEnv !== void 0 ? Number(threadsEnv) : void 0);
|
|
3831
|
+
if (threadsRaw !== void 0) {
|
|
3832
|
+
resolved.threads = validateThreads(threadsRaw);
|
|
3833
|
+
}
|
|
3834
|
+
return resolved;
|
|
3835
|
+
}
|
|
3836
|
+
function envValue(name) {
|
|
3837
|
+
const raw = process.env[name];
|
|
3838
|
+
if (raw === void 0 || raw.trim() === "") return void 0;
|
|
3839
|
+
return raw;
|
|
3840
|
+
}
|
|
3841
|
+
async function applyResourceBounds(conn, bounds) {
|
|
3842
|
+
if (bounds.memoryLimit !== void 0) {
|
|
3843
|
+
await conn.run(`SET memory_limit='${bounds.memoryLimit}'`);
|
|
3844
|
+
}
|
|
3845
|
+
if (bounds.threads !== void 0) {
|
|
3846
|
+
await conn.run(`SET threads=${bounds.threads}`);
|
|
3847
|
+
}
|
|
3848
|
+
}
|
|
3849
|
+
async function openMarketParquetConnection(baseDir, options) {
|
|
3850
|
+
const bounds = resolveResourceBounds(options);
|
|
1865
3851
|
const memoryInstance = await DuckDBInstance.create(":memory:", {
|
|
1866
3852
|
enable_external_access: "true"
|
|
1867
3853
|
});
|
|
1868
3854
|
const conn = await memoryInstance.connect();
|
|
3855
|
+
await applyResourceBounds(conn, bounds);
|
|
1869
3856
|
await conn.run("CREATE SCHEMA IF NOT EXISTS market");
|
|
1870
3857
|
const dataRoot = getDataRoot(baseDir);
|
|
1871
3858
|
await createMarketParquetViews(conn, dataRoot);
|
|
@@ -1884,12 +3871,12 @@ async function openMarketParquetConnection(baseDir) {
|
|
|
1884
3871
|
};
|
|
1885
3872
|
return { conn, dataRoot, close };
|
|
1886
3873
|
}
|
|
1887
|
-
function openMarketReadOnlyConnection(baseDir) {
|
|
1888
|
-
return openMarketParquetConnection(baseDir);
|
|
3874
|
+
function openMarketReadOnlyConnection(baseDir, options) {
|
|
3875
|
+
return openMarketParquetConnection(baseDir, options);
|
|
1889
3876
|
}
|
|
1890
3877
|
async function getReadOnlyConnection(dataDir) {
|
|
1891
3878
|
if (connection) return connection;
|
|
1892
|
-
const dbPath =
|
|
3879
|
+
const dbPath = path11.join(dataDir, "analytics.duckdb");
|
|
1893
3880
|
const threads = process.env.DUCKDB_THREADS || defaultThreads();
|
|
1894
3881
|
const memoryLimit = process.env.DUCKDB_MEMORY_LIMIT || defaultMemoryLimit();
|
|
1895
3882
|
storedDbPath = dbPath;
|
|
@@ -1915,14 +3902,14 @@ function getCurrentConnection() {
|
|
|
1915
3902
|
}
|
|
1916
3903
|
|
|
1917
3904
|
// src/sync/block-sync.ts
|
|
1918
|
-
import * as
|
|
1919
|
-
import * as
|
|
3905
|
+
import * as fs10 from "fs/promises";
|
|
3906
|
+
import * as path14 from "path";
|
|
1920
3907
|
|
|
1921
3908
|
// src/sync/hasher.ts
|
|
1922
3909
|
import * as crypto from "crypto";
|
|
1923
|
-
import * as
|
|
3910
|
+
import * as fs7 from "fs/promises";
|
|
1924
3911
|
async function hashFileContent(filePath) {
|
|
1925
|
-
const buffer = await
|
|
3912
|
+
const buffer = await fs7.readFile(filePath);
|
|
1926
3913
|
return crypto.createHash("sha256").update(buffer).digest("hex");
|
|
1927
3914
|
}
|
|
1928
3915
|
|
|
@@ -2072,12 +4059,12 @@ function marketTickerDateKey(ticker, date) {
|
|
|
2072
4059
|
}
|
|
2073
4060
|
|
|
2074
4061
|
// src/utils/block-loader.ts
|
|
2075
|
-
import * as
|
|
2076
|
-
import * as
|
|
4062
|
+
import * as fs9 from "fs/promises";
|
|
4063
|
+
import * as path13 from "path";
|
|
2077
4064
|
|
|
2078
4065
|
// src/utils/csv-discovery.ts
|
|
2079
|
-
import * as
|
|
2080
|
-
import * as
|
|
4066
|
+
import * as fs8 from "fs/promises";
|
|
4067
|
+
import * as path12 from "path";
|
|
2081
4068
|
function parseCSVLine(line) {
|
|
2082
4069
|
const result = [];
|
|
2083
4070
|
let current = "";
|
|
@@ -2102,7 +4089,7 @@ function parseCSVLine(line) {
|
|
|
2102
4089
|
return result;
|
|
2103
4090
|
}
|
|
2104
4091
|
async function readCsvHeaders(filePath) {
|
|
2105
|
-
const content = await
|
|
4092
|
+
const content = await fs8.readFile(filePath, "utf-8");
|
|
2106
4093
|
const firstLine = content.replace(/^\uFEFF/, "").split("\n")[0] || "";
|
|
2107
4094
|
return parseCSVLine(firstLine).map((h) => h.toLowerCase().trim());
|
|
2108
4095
|
}
|
|
@@ -2178,7 +4165,7 @@ async function discoverCsvFiles(folderPath) {
|
|
|
2178
4165
|
const mappings = {};
|
|
2179
4166
|
const unrecognized = [];
|
|
2180
4167
|
try {
|
|
2181
|
-
const entries = await
|
|
4168
|
+
const entries = await fs8.readdir(folderPath, { withFileTypes: true });
|
|
2182
4169
|
const csvFiles = entries.filter((e) => e.isFile() && e.name.toLowerCase().endsWith(".csv")).map((e) => e.name);
|
|
2183
4170
|
if (csvFiles.includes("tradelog.csv")) {
|
|
2184
4171
|
mappings.tradelog = "tradelog.csv";
|
|
@@ -2202,7 +4189,7 @@ async function discoverCsvFiles(folderPath) {
|
|
|
2202
4189
|
if (csvFile === "tradelog.csv" || csvFile === "dailylog.csv" || csvFile === "reportinglog.csv" || csvFile === mappings.reportinglog) {
|
|
2203
4190
|
continue;
|
|
2204
4191
|
}
|
|
2205
|
-
const csvPath =
|
|
4192
|
+
const csvPath = path12.join(folderPath, csvFile);
|
|
2206
4193
|
const detectedType = await detectCsvType(csvPath);
|
|
2207
4194
|
if (detectedType) {
|
|
2208
4195
|
if (detectedType === "tradelog" && !mappings.tradelog) {
|
|
@@ -2398,8 +4385,8 @@ function convertToDailyLogEntry(raw, blockId) {
|
|
|
2398
4385
|
}
|
|
2399
4386
|
}
|
|
2400
4387
|
async function loadTrades(blockPath, filename = "tradelog.csv", blockId) {
|
|
2401
|
-
const tradelogPath =
|
|
2402
|
-
const content = await
|
|
4388
|
+
const tradelogPath = path13.join(blockPath, filename);
|
|
4389
|
+
const content = await fs9.readFile(tradelogPath, "utf-8");
|
|
2403
4390
|
const records = parseCSV(content);
|
|
2404
4391
|
const trades = [];
|
|
2405
4392
|
for (const record of records) {
|
|
@@ -2416,10 +4403,10 @@ async function loadTrades(blockPath, filename = "tradelog.csv", blockId) {
|
|
|
2416
4403
|
return trades;
|
|
2417
4404
|
}
|
|
2418
4405
|
async function loadDailyLogs(blockPath, blockId, filename = "dailylog.csv") {
|
|
2419
|
-
const dailylogPath =
|
|
4406
|
+
const dailylogPath = path13.join(blockPath, filename);
|
|
2420
4407
|
try {
|
|
2421
|
-
await
|
|
2422
|
-
const content = await
|
|
4408
|
+
await fs9.access(dailylogPath);
|
|
4409
|
+
const content = await fs9.readFile(dailylogPath, "utf-8");
|
|
2423
4410
|
const records = parseCSV(content);
|
|
2424
4411
|
const entries = [];
|
|
2425
4412
|
for (const record of records) {
|
|
@@ -2436,12 +4423,12 @@ async function loadDailyLogs(blockPath, blockId, filename = "dailylog.csv") {
|
|
|
2436
4423
|
}
|
|
2437
4424
|
async function loadBlock(baseDir, blockId) {
|
|
2438
4425
|
const blocksDir = resolveBlocksBaseDir(baseDir);
|
|
2439
|
-
const blockPath =
|
|
4426
|
+
const blockPath = path13.join(blocksDir, blockId);
|
|
2440
4427
|
const { mappings } = await discoverCsvFiles(blockPath);
|
|
2441
4428
|
const tradelogFilename = mappings.tradelog || "tradelog.csv";
|
|
2442
|
-
const tradelogPath =
|
|
4429
|
+
const tradelogPath = path13.join(blockPath, tradelogFilename);
|
|
2443
4430
|
try {
|
|
2444
|
-
await
|
|
4431
|
+
await fs9.access(tradelogPath);
|
|
2445
4432
|
} catch {
|
|
2446
4433
|
throw new Error(`Block not found or missing tradelog: ${blockId}`);
|
|
2447
4434
|
}
|
|
@@ -2542,7 +4529,7 @@ async function listBlocks(baseDir) {
|
|
|
2542
4529
|
}
|
|
2543
4530
|
const syncMeta = /* @__PURE__ */ new Map();
|
|
2544
4531
|
if (isParquetMode()) {
|
|
2545
|
-
const metaEntries = await
|
|
4532
|
+
const metaEntries = await fs9.readdir(blocksDir, { withFileTypes: true });
|
|
2546
4533
|
for (const entry of metaEntries) {
|
|
2547
4534
|
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
2548
4535
|
const meta = await getSyncMetadataJson(entry.name, blocksDir);
|
|
@@ -2565,7 +4552,7 @@ async function listBlocks(baseDir) {
|
|
|
2565
4552
|
});
|
|
2566
4553
|
}
|
|
2567
4554
|
}
|
|
2568
|
-
const entries = await
|
|
4555
|
+
const entries = await fs9.readdir(blocksDir, { withFileTypes: true });
|
|
2569
4556
|
const blockFolders = /* @__PURE__ */ new Set();
|
|
2570
4557
|
for (const entry of entries) {
|
|
2571
4558
|
if (!entry.isDirectory()) continue;
|
|
@@ -2609,7 +4596,7 @@ async function listBlocks(baseDir) {
|
|
|
2609
4596
|
}
|
|
2610
4597
|
blocks.push(info);
|
|
2611
4598
|
} else if (!stats) {
|
|
2612
|
-
const blockPath =
|
|
4599
|
+
const blockPath = path13.join(blocksDir, blockId);
|
|
2613
4600
|
try {
|
|
2614
4601
|
const { mappings } = await discoverCsvFiles(blockPath);
|
|
2615
4602
|
if (mappings.tradelog) {
|
|
@@ -2677,16 +4664,16 @@ function convertToReportingTrade(raw) {
|
|
|
2677
4664
|
}
|
|
2678
4665
|
async function loadReportingLog(baseDir, blockId) {
|
|
2679
4666
|
const blocksDir = resolveBlocksBaseDir(baseDir);
|
|
2680
|
-
const blockPath =
|
|
4667
|
+
const blockPath = path13.join(blocksDir, blockId);
|
|
2681
4668
|
const { mappings } = await discoverCsvFiles(blockPath);
|
|
2682
4669
|
const filename = mappings.reportinglog || "reportinglog.csv";
|
|
2683
|
-
const reportingLogPath =
|
|
4670
|
+
const reportingLogPath = path13.join(blockPath, filename);
|
|
2684
4671
|
try {
|
|
2685
|
-
await
|
|
4672
|
+
await fs9.access(reportingLogPath);
|
|
2686
4673
|
} catch {
|
|
2687
4674
|
throw new Error(`reportinglog.csv not found in block: ${blockId}`);
|
|
2688
4675
|
}
|
|
2689
|
-
const content = await
|
|
4676
|
+
const content = await fs9.readFile(reportingLogPath, "utf-8");
|
|
2690
4677
|
const records = parseCSV(content);
|
|
2691
4678
|
const trades = [];
|
|
2692
4679
|
for (const record of records) {
|
|
@@ -2756,11 +4743,11 @@ async function importCsv(baseDir, options) {
|
|
|
2756
4743
|
let { csvType = "tradelog" } = options;
|
|
2757
4744
|
const blocksDir = resolveBlocksBaseDir(baseDir);
|
|
2758
4745
|
try {
|
|
2759
|
-
await
|
|
4746
|
+
await fs9.access(csvPath);
|
|
2760
4747
|
} catch {
|
|
2761
4748
|
throw new Error(`CSV file not found: ${csvPath}`);
|
|
2762
4749
|
}
|
|
2763
|
-
const content = await
|
|
4750
|
+
const content = await fs9.readFile(csvPath, "utf-8");
|
|
2764
4751
|
const records = parseCSV(content);
|
|
2765
4752
|
if (csvType === "tradelog" && records.length > 0) {
|
|
2766
4753
|
const headers = Object.keys(records[0]);
|
|
@@ -2777,9 +4764,9 @@ async function importCsv(baseDir, options) {
|
|
|
2777
4764
|
if (!blockId) {
|
|
2778
4765
|
throw new Error("Could not derive a valid block ID from the filename or provided name");
|
|
2779
4766
|
}
|
|
2780
|
-
const blockPath =
|
|
4767
|
+
const blockPath = path13.join(blocksDir, blockId);
|
|
2781
4768
|
try {
|
|
2782
|
-
await
|
|
4769
|
+
await fs9.access(blockPath);
|
|
2783
4770
|
throw new Error(
|
|
2784
4771
|
`Block "${blockId}" already exists. Use a different blockName or delete the existing block first.`
|
|
2785
4772
|
);
|
|
@@ -2788,10 +4775,10 @@ async function importCsv(baseDir, options) {
|
|
|
2788
4775
|
throw error;
|
|
2789
4776
|
}
|
|
2790
4777
|
}
|
|
2791
|
-
await
|
|
4778
|
+
await fs9.mkdir(blockPath, { recursive: true });
|
|
2792
4779
|
const targetFilename = csvType === "tradelog" ? "tradelog.csv" : csvType === "dailylog" ? "dailylog.csv" : "reportinglog.csv";
|
|
2793
|
-
const targetPath =
|
|
2794
|
-
await
|
|
4780
|
+
const targetPath = path13.join(blockPath, targetFilename);
|
|
4781
|
+
await fs9.copyFile(csvPath, targetPath);
|
|
2795
4782
|
let dateRange = {
|
|
2796
4783
|
start: null,
|
|
2797
4784
|
end: null
|
|
@@ -3036,7 +5023,7 @@ function versionedHash(hash) {
|
|
|
3036
5023
|
return `${hash}:${PARSE_VERSION}`;
|
|
3037
5024
|
}
|
|
3038
5025
|
async function syncBlockInternal(conn, blockId, blockPath) {
|
|
3039
|
-
const blocksDir =
|
|
5026
|
+
const blocksDir = path14.dirname(blockPath);
|
|
3040
5027
|
try {
|
|
3041
5028
|
const existingMetadata = await getSyncMetadata(conn, blockId, blocksDir);
|
|
3042
5029
|
const tradelogFilename = await findTradelogFile(blockPath);
|
|
@@ -3060,12 +5047,12 @@ async function syncBlockInternal(conn, blockId, blockPath) {
|
|
|
3060
5047
|
error: "No tradelog CSV found in block"
|
|
3061
5048
|
};
|
|
3062
5049
|
}
|
|
3063
|
-
const tradelogPath =
|
|
5050
|
+
const tradelogPath = path14.join(blockPath, tradelogFilename);
|
|
3064
5051
|
const tradelogHash = versionedHash(await hashFileContent(tradelogPath));
|
|
3065
5052
|
if (existingMetadata && existingMetadata.tradelog_hash === tradelogHash) {
|
|
3066
5053
|
const optionalLogs = await findOptionalLogFiles(blockPath);
|
|
3067
5054
|
if (optionalLogs.reportinglog) {
|
|
3068
|
-
const reportinglogPath =
|
|
5055
|
+
const reportinglogPath = path14.join(blockPath, optionalLogs.reportinglog);
|
|
3069
5056
|
const reportinglogHash = versionedHash(await hashFileContent(reportinglogPath));
|
|
3070
5057
|
if (existingMetadata.reportinglog_hash !== reportinglogHash) {
|
|
3071
5058
|
} else {
|
|
@@ -3081,7 +5068,7 @@ async function syncBlockInternal(conn, blockId, blockPath) {
|
|
|
3081
5068
|
await conn.run("BEGIN TRANSACTION");
|
|
3082
5069
|
try {
|
|
3083
5070
|
await conn.run("DELETE FROM trades.trade_data WHERE block_id = $1", [blockId]);
|
|
3084
|
-
const csvContent = await
|
|
5071
|
+
const csvContent = await fs10.readFile(tradelogPath, "utf-8");
|
|
3085
5072
|
const records = parseCSV2(csvContent);
|
|
3086
5073
|
const batchSize = 500;
|
|
3087
5074
|
for (let i = 0; i < records.length; i += batchSize) {
|
|
@@ -3093,7 +5080,7 @@ async function syncBlockInternal(conn, blockId, blockPath) {
|
|
|
3093
5080
|
if (optionalLogs.dailylog) {
|
|
3094
5081
|
try {
|
|
3095
5082
|
dailylogHash = versionedHash(
|
|
3096
|
-
await hashFileContent(
|
|
5083
|
+
await hashFileContent(path14.join(blockPath, optionalLogs.dailylog))
|
|
3097
5084
|
);
|
|
3098
5085
|
} catch {
|
|
3099
5086
|
}
|
|
@@ -3101,15 +5088,15 @@ async function syncBlockInternal(conn, blockId, blockPath) {
|
|
|
3101
5088
|
if (optionalLogs.reportinglog) {
|
|
3102
5089
|
try {
|
|
3103
5090
|
reportinglogHash = versionedHash(
|
|
3104
|
-
await hashFileContent(
|
|
5091
|
+
await hashFileContent(path14.join(blockPath, optionalLogs.reportinglog))
|
|
3105
5092
|
);
|
|
3106
5093
|
} catch {
|
|
3107
5094
|
}
|
|
3108
5095
|
}
|
|
3109
5096
|
await conn.run("DELETE FROM trades.reporting_data WHERE block_id = $1", [blockId]);
|
|
3110
5097
|
if (optionalLogs.reportinglog && reportinglogHash) {
|
|
3111
|
-
const reportingPath =
|
|
3112
|
-
const reportingContent = await
|
|
5098
|
+
const reportingPath = path14.join(blockPath, optionalLogs.reportinglog);
|
|
5099
|
+
const reportingContent = await fs10.readFile(reportingPath, "utf-8");
|
|
3113
5100
|
const reportingRecords = parseCSV2(reportingContent);
|
|
3114
5101
|
const reportingTrades = [];
|
|
3115
5102
|
const reportingTickers = [];
|
|
@@ -3177,7 +5164,7 @@ async function detectBlockChanges(conn, baseDir) {
|
|
|
3177
5164
|
const toSync = [];
|
|
3178
5165
|
const toDelete = [];
|
|
3179
5166
|
const syncedBlockIds = new Set(await getAllSyncedBlockIds(conn, baseDir));
|
|
3180
|
-
const entries = await
|
|
5167
|
+
const entries = await fs10.readdir(baseDir, { withFileTypes: true });
|
|
3181
5168
|
const folderNames = /* @__PURE__ */ new Set();
|
|
3182
5169
|
for (const entry of entries) {
|
|
3183
5170
|
if (!entry.isDirectory()) continue;
|
|
@@ -3186,7 +5173,7 @@ async function detectBlockChanges(conn, baseDir) {
|
|
|
3186
5173
|
continue;
|
|
3187
5174
|
const blockId = entry.name;
|
|
3188
5175
|
folderNames.add(blockId);
|
|
3189
|
-
const blockPath =
|
|
5176
|
+
const blockPath = path14.join(baseDir, blockId);
|
|
3190
5177
|
if (!syncedBlockIds.has(blockId)) {
|
|
3191
5178
|
const tradelog = await findTradelogFile(blockPath);
|
|
3192
5179
|
if (tradelog) {
|
|
@@ -3200,7 +5187,7 @@ async function detectBlockChanges(conn, baseDir) {
|
|
|
3200
5187
|
continue;
|
|
3201
5188
|
}
|
|
3202
5189
|
try {
|
|
3203
|
-
const tradelogPath =
|
|
5190
|
+
const tradelogPath = path14.join(blockPath, tradelogFilename);
|
|
3204
5191
|
const currentHash = versionedHash(await hashFileContent(tradelogPath));
|
|
3205
5192
|
const metadata = await getSyncMetadata(conn, blockId, baseDir);
|
|
3206
5193
|
if (!metadata || metadata.tradelog_hash !== currentHash) {
|
|
@@ -3208,7 +5195,7 @@ async function detectBlockChanges(conn, baseDir) {
|
|
|
3208
5195
|
} else {
|
|
3209
5196
|
const optionalLogs = await findOptionalLogFiles(blockPath);
|
|
3210
5197
|
if (optionalLogs.reportinglog) {
|
|
3211
|
-
const reportinglogPath =
|
|
5198
|
+
const reportinglogPath = path14.join(blockPath, optionalLogs.reportinglog);
|
|
3212
5199
|
const reportingHash = versionedHash(await hashFileContent(reportinglogPath));
|
|
3213
5200
|
if (metadata.reportinglog_hash !== reportingHash) {
|
|
3214
5201
|
toSync.push(blockId);
|
|
@@ -3250,7 +5237,7 @@ function setBlocksDir(dir) {
|
|
|
3250
5237
|
}
|
|
3251
5238
|
function getBlocksDir(baseDir) {
|
|
3252
5239
|
if (_blocksDir) return _blocksDir;
|
|
3253
|
-
const nestedBlocksDir =
|
|
5240
|
+
const nestedBlocksDir = path15.join(baseDir, "blocks");
|
|
3254
5241
|
return existsSync2(nestedBlocksDir) ? nestedBlocksDir : baseDir;
|
|
3255
5242
|
}
|
|
3256
5243
|
async function syncAllBlocks(baseDir) {
|
|
@@ -3269,7 +5256,7 @@ async function syncAllBlocks(baseDir) {
|
|
|
3269
5256
|
}
|
|
3270
5257
|
}
|
|
3271
5258
|
for (const blockId of toSync) {
|
|
3272
|
-
const blockPath =
|
|
5259
|
+
const blockPath = path15.join(blocksDir, blockId);
|
|
3273
5260
|
const result = await syncBlockInternal(conn, blockId, blockPath);
|
|
3274
5261
|
results.push(result);
|
|
3275
5262
|
if (result.status === "error" && result.error) {
|
|
@@ -3288,9 +5275,9 @@ async function syncAllBlocks(baseDir) {
|
|
|
3288
5275
|
async function syncBlock(blockId, baseDir) {
|
|
3289
5276
|
const conn = await getConnection(baseDir);
|
|
3290
5277
|
const blocksDir = getBlocksDir(baseDir);
|
|
3291
|
-
const blockPath =
|
|
5278
|
+
const blockPath = path15.join(blocksDir, blockId);
|
|
3292
5279
|
try {
|
|
3293
|
-
await
|
|
5280
|
+
await fs11.access(blockPath);
|
|
3294
5281
|
} catch {
|
|
3295
5282
|
const existing = await getSyncMetadata(conn, blockId, blocksDir);
|
|
3296
5283
|
if (existing) {
|
|
@@ -3305,6 +5292,35 @@ async function syncBlock(blockId, baseDir) {
|
|
|
3305
5292
|
export {
|
|
3306
5293
|
ensureMutableMarketTables,
|
|
3307
5294
|
ensureMarketDataTables,
|
|
5295
|
+
CANONICAL_JSON_VERSION,
|
|
5296
|
+
canonicalJson,
|
|
5297
|
+
canonicalJsonBytes,
|
|
5298
|
+
addressCanonicalJson,
|
|
5299
|
+
addressBytes,
|
|
5300
|
+
parseCanonicalJsonAddress,
|
|
5301
|
+
parseSha256Address,
|
|
5302
|
+
ContentObjectCollisionError,
|
|
5303
|
+
ContentObjectStore,
|
|
5304
|
+
MARKET_DATASETS,
|
|
5305
|
+
isRealMarketSessionDate,
|
|
5306
|
+
marketDatasetDefinition,
|
|
5307
|
+
canonicalPartitionDataset,
|
|
5308
|
+
validatePartitionIdentity,
|
|
5309
|
+
canonicalPartitionRelativePath,
|
|
5310
|
+
PARTITION_COMMIT_RECEIPT_KIND,
|
|
5311
|
+
PARTITION_COMMIT_RECEIPT_VERSION,
|
|
5312
|
+
PARTITION_COMMIT_EVENT_KIND,
|
|
5313
|
+
PARTITION_COMMIT_EVENT_VERSION,
|
|
5314
|
+
INTERNAL_HISTORICAL_PARTITION_ADOPTION,
|
|
5315
|
+
setPartitionCommitTestFault,
|
|
5316
|
+
PartitionFileIntegrityError,
|
|
5317
|
+
FilePartitionCommitStore,
|
|
5318
|
+
publishRefreshCompletionAuthority,
|
|
5319
|
+
verifyRefreshCompletionAuthority,
|
|
5320
|
+
PartitionFilePublicationError,
|
|
5321
|
+
runPartitionCommitAttempt,
|
|
5322
|
+
activePartitionCommitAttempt,
|
|
5323
|
+
capturePartitionCommitReceipt,
|
|
3308
5324
|
resolveDbPath,
|
|
3309
5325
|
setDataRoot,
|
|
3310
5326
|
getDataRoot,
|
|
@@ -3321,8 +5337,12 @@ export {
|
|
|
3321
5337
|
writeQuoteMinutesPartition,
|
|
3322
5338
|
writeOiDailyPartition,
|
|
3323
5339
|
writeEnrichedTickerFile,
|
|
5340
|
+
writeEnrichedTickerPartition,
|
|
3324
5341
|
writeEnrichedContext,
|
|
5342
|
+
writeEnrichedContextPartition,
|
|
3325
5343
|
isParquetMode,
|
|
5344
|
+
ParquetProvenanceOrphanError,
|
|
5345
|
+
UnmanifestedParquetWriteError,
|
|
3326
5346
|
writeParquetAtomic,
|
|
3327
5347
|
writeParquetPartition,
|
|
3328
5348
|
writeJsonFile,
|
|
@@ -3369,12 +5389,6 @@ export {
|
|
|
3369
5389
|
getProfile,
|
|
3370
5390
|
listProfiles,
|
|
3371
5391
|
deleteProfile,
|
|
3372
|
-
escapeSqlLiteral,
|
|
3373
|
-
readParquetFilesSql,
|
|
3374
|
-
describeReadParquetColumns,
|
|
3375
|
-
describeQueryColumns,
|
|
3376
|
-
quoteParquetCanonicalProjection,
|
|
3377
|
-
quoteParquetCanonicalWriteProjection,
|
|
3378
5392
|
createMarketParquetViews,
|
|
3379
5393
|
migrateMetadataToJson,
|
|
3380
5394
|
getConnection,
|
|
@@ -3393,4 +5407,4 @@ export {
|
|
|
3393
5407
|
loadReportingLog,
|
|
3394
5408
|
importCsv
|
|
3395
5409
|
};
|
|
3396
|
-
//# sourceMappingURL=chunk-
|
|
5410
|
+
//# sourceMappingURL=chunk-5HBZ3TZM.js.map
|