Repository navigation
fix(data): publish migration and cursor corrections (successor to #185) #195
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
PeterGuy326
wants to merge
8
commits into
main
Choose a base branch
from
codex/fix-pr-185
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
8 commits
Select commit
Hold shift + click to select a range
bc118e5
chore(data): resolve five data-plane hygiene items from index audit (…
liyuanyang bc07159
fix(data): rebase onto main, renumber migration to 0026
liyuanyang ac4a3c9
fix(data): preserve relation cursor ties and verify populated migrations
PeterGuy326 df63c42
chore(data): integrate latest main before #195
waterbro-8 71db9ab
chore(data): sequence hygiene migration after main 0026
waterbro-8 d7a16a7
Merge remote-tracking branch 'origin/main' into fix195
sun-970 e4e085f
fix(db): resequence hygiene migration to 0028 after main took 0027
sun-970 8263a01
fix(ci): point migration runner at head 28
sun-970 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
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
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,85 @@ | ||
| -- +goose Up | ||
| -- Data-plane hygiene from the index audit (#178), sequenced after main's 0026. | ||
| -- Three independent fixes bundled into one migration because each is a single | ||
| -- DDL statement and none warrants its own schema version. | ||
|
|
||
| -- Item 1: embeddings_text uniqueness on (file_id, chunk_index). | ||
| -- The write path (indexer.go) DELETEs all chunks for a file before re-inserting, | ||
| -- so duplicates should never exist in practice. Deduplicate defensively before | ||
| -- adding the constraint: if any duplicates survived, keep the row with the | ||
| -- smallest UUID for deterministic selection (UUID order is not insert order). | ||
| -- +goose StatementBegin | ||
| DELETE FROM embeddings_text | ||
| WHERE id NOT IN ( | ||
| SELECT DISTINCT ON (file_id, chunk_index) id | ||
| FROM embeddings_text | ||
| ORDER BY file_id, chunk_index, id | ||
| ); | ||
| -- +goose StatementEnd | ||
|
|
||
| -- +goose StatementBegin | ||
| ALTER TABLE embeddings_text | ||
| ADD CONSTRAINT uq_embeddings_text_file_chunk UNIQUE (file_id, chunk_index); | ||
| -- +goose StatementEnd | ||
|
|
||
| -- Item 2: partial indexes for ON DELETE SET NULL lookups on memories. | ||
| -- Without these, every file or user delete takes a RowExclusiveLock on memories | ||
| -- and performs a sequential scan to find the rows to null out. | ||
| -- +goose StatementBegin | ||
| CREATE INDEX IF NOT EXISTS idx_memories_source_file_id | ||
| ON memories (source_file_id) WHERE source_file_id IS NOT NULL; | ||
| -- +goose StatementEnd | ||
|
|
||
| -- +goose StatementBegin | ||
| CREATE INDEX IF NOT EXISTS idx_memories_created_by_user_id | ||
| ON memories (created_by_user_id) WHERE created_by_user_id IS NOT NULL; | ||
| -- +goose StatementEnd | ||
|
|
||
| -- Item 3: memory_relations FKs must cascade with memories. | ||
| -- memories.workspace_id is ON DELETE CASCADE, so a workspace delete removes | ||
| -- memories rows. Without matching cascade on memory_relations, the delete then | ||
| -- fails on any edge touching those memories. Align the referential actions. | ||
| -- +goose StatementBegin | ||
| ALTER TABLE memory_relations | ||
| DROP CONSTRAINT IF EXISTS memory_relations_workspace_id_fkey, | ||
| DROP CONSTRAINT IF EXISTS memory_relations_source_id_fkey, | ||
| DROP CONSTRAINT IF EXISTS memory_relations_target_id_fkey; | ||
| -- +goose StatementEnd | ||
|
|
||
| -- +goose StatementBegin | ||
| ALTER TABLE memory_relations | ||
| ADD CONSTRAINT memory_relations_workspace_id_fkey | ||
| FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE, | ||
| ADD CONSTRAINT memory_relations_source_id_fkey | ||
| FOREIGN KEY (source_id) REFERENCES memories(id) ON DELETE CASCADE, | ||
| ADD CONSTRAINT memory_relations_target_id_fkey | ||
| FOREIGN KEY (target_id) REFERENCES memories(id) ON DELETE CASCADE; | ||
| -- +goose StatementEnd | ||
|
|
||
| -- +goose Down | ||
| -- +goose StatementBegin | ||
| ALTER TABLE memory_relations | ||
| DROP CONSTRAINT IF EXISTS memory_relations_workspace_id_fkey, | ||
| DROP CONSTRAINT IF EXISTS memory_relations_source_id_fkey, | ||
| DROP CONSTRAINT IF EXISTS memory_relations_target_id_fkey; | ||
| -- +goose StatementEnd | ||
|
|
||
| -- +goose StatementBegin | ||
| ALTER TABLE memory_relations | ||
| ADD CONSTRAINT memory_relations_workspace_id_fkey | ||
| FOREIGN KEY (workspace_id) REFERENCES workspaces(id), | ||
| ADD CONSTRAINT memory_relations_source_id_fkey | ||
| FOREIGN KEY (source_id) REFERENCES memories(id), | ||
| ADD CONSTRAINT memory_relations_target_id_fkey | ||
| FOREIGN KEY (target_id) REFERENCES memories(id); | ||
| -- +goose StatementEnd | ||
|
|
||
| -- +goose StatementBegin | ||
| DROP INDEX IF EXISTS idx_memories_created_by_user_id; | ||
| DROP INDEX IF EXISTS idx_memories_source_file_id; | ||
| -- +goose StatementEnd | ||
|
|
||
| -- +goose StatementBegin | ||
| ALTER TABLE embeddings_text | ||
| DROP CONSTRAINT IF EXISTS uq_embeddings_text_file_chunk; | ||
| -- +goose StatementEnd | ||
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,109 @@ | ||
| package db | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "os" | ||
| "strings" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/google/uuid" | ||
| "github.com/jackc/pgx/v5/pgconn" | ||
| "github.com/jackc/pgx/v5/pgxpool" | ||
| ) | ||
|
|
||
| func TestEmbeddingsTextUniqueConstraint(t *testing.T) { | ||
| dsn := os.Getenv("MEM_TEST_DB") | ||
| if dsn == "" { | ||
| t.Skip("MEM_TEST_DB not set; skipping DB integration test") | ||
| } | ||
| config, err := pgxpool.ParseConfig(dsn) | ||
| if err != nil { | ||
| t.Fatalf("parse MEM_TEST_DB: %v", err) | ||
| } | ||
| if !strings.HasSuffix(config.ConnConfig.Database, "_test") { | ||
| t.Fatalf("refusing to modify non-test database %q", config.ConnConfig.Database) | ||
| } | ||
|
|
||
| ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) | ||
| defer cancel() | ||
|
|
||
| pool, err := pgxpool.NewWithConfig(ctx, config) | ||
| if err != nil { | ||
| t.Fatalf("connect: %v", err) | ||
| } | ||
| defer pool.Close() | ||
|
|
||
| db := &DB{Pool: pool, url: dsn} | ||
| if err := db.Migrate(ctx); err != nil { | ||
| t.Fatalf("migrate: %v", err) | ||
| } | ||
| tx, err := pool.Begin(ctx) | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| defer tx.Rollback(context.Background()) | ||
|
|
||
| var userID, workspaceID, fileID uuid.UUID | ||
| if err := tx.QueryRow(ctx, ` | ||
| INSERT INTO users (email, password_hash) VALUES ($1, 'x') | ||
| RETURNING id | ||
| `, uuid.NewString()+"@example.test").Scan(&userID); err != nil { | ||
| t.Fatalf("seed user: %v", err) | ||
| } | ||
| if err := tx.QueryRow(ctx, ` | ||
| INSERT INTO workspaces (name, resource_owner_user_id) | ||
| VALUES ('unique-ctest-ws', $1) | ||
| ON CONFLICT (resource_owner_user_id) DO UPDATE SET name = EXCLUDED.name | ||
| RETURNING id | ||
| `, userID).Scan(&workspaceID); err != nil { | ||
| t.Fatalf("seed workspace: %v", err) | ||
| } | ||
| if err := tx.QueryRow(ctx, ` | ||
| INSERT INTO files (user_id, name, path, size, sha256, mime, storage_key) | ||
| VALUES ($1, 'unique-ctest.txt', '/unique-ctest.txt', 0, '', 'text/plain', 'test://unique') | ||
| RETURNING id | ||
| `, userID).Scan(&fileID); err != nil { | ||
| t.Fatalf("seed file: %v", err) | ||
| } | ||
| if _, err := tx.Exec(ctx, ` | ||
| INSERT INTO embeddings_text (file_id, chunk_index, chunk_text, provider) | ||
| VALUES ($1, 0, 'chunk zero', 'test') | ||
| `, fileID); err != nil { | ||
| t.Fatalf("first insert: %v", err) | ||
| } | ||
| // Replay the actual migration over populated, pre-constraint data inside | ||
| // this rollback-only transaction. A fresh-schema migrate alone misses this. | ||
| if _, err := tx.Exec(ctx, `ALTER TABLE embeddings_text DROP CONSTRAINT uq_embeddings_text_file_chunk`); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if _, err := tx.Exec(ctx, `INSERT INTO embeddings_text (file_id, chunk_index, chunk_text) | ||
| VALUES ($1, 0, 'legacy duplicate')`, fileID); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| migration, err := migrationsFS.ReadFile("migrations/0028_data_plane_hygiene.sql") | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| up := strings.SplitN(string(migration), "-- +goose Down", 2)[0] | ||
| if _, err := tx.Exec(ctx, up); err != nil { | ||
| t.Fatalf("migrate populated table: %v", err) | ||
| } | ||
| var survivors int | ||
| if err := tx.QueryRow(ctx, `SELECT count(*) FROM embeddings_text WHERE file_id=$1`, fileID).Scan(&survivors); err != nil || survivors != 1 { | ||
| t.Fatalf("deduplicated rows = %d, err=%v", survivors, err) | ||
| } | ||
|
|
||
| _, err = tx.Exec(ctx, ` | ||
| INSERT INTO embeddings_text (file_id, chunk_index, chunk_text, provider) | ||
| VALUES ($1, 0, 'duplicate chunk', 'test') | ||
| `, fileID) | ||
| if err == nil { | ||
| t.Fatal("expected duplicate (file_id, chunk_index) to be rejected") | ||
| } | ||
| var pgErr *pgconn.PgError | ||
| if !errors.As(err, &pgErr) || pgErr.Code != "23505" { | ||
| t.Fatalf("expected unique violation 23505, got %v", err) | ||
| } | ||
| } |
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.