-
Notifications
You must be signed in to change notification settings - Fork 834
feat(new-compiler): migrate metadata storage from lockfile to LMDB #1955
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
Open
AndreyHirsa
wants to merge
5
commits into
main
Choose a base branch
from
feat/metadata-lmdb-storage
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
a1edc10
feat(new-compiler): migrate metadata storage from lockfile to LMDB
AndreyHirsa dc7bd46
fix(new-compiler): use correct assertion for sync constructor throw
AndreyHirsa b2f695f
fix(new-compiler): remove await from sync cleanupExistingMetadata calls
AndreyHirsa 1bfb5ea
refactor(new-compiler): replace MetadataManager class with pure funct…
AndreyHirsa c82ee97
fix(new-compiler): optimization fixes
AndreyHirsa File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| --- | ||
| "@lingo.dev/compiler": patch | ||
| --- | ||
|
|
||
| - Migrate metadata storage from JSON files to LMDB | ||
| - New storage locations: .lingo/metadata-dev/ and .lingo/metadata-build/ | ||
| - Use pure functions with short-lived connections for multi-worker safety | ||
| - Update compiler docs | ||
| - Remove proper-lockfile dependency |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,219 @@ | ||
| import { describe, it, expect, beforeEach, afterEach } from "vitest"; | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| import os from "os"; | ||
| import { | ||
| createEmptyMetadata, | ||
| loadMetadata, | ||
| saveMetadata, | ||
| cleanupExistingMetadata, | ||
| getMetadataPath, | ||
| } from "./manager"; | ||
| import type { TranslationEntry } from "../types"; | ||
|
|
||
| function createTestEntry( | ||
| overrides: Partial<TranslationEntry> & { | ||
| hash?: string; | ||
| sourceText?: string; | ||
| } = {}, | ||
| ): TranslationEntry { | ||
| const hash = overrides.hash ?? `hash_${Math.random().toString(36).slice(2)}`; | ||
| return { | ||
| type: "content", | ||
| hash, | ||
| sourceText: overrides.sourceText ?? `Source text for ${hash}`, | ||
| context: { filePath: "test.tsx", componentName: "TestComponent" }, | ||
| location: { filePath: "test.tsx", line: 1, column: 1 }, | ||
| ...overrides, | ||
| } as TranslationEntry; | ||
| } | ||
|
|
||
| function createUniqueDbPath(): string { | ||
| return path.join( | ||
| os.tmpdir(), | ||
| `lmdb-test-${Date.now()}-${Math.random().toString(36).slice(2)}`, | ||
| ); | ||
| } | ||
|
|
||
| describe("metadata", () => { | ||
| let testDbPath: string; | ||
|
|
||
| beforeEach(() => { | ||
| testDbPath = createUniqueDbPath(); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| cleanupExistingMetadata(testDbPath); | ||
| }); | ||
|
|
||
| describe("createEmptyMetadata", () => { | ||
| it("should return valid empty metadata structure", () => { | ||
| const metadata = createEmptyMetadata(); | ||
|
|
||
| expect(metadata.entries).toEqual({}); | ||
| expect(metadata.stats!.totalEntries).toBe(0); | ||
| // Verify valid ISO date | ||
| const date = new Date(metadata.stats!.lastUpdated); | ||
| expect(date.getTime()).not.toBeNaN(); | ||
| }); | ||
| }); | ||
|
|
||
| describe("loadMetadata", () => { | ||
| it("should return empty metadata for new database", async () => { | ||
| const metadata = await loadMetadata(testDbPath); | ||
| expect(metadata.entries).toEqual({}); | ||
| expect(metadata.stats!.totalEntries).toBe(0); | ||
| }); | ||
|
|
||
| it("should load and preserve all entry fields", async () => { | ||
| const entry: TranslationEntry = { | ||
| type: "content", | ||
| hash: "full-entry", | ||
| sourceText: "Hello world", | ||
| context: { filePath: "app.tsx", componentName: "AppComponent" }, | ||
| location: { filePath: "app.tsx", line: 42, column: 10 }, | ||
| }; | ||
|
|
||
| await saveMetadata(testDbPath, [entry]); | ||
| const metadata = await loadMetadata(testDbPath); | ||
|
|
||
| expect(metadata.entries["full-entry"]).toEqual(entry); | ||
| expect(metadata.stats!.totalEntries).toBe(1); | ||
| }); | ||
|
|
||
| it("should handle entries with very long sourceText", async () => { | ||
| const longText = "A".repeat(100000); | ||
| await saveMetadata(testDbPath, [ | ||
| createTestEntry({ hash: "long-text", sourceText: longText }), | ||
| ]); | ||
|
|
||
| const metadata = await loadMetadata(testDbPath); | ||
| expect(metadata.entries["long-text"].sourceText).toBe(longText); | ||
| }); | ||
| }); | ||
|
|
||
| describe("saveMetadata", () => { | ||
| it("should save, accumulate, and update entries correctly", async () => { | ||
| // Save single entry | ||
| await saveMetadata(testDbPath, [ | ||
| createTestEntry({ hash: "entry-1", sourceText: "v1" }), | ||
| ]); | ||
| expect((await loadMetadata(testDbPath)).stats!.totalEntries).toBe(1); | ||
|
|
||
| // Accumulate multiple entries | ||
| await saveMetadata(testDbPath, [ | ||
| createTestEntry({ hash: "entry-2" }), | ||
| createTestEntry({ hash: "entry-3" }), | ||
| ]); | ||
| expect((await loadMetadata(testDbPath)).stats!.totalEntries).toBe(3); | ||
|
|
||
| // Update existing entry (count should not increase) | ||
| await saveMetadata(testDbPath, [ | ||
| createTestEntry({ hash: "entry-1", sourceText: "v2" }), | ||
| ]); | ||
| const updated = await loadMetadata(testDbPath); | ||
| expect(updated.stats!.totalEntries).toBe(3); | ||
| expect(updated.entries["entry-1"].sourceText).toBe("v2"); | ||
|
|
||
| // Empty array should not change anything | ||
| await saveMetadata(testDbPath, []); | ||
| expect((await loadMetadata(testDbPath)).stats!.totalEntries).toBe(3); | ||
| }); | ||
|
|
||
| it("should handle large batch of entries", async () => { | ||
| const entries = Array.from({ length: 100 }, (_, i) => | ||
| createTestEntry({ hash: `batch-${i}` }), | ||
| ); | ||
|
|
||
| await saveMetadata(testDbPath, entries); | ||
| expect((await loadMetadata(testDbPath)).stats!.totalEntries).toBe(100); | ||
| }); | ||
|
|
||
| it("should maintain data integrity after many operations", async () => { | ||
| // Many saves with overlapping keys | ||
| for (let i = 0; i < 10; i++) { | ||
| await saveMetadata(testDbPath, [ | ||
| createTestEntry({ | ||
| hash: `persistent-${i % 5}`, | ||
| sourceText: `v${i}`, | ||
| }), | ||
| createTestEntry({ hash: `unique-${i}` }), | ||
| ]); | ||
| } | ||
|
|
||
| const final = await loadMetadata(testDbPath); | ||
| // 5 persistent + 10 unique = 15 | ||
| expect(final.stats!.totalEntries).toBe(15); | ||
| }); | ||
| }); | ||
|
|
||
| describe("concurrent access (single process)", () => { | ||
| it("should handle concurrent operations from multiple calls", async () => { | ||
| // LMDB handles concurrent writes via OS-level locking | ||
| const promises = Array.from({ length: 10 }, async (_, i) => { | ||
| await saveMetadata(testDbPath, [ | ||
| createTestEntry({ hash: `concurrent-${i}` }), | ||
| ]); | ||
| }); | ||
| await Promise.all(promises); | ||
|
|
||
| // Verify all entries are present | ||
| expect((await loadMetadata(testDbPath)).stats!.totalEntries).toBe(10); | ||
| }); | ||
| }); | ||
|
|
||
| describe("cleanupExistingMetadata", () => { | ||
| it("should remove database and allow reopening with fresh state", async () => { | ||
| await saveMetadata(testDbPath, [createTestEntry({ hash: "before" })]); | ||
| expect(fs.existsSync(testDbPath)).toBe(true); | ||
|
|
||
| // Cleanup should succeed because saveMetadata closes the DB | ||
| cleanupExistingMetadata(testDbPath); | ||
| expect(fs.existsSync(testDbPath)).toBe(false); | ||
|
|
||
| // Should work with fresh state after cleanup | ||
| const metadata = await loadMetadata(testDbPath); | ||
| expect(metadata.entries["before"]).toBeUndefined(); | ||
| expect(metadata.stats!.totalEntries).toBe(0); | ||
| }); | ||
|
|
||
| it("should handle non-existent path and multiple calls gracefully", () => { | ||
| const nonExistent = path.join(os.tmpdir(), "does-not-exist-db"); | ||
| expect(() => cleanupExistingMetadata(nonExistent)).not.toThrow(); | ||
| expect(() => cleanupExistingMetadata(nonExistent)).not.toThrow(); | ||
| }); | ||
| }); | ||
|
|
||
| describe("getMetadataPath", () => { | ||
| it("should return correct path based on environment and config", () => { | ||
| const devResult = getMetadataPath({ | ||
| sourceRoot: "/app", | ||
| lingoDir: ".lingo", | ||
| environment: "development", | ||
| }); | ||
| expect(devResult).toContain("metadata-dev"); | ||
| expect(devResult).not.toContain("metadata-build"); | ||
|
|
||
| const prodResult = getMetadataPath({ | ||
| sourceRoot: "/app", | ||
| lingoDir: ".lingo", | ||
| environment: "production", | ||
| }); | ||
| expect(prodResult).toContain("metadata-build"); | ||
|
|
||
| const customResult = getMetadataPath({ | ||
| sourceRoot: "/app", | ||
| lingoDir: ".custom-lingo", | ||
| environment: "development", | ||
| }); | ||
| expect(customResult).toContain(".custom-lingo"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("error handling", () => { | ||
| it("should throw descriptive error for invalid path", async () => { | ||
| const invalidPath = "/root/definitely/cannot/create/this/path"; | ||
| await expect(loadMetadata(invalidPath)).rejects.toThrow(); | ||
| }); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Make the invalid-path test deterministic across CI environments.
Line 215 uses a hard-coded
/root/...path, which can be writable when tests run as root, soloadMetadatamay not throw and the test can flake. Use a temp file path to guaranteemkdirSyncfails.🔧 Suggested fix
describe("error handling", () => { it("should throw descriptive error for invalid path", async () => { - const invalidPath = "/root/definitely/cannot/create/this/path"; - await expect(loadMetadata(invalidPath)).rejects.toThrow(); + const invalidPath = path.join( + os.tmpdir(), + `lmdb-invalid-${Date.now()}-${Math.random().toString(36).slice(2)}`, + ); + fs.writeFileSync(invalidPath, "not a directory"); + try { + await expect(loadMetadata(invalidPath)).rejects.toThrow(); + } finally { + fs.rmSync(invalidPath, { force: true }); + } }); });As per coding guidelines, "When you add tests, make sure they pass".
📝 Committable suggestion
🤖 Prompt for AI Agents