-
Notifications
You must be signed in to change notification settings - Fork 0
fix(pipeline): align stale day detection with local midnight #48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
286b532
0cffde3
f45ff21
3e811af
1eed41c
f8bf29c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| import fs from 'fs'; | ||
| import path from 'path'; | ||
| import { createRequire } from 'node:module'; | ||
| import Database from 'better-sqlite3'; | ||
| import type { DatasetSummary } from '$lib/types/types'; | ||
| import { getDatasetsConfigPath, getRepoRoot } from '$lib/server/paths'; | ||
|
|
@@ -17,9 +18,21 @@ export interface DatasetConfig { | |
|
|
||
| const repoRoot = getRepoRoot(); | ||
| const defaultRegistryPath = getDatasetsConfigPath(); | ||
| const require = createRequire(import.meta.url); | ||
|
|
||
| type PreparedStatement = { | ||
| get(...params: unknown[]): unknown; | ||
| all(...params: unknown[]): unknown[]; | ||
| }; | ||
|
|
||
| export interface ReadonlyDatasetDb { | ||
| prepare(sql: string): PreparedStatement; | ||
| close(): void; | ||
| } | ||
|
|
||
| const datasetDbCache = new Map< | ||
| string, | ||
| { db: Database.Database; dbPath: string; mtimeMs: number } | ||
| { db: ReadonlyDatasetDb; dbPath: string; mtimeMs: number } | ||
| >(); | ||
| const datasetDefaultStartCache = new Map< | ||
| string, | ||
|
|
@@ -197,7 +210,25 @@ export function getDatasetDbPath(datasetId: string): string { | |
| return getDatasetConfig(datasetId).db_path; | ||
| } | ||
|
|
||
| export function getDatasetDb(datasetId: string): Database.Database { | ||
| function openNodeSqliteDatabase(dbPath: string): ReadonlyDatasetDb { | ||
| const { DatabaseSync } = require('node:sqlite') as typeof import('node:sqlite'); | ||
| type SQLInputValue = import('node:sqlite').SQLInputValue; | ||
| const db = new DatabaseSync(dbPath, { open: true, readOnly: true }); | ||
| return { | ||
| prepare(sql: string): PreparedStatement { | ||
| const stmt = db.prepare(sql); | ||
| return { | ||
| get: (...params: unknown[]) => stmt.get(...(params as SQLInputValue[])), | ||
| all: (...params: unknown[]) => stmt.all(...(params as SQLInputValue[])) | ||
| }; | ||
| }, | ||
| close() { | ||
| db.close(); | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| export function getDatasetDb(datasetId: string): ReadonlyDatasetDb { | ||
| const dbPath = getDatasetDbPath(datasetId); | ||
| if (!fs.existsSync(dbPath)) { | ||
| throw new Error(`Dataset database not found for '${datasetId}' at ${dbPath}`); | ||
|
|
@@ -217,7 +248,18 @@ export function getDatasetDb(datasetId: string): Database.Database { | |
| } | ||
| } | ||
|
|
||
| const db = new Database(dbPath, { readonly: true }); | ||
| let db: ReadonlyDatasetDb; | ||
| try { | ||
| db = new Database(dbPath, { readonly: true }); | ||
| } catch (error) { | ||
| if (error instanceof Error && 'code' in error && error.code === 'ERR_DLOPEN_FAILED') { | ||
| console.warn(`better-sqlite3 failed to load for '${datasetId}', falling back to node:sqlite`); | ||
| db = openNodeSqliteDatabase(dbPath); | ||
| } else { | ||
| throw error; | ||
| } | ||
| } | ||
|
Comment on lines
+251
to
+261
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing error handling when If 💡 Suggested improvement try {
db = new Database(dbPath, { readonly: true });
} catch (error) {
if (error instanceof Error && 'code' in error && error.code === 'ERR_DLOPEN_FAILED') {
console.warn(`better-sqlite3 failed to load for '${datasetId}', falling back to node:sqlite`);
- db = openNodeSqliteDatabase(dbPath);
+ try {
+ db = openNodeSqliteDatabase(dbPath);
+ } catch (fallbackError) {
+ console.error(`node:sqlite fallback also failed for '${datasetId}':`, fallbackError);
+ throw new Error(
+ `Failed to open database for '${datasetId}': better-sqlite3 and node:sqlite both unavailable`
+ );
+ }
} else {
throw error;
}
}🤖 Prompt for AI Agents |
||
|
|
||
| datasetDbCache.set(datasetId, { db, dbPath, mtimeMs: stat.mtimeMs }); | ||
| return db; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,25 +4,54 @@ import path from 'path'; | |
| import { spawnSync } from 'child_process'; | ||
| import { afterEach, describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| vi.mock('better-sqlite3', () => ({ | ||
| default: class MockDatabase { | ||
| constructor(private readonly dbPath: string) {} | ||
|
|
||
| prepare(query: string) { | ||
| return { | ||
| get: () => { | ||
| const result = spawnSync('sqlite3', [this.dbPath, query], { encoding: 'utf-8' }); | ||
| if (result.status !== 0) { | ||
| throw new Error(result.stderr || 'sqlite3 query failed'); | ||
| } | ||
| class MockDatabaseSync { | ||
| constructor(private readonly dbPath: string) {} | ||
|
|
||
| prepare(query: string) { | ||
| return { | ||
| get: () => { | ||
| const result = spawnSync('sqlite3', [this.dbPath, query], { encoding: 'utf-8' }); | ||
| if (result.status !== 0) { | ||
| throw new Error(result.stderr || 'sqlite3 query failed'); | ||
| } | ||
|
|
||
| const minTimestamp = Number(result.stdout.trim()); | ||
| return { minTimestamp: Number.isFinite(minTimestamp) ? minTimestamp : null }; | ||
| }, | ||
| all: () => [] | ||
| }; | ||
| } | ||
|
|
||
| close() {} | ||
| } | ||
|
|
||
| const minTimestamp = Number(result.stdout.trim()); | ||
| return { minTimestamp: Number.isFinite(minTimestamp) ? minTimestamp : null }; | ||
| const betterSqlite3Factory = vi.fn((dbPath: string) => ({ | ||
| prepare(query: string) { | ||
| return { | ||
| get: () => { | ||
| const result = spawnSync('sqlite3', [dbPath, query], { encoding: 'utf-8' }); | ||
| if (result.status !== 0) { | ||
| throw new Error(result.stderr || 'sqlite3 query failed'); | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| close() {} | ||
| const minTimestamp = Number(result.stdout.trim()); | ||
| return { minTimestamp: Number.isFinite(minTimestamp) ? minTimestamp : null }; | ||
| } | ||
| }; | ||
| }, | ||
| close() {} | ||
| })); | ||
|
|
||
| vi.mock('better-sqlite3', () => ({ | ||
| default: vi.fn().mockImplementation((dbPath: string) => betterSqlite3Factory(dbPath)) | ||
| })); | ||
|
Comment on lines
+28
to
47
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mock structure doesn't support testing the The
To properly test the fallback, you need to also mock 💡 Suggested approach: Skip test if node:sqlite unavailable- it('falls back to node:sqlite when better-sqlite3 fails to load', async () => {
+ it('falls back to node:sqlite when better-sqlite3 fails to load', async () => {
+ // Skip if node:sqlite is not available (requires Node 22.5+)
+ try {
+ require('node:sqlite');
+ } catch {
+ console.log('Skipping test: node:sqlite not available');
+ return;
+ }
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'datasets-test-'));Or use Vitest's const nodeSqliteAvailable = (() => {
try {
require('node:sqlite');
return true;
} catch {
return false;
}
})();
it.skipIf(!nodeSqliteAvailable)('falls back to node:sqlite when better-sqlite3 fails to load', async () => {🤖 Prompt for AI Agents |
||
|
|
||
| vi.mock('node:module', async () => ({ | ||
| createRequire: () => (specifier: string) => { | ||
| if (specifier === 'node:sqlite') { | ||
| return { DatabaseSync: MockDatabaseSync }; | ||
| } | ||
| throw new Error(`Unexpected require: ${specifier}`); | ||
| } | ||
| })); | ||
|
|
||
|
|
@@ -34,6 +63,7 @@ async function loadDatasetsModule() { | |
| describe('dataset server helpers', () => { | ||
| afterEach(() => { | ||
| vi.unstubAllEnvs(); | ||
| betterSqlite3Factory.mockClear(); | ||
| }); | ||
|
|
||
| it('lists dataset summaries from registry + sqlite min timestamp', async () => { | ||
|
|
@@ -115,4 +145,43 @@ describe('dataset server helpers', () => { | |
| expect(datasets.listDatasetSources('alpha')).toEqual(['r1', 'r2']); | ||
| expect(() => datasets.getDatasetConfig('missing')).toThrow(/Unknown dataset 'missing'/); | ||
| }); | ||
|
|
||
| it('falls back to node:sqlite when better-sqlite3 fails to load', async () => { | ||
| const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'datasets-test-')); | ||
| const dbPath = path.join(tempDir, 'netflow.sqlite'); | ||
| const registryPath = path.join(tempDir, 'datasets.json'); | ||
|
|
||
| const seedResult = spawnSync( | ||
| 'sqlite3', | ||
| [ | ||
| dbPath, | ||
| 'CREATE TABLE netflow_stats (timestamp INTEGER NOT NULL); INSERT INTO netflow_stats (timestamp) VALUES (1740823200);' | ||
| ], | ||
| { encoding: 'utf-8' } | ||
| ); | ||
| expect(seedResult.status).toBe(0); | ||
|
|
||
| fs.writeFileSync( | ||
| registryPath, | ||
| JSON.stringify([ | ||
| { | ||
| dataset_id: 'alpha', | ||
| label: 'Alpha Label', | ||
| root_path: tempDir, | ||
| db_path: dbPath | ||
| } | ||
| ]) | ||
| ); | ||
|
|
||
| betterSqlite3Factory.mockImplementationOnce(() => { | ||
| const error = new Error('Module did not self-register') as Error & { code: string }; | ||
| error.code = 'ERR_DLOPEN_FAILED'; | ||
| throw error; | ||
| }); | ||
|
|
||
| vi.stubEnv('DATASETS_CONFIG_PATH', registryPath); | ||
|
|
||
| const datasets = await loadDatasetsModule(); | ||
| expect(datasets.getDatasetDefaultStartDate('alpha')).toBe('2025-03-01'); | ||
| }); | ||
|
Comment on lines
+149
to
+186
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check Node.js version to verify node:sqlite availability (requires Node 22.5+)
node --version
# Check if node:sqlite is available
node -e "try { require('node:sqlite'); console.log('node:sqlite available'); } catch(e) { console.log('node:sqlite NOT available:', e.message); }"Repository: flamboh/netflow-analysis Length of output: 279 🏁 Script executed: # Read the test file to understand the implementation
head -160 apps/web/tests/lib/server/datasets.test.ts | tail -45Repository: flamboh/netflow-analysis Length of output: 1385 🏁 Script executed: # Find and read the datasets.ts file to understand the fallback logic
fd 'datasets\.ts$' --type f | grep -v test | head -1Repository: flamboh/netflow-analysis Length of output: 96 🏁 Script executed: # Read the datasets.ts file to understand the fallback logic
cat -n apps/web/src/lib/datasets.ts | head -200Repository: flamboh/netflow-analysis Length of output: 2404 🏁 Script executed: # Find server-side datasets file
fd 'datasets\.ts$' --type f apps/web/src/lib/server/ 2>/dev/nullRepository: flamboh/netflow-analysis Length of output: 103 🏁 Script executed: # Search for getFallbackStartDate function
rg 'getFallbackStartDate' --type tsRepository: flamboh/netflow-analysis Length of output: 282 🏁 Script executed: # Read the server-side datasets.ts file
cat -n apps/web/src/lib/server/datasets.ts | head -250Repository: flamboh/netflow-analysis Length of output: 8955 🏁 Script executed: # Continue reading to see the complete getDatasetDb implementation
cat -n apps/web/src/lib/server/datasets.ts | sed -n '250,280p'Repository: flamboh/netflow-analysis Length of output: 859 🏁 Script executed: # Check the test file setup to see how mocks are configured
head -40 apps/web/tests/lib/server/datasets.test.tsRepository: flamboh/netflow-analysis Length of output: 1227 🏁 Script executed: # Check if there are any environment setup or mocking issues in the test
grep -n "node:sqlite" apps/web/tests/lib/server/datasets.test.tsRepository: flamboh/netflow-analysis Length of output: 152 🏁 Script executed: # Check if node:sqlite requires special handling in the test environment
grep -n "vi.mock" apps/web/tests/lib/server/datasets.test.tsRepository: flamboh/netflow-analysis Length of output: 105 🏁 Script executed: # Check if the database seeding command works (recreate the test scenario)
tempDir=$(mktemp -d)
dbPath="$tempDir/test.sqlite"
sqlite3 "$dbPath" "CREATE TABLE netflow_stats (timestamp INTEGER NOT NULL); INSERT INTO netflow_stats (timestamp) VALUES (1740823200);"
echo "Seeding status: $?"
# Test if node:sqlite can read this database
node -e "
try {
const { DatabaseSync } = require('node:sqlite');
const db = new DatabaseSync('$dbPath', { open: true, readOnly: true });
const row = db.prepare('SELECT MIN(timestamp) AS minTimestamp FROM netflow_stats').get();
console.log('Query result:', row);
db.close();
} catch (e) {
console.log('Error:', e.message);
}
"
# Clean up
rm -rf "$tempDir"Repository: flamboh/netflow-analysis Length of output: 351 🏁 Script executed: # Check if the test has any skip conditions or checks for sqlite3
grep -B5 -A5 "spawnSync" apps/web/tests/lib/server/datasets.test.ts | head -20Repository: flamboh/netflow-analysis Length of output: 804 🏁 Script executed: # Check what the actual error is when running the test
grep -n "spawnSync" apps/web/tests/lib/server/datasets.test.tsRepository: flamboh/netflow-analysis Length of output: 267 🏁 Script executed: # Check the exact test assertion for seedResult
sed -n '119,160p' apps/web/tests/lib/server/datasets.test.ts | grep -n "seedResult"Repository: flamboh/netflow-analysis Length of output: 141 Test fails because database seeding requires the The test attempts to seed a SQLite database using To fix this, either:
Note: 🧰 Tools🪛 GitHub Actions: CI[error] 155-155: Test failed: expected '2026-02-22' to be '2025-03-01' (Object.is equality). 🪛 GitHub Check: Web + Python Checks[failure] 155-155: tests/lib/server/datasets.test.ts > dataset server helpers > falls back to node:sqlite when better-sqlite3 fails to load Expected: "2025-03-01" ❯ tests/lib/server/datasets.test.ts:155:56 🤖 Prompt for AI Agents |
||
| }); | ||
This file was deleted.
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
What Node.js version introduced node:sqlite module?💡 Result:
Node.js version 22.5.0 introduced the node:sqlite module.
Citations:
Add graceful error handling for
node:sqlitemodule availability.The
node:sqlitemodule was introduced in Node.js 22.5.0 and is not available in earlier versions. The pipeline failure showsERR_UNKNOWN_BUILTIN_MODULE, indicating the environment uses an older Node.js version. The current code path will fail when this module cannot be loaded, without clear error recovery.Add explicit error handling for module import failures and document the minimum Node.js version requirement. Consider whether the environment guarantees Node.js 22.5.0+, or implement a fallback mechanism that handles missing module gracefully.
🧰 Tools
🪛 GitHub Actions: CI
[error] 214-214: Failed to derive default start date for dataset 'alpha'. better-sqlite3 failed to load; fallback to node:sqlite failed with 'ERR_UNKNOWN_BUILTIN_MODULE' / Error: No such built-in module: node:sqlite.
🤖 Prompt for AI Agents