From d3db12db20f9f2474ec4da04bbcad2c4fe39f8b6 Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Fri, 12 Sep 2025 13:19:28 -0700 Subject: [PATCH 1/4] fix(community): Add INSERT support to PrismaVectorStore for ParentDocumentRetriever compatibility (#8833) Previously, PrismaVectorStore only used UPDATE statements when adding vectors, which caused silent failures when used with ParentDocumentRetriever. The retriever creates new child documents that don't exist in the database, so UPDATE statements would succeed but not create any records. Changes: - Add new `addDocumentsWithVectors` method that uses INSERT statements to create records - Modify `addDocuments` to use `addDocumentsWithVectors` instead of `addVectors` - Maintain backward compatibility by keeping the original `addVectors` method unchanged - Add tests to verify the new behavior and ensure no regression This fix ensures PrismaVectorStore works correctly with ParentDocumentRetriever while maintaining compatibility with existing code. Fixes #8833 --- .../src/vectorstores/prisma.ts | 54 ++++++++- .../src/vectorstores/tests/prisma.test.ts | 109 ++++++++++++++++++ 2 files changed, 162 insertions(+), 1 deletion(-) diff --git a/libs/langchain-community/src/vectorstores/prisma.ts b/libs/langchain-community/src/vectorstores/prisma.ts index b462b395ca33..56dc596264f0 100644 --- a/libs/langchain-community/src/vectorstores/prisma.ts +++ b/libs/langchain-community/src/vectorstores/prisma.ts @@ -303,7 +303,7 @@ export class PrismaVectorStore< */ async addDocuments(documents: Document[]) { const texts = documents.map(({ pageContent }) => pageContent); - return this.addVectors( + return this.addDocumentsWithVectors( await this.embeddings.embedDocuments(texts), documents ); @@ -350,6 +350,58 @@ export class PrismaVectorStore< ); } + /** + * Adds documents with their corresponding vectors to the store using INSERT statements. + * This method ensures documents are created if they don't exist, making it compatible + * with ParentDocumentRetriever which creates new child documents. + * @param vectors The vectors to add. + * @param documents The documents associated with the vectors. + * @returns A promise that resolves when the documents have been added. + */ + async addDocumentsWithVectors( + vectors: number[][], + documents: Document[] + ) { + // table name, column name cannot be parametrised + // these fields are thus not escaped by Prisma and can be dangerous if user input is used + const tableNameRaw = this.Prisma.raw(`"${this.tableName}"`); + const vectorColumnRaw = this.Prisma.raw(`"${this.vectorColumnName}"`); + + // Build column names for INSERT statement + const columnNames = this.selectColumns.map((col) => + this.Prisma.raw(`"${col}"`) + ); + const allColumns = [...columnNames, vectorColumnRaw]; + + await this.db.$transaction( + vectors.map((vector, idx) => { + const document = documents[idx]; + const vectorString = `[${vector.join(",")}]`; + + // Build values for each column + const columnValues = this.selectColumns.map((col) => { + if (col === this.contentColumn) { + return document.pageContent; + } + return document.metadata[col]; + }); + + // Add vector as the last value + const allValues = [ + ...columnValues, + this.Prisma.sql`${vectorString}::vector`, + ]; + + return this.db.$executeRaw( + this.Prisma.sql` + INSERT INTO ${tableNameRaw} (${this.Prisma.join(allColumns, ", ")}) + VALUES (${this.Prisma.join(allValues, ", ")}) + ` + ); + }) + ); + } + /** * Performs a similarity search with the specified query. * @param query The query to use for the similarity search. diff --git a/libs/langchain-community/src/vectorstores/tests/prisma.test.ts b/libs/langchain-community/src/vectorstores/tests/prisma.test.ts index de21eead4cee..f475261c9905 100644 --- a/libs/langchain-community/src/vectorstores/tests/prisma.test.ts +++ b/libs/langchain-community/src/vectorstores/tests/prisma.test.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { FakeEmbeddings } from "@langchain/core/utils/testing"; import { jest, test, expect } from "@jest/globals"; +import { Document } from "@langchain/core/documents"; import { PrismaVectorStore } from "../prisma.js"; class Sql { @@ -39,6 +40,7 @@ describe("Prisma", () => { beforeEach(() => { jest.clearAllMocks(); }); + test("passes provided filters with simiaritySearch", async () => { const embeddings = new FakeEmbeddings(); const store = new PrismaVectorStore(new FakeEmbeddings(), { @@ -270,4 +272,111 @@ describe("Prisma", () => { expect(sqlCall).toBeDefined(); }); }); + + test("addDocumentsWithVectors creates new documents with INSERT", async () => { + const embeddings = new FakeEmbeddings(); + const store = new PrismaVectorStore(embeddings, { + db: mockPrismaClient, + prisma: mockPrismaNamespace, + tableName: "test", + vectorColumnName: "vector", + columns: mockColumns, + }); + + const documents = [ + new Document({ + pageContent: "test content 1", + metadata: { id: "doc1", custom: "value1" }, + }), + new Document({ + pageContent: "test content 2", + metadata: { id: "doc2", custom: "value2" }, + }), + ]; + + const vectors = [ + [1, 2, 3], + [4, 5, 6], + ]; + + // Mock the transaction to capture the SQL statements + $transaction.mockImplementation((queries) => { + // Verify that INSERT statements are being used + expect(queries).toHaveLength(2); + return Promise.resolve(); + }); + + await store.addDocumentsWithVectors(vectors, documents); + + expect($transaction).toHaveBeenCalledTimes(1); + expect($executeRaw).toHaveBeenCalledTimes(2); + }); + + test("addDocuments uses addDocumentsWithVectors instead of addVectors", async () => { + const embeddings = new FakeEmbeddings(); + const store = new PrismaVectorStore(embeddings, { + db: mockPrismaClient, + prisma: mockPrismaNamespace, + tableName: "test", + vectorColumnName: "vector", + columns: mockColumns, + }); + + const documents = [ + new Document({ + pageContent: "test content", + metadata: { id: "doc1" }, + }), + ]; + + // Spy on both methods + const addDocumentsWithVectorsSpy = jest + .spyOn(store, "addDocumentsWithVectors") + .mockResolvedValue(); + const addVectorsSpy = jest.spyOn(store, "addVectors").mockResolvedValue(); + + await store.addDocuments(documents); + + // Verify addDocumentsWithVectors was called + expect(addDocumentsWithVectorsSpy).toHaveBeenCalledTimes(1); + // Verify addVectors was NOT called + expect(addVectorsSpy).not.toHaveBeenCalled(); + }); + + test("addVectors still uses UPDATE statements for backward compatibility", async () => { + const embeddings = new FakeEmbeddings(); + const store = new PrismaVectorStore(embeddings, { + db: mockPrismaClient, + prisma: mockPrismaNamespace, + tableName: "test", + vectorColumnName: "vector", + columns: mockColumns, + }); + + const documents = [ + new Document({ + pageContent: "test content", + metadata: { id: "doc1" }, + }), + ]; + + const vectors = [[1, 2, 3]]; + + // Mock sql function to capture the SQL template + let capturedSql = ""; + // @ts-expect-error - we are mocking the sql function + sql.mockImplementation((strings: string[], ...values) => { + capturedSql = strings.join(""); + return { strings, values }; + }); + + $transaction.mockResolvedValue([]); + + await store.addVectors(vectors, documents); + + expect($transaction).toHaveBeenCalledTimes(1); + // Verify UPDATE statement is used + expect(capturedSql).toContain("UPDATE"); + expect(capturedSql).not.toContain("INSERT"); + }); }); From e87e9bf939c23fba9f42abfb97139b8a2a0d6971 Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Fri, 12 Sep 2025 13:21:00 -0700 Subject: [PATCH 2/4] Add INSERT support to PrismaVectorStore Add INSERT support to PrismaVectorStore for compatibility with ParentDocumentRetriever. --- .changeset/small-parrots-lick.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/small-parrots-lick.md diff --git a/.changeset/small-parrots-lick.md b/.changeset/small-parrots-lick.md new file mode 100644 index 000000000000..9b1aba5a1df7 --- /dev/null +++ b/.changeset/small-parrots-lick.md @@ -0,0 +1,5 @@ +--- +"@langchain/community": patch +--- + +fix(community): Add INSERT support to PrismaVectorStore for ParentDocumentRetriever compatibility (#8833) From ca21038e5be5cb893872947b9c7aeda625972dba Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Fri, 12 Dec 2025 10:44:22 -0800 Subject: [PATCH 3/4] put change behind flag --- .../src/vectorstores/prisma.ts | 29 +++++++++++++--- .../src/vectorstores/tests/prisma.test.ts | 34 ++++++++++++++++++- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/libs/langchain-community/src/vectorstores/prisma.ts b/libs/langchain-community/src/vectorstores/prisma.ts index 56dc596264f0..ce3a9e11858b 100644 --- a/libs/langchain-community/src/vectorstores/prisma.ts +++ b/libs/langchain-community/src/vectorstores/prisma.ts @@ -138,6 +138,13 @@ export class PrismaVectorStore< protected columnTypes?: ColumnTypeConfig; + /** + * When true, addDocuments uses INSERT statements to create new records. + * When false (default), addDocuments uses UPDATE statements to update existing records by ID. + * Set to true when using with ParentDocumentRetriever or when documents don't pre-exist in the database. + */ + protected useInsert: boolean; + static IdColumn: typeof IdColumnSymbol = IdColumnSymbol; static ContentColumn: typeof ContentColumnSymbol = ContentColumnSymbol; @@ -160,6 +167,12 @@ export class PrismaVectorStore< columns: TSelectModel; filter?: TFilterModel; columnTypes?: ColumnTypeConfig; + /** + * When true, addDocuments uses INSERT statements to create new records. + * When false (default), addDocuments uses UPDATE statements to update existing records by ID. + * Set to true when using with ParentDocumentRetriever or when documents don't pre-exist in the database. + */ + useInsert?: boolean; } ) { super(embeddings, {}); @@ -182,6 +195,7 @@ export class PrismaVectorStore< this.tableName = config.tableName; this.vectorColumnName = config.vectorColumnName; this.columnTypes = config.columnTypes; + this.useInsert = config.useInsert ?? false; this.selectColumns = entries .map(([key, alias]) => (alias && key) || null) @@ -211,6 +225,7 @@ export class PrismaVectorStore< columns: TColumns; filter?: TFilters; columnTypes?: ColumnTypeConfig; + useInsert?: boolean; } ) { type ModelName = keyof TPrisma["ModelName"] & string; @@ -233,6 +248,7 @@ export class PrismaVectorStore< vectorColumnName: string; columns: TColumns; columnTypes?: ColumnTypeConfig; + useInsert?: boolean; } ) { const docs: Document[] = []; @@ -264,6 +280,7 @@ export class PrismaVectorStore< vectorColumnName: string; columns: TColumns; columnTypes?: ColumnTypeConfig; + useInsert?: boolean; } ) { type ModelName = keyof TPrisma["ModelName"] & string; @@ -303,10 +320,12 @@ export class PrismaVectorStore< */ async addDocuments(documents: Document[]) { const texts = documents.map(({ pageContent }) => pageContent); - return this.addDocumentsWithVectors( - await this.embeddings.embedDocuments(texts), - documents - ); + const vectors = await this.embeddings.embedDocuments(texts); + + if (this.useInsert) { + return this.addDocumentsWithVectors(vectors, documents); + } + return this.addVectors(vectors, documents); } /** @@ -624,6 +643,7 @@ export class PrismaVectorStore< vectorColumnName: string; columns: ModelColumns>; columnTypes?: ColumnTypeConfig; + useInsert?: boolean; } ): Promise { const docs: Document[] = []; @@ -656,6 +676,7 @@ export class PrismaVectorStore< vectorColumnName: string; columns: ModelColumns>; columnTypes?: ColumnTypeConfig; + useInsert?: boolean; } ): Promise { const instance = new PrismaVectorStore(embeddings, dbConfig); diff --git a/libs/langchain-community/src/vectorstores/tests/prisma.test.ts b/libs/langchain-community/src/vectorstores/tests/prisma.test.ts index f475261c9905..943e6ede0d22 100644 --- a/libs/langchain-community/src/vectorstores/tests/prisma.test.ts +++ b/libs/langchain-community/src/vectorstores/tests/prisma.test.ts @@ -312,7 +312,7 @@ describe("Prisma", () => { expect($executeRaw).toHaveBeenCalledTimes(2); }); - test("addDocuments uses addDocumentsWithVectors instead of addVectors", async () => { + test("addDocuments uses addVectors by default (backward compatibility)", async () => { const embeddings = new FakeEmbeddings(); const store = new PrismaVectorStore(embeddings, { db: mockPrismaClient, @@ -337,6 +337,38 @@ describe("Prisma", () => { await store.addDocuments(documents); + // Verify addVectors was called (default behavior) + expect(addVectorsSpy).toHaveBeenCalledTimes(1); + // Verify addDocumentsWithVectors was NOT called + expect(addDocumentsWithVectorsSpy).not.toHaveBeenCalled(); + }); + + test("addDocuments uses addDocumentsWithVectors when useInsert is true", async () => { + const embeddings = new FakeEmbeddings(); + const store = new PrismaVectorStore(embeddings, { + db: mockPrismaClient, + prisma: mockPrismaNamespace, + tableName: "test", + vectorColumnName: "vector", + columns: mockColumns, + useInsert: true, + }); + + const documents = [ + new Document({ + pageContent: "test content", + metadata: { id: "doc1" }, + }), + ]; + + // Spy on both methods + const addDocumentsWithVectorsSpy = jest + .spyOn(store, "addDocumentsWithVectors") + .mockResolvedValue(); + const addVectorsSpy = jest.spyOn(store, "addVectors").mockResolvedValue(); + + await store.addDocuments(documents); + // Verify addDocumentsWithVectors was called expect(addDocumentsWithVectorsSpy).toHaveBeenCalledTimes(1); // Verify addVectors was NOT called From 5543a9bea8d1562182673af3113c1ab9e0c0dd24 Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Fri, 12 Dec 2025 10:45:05 -0800 Subject: [PATCH 4/4] remove changesets after rebase --- .changeset/fifty-plants-drive.md | 5 ----- .changeset/hungry-dolls-turn.md | 6 ------ 2 files changed, 11 deletions(-) delete mode 100644 .changeset/fifty-plants-drive.md delete mode 100644 .changeset/hungry-dolls-turn.md diff --git a/.changeset/fifty-plants-drive.md b/.changeset/fifty-plants-drive.md deleted file mode 100644 index 2d6dc15447c7..000000000000 --- a/.changeset/fifty-plants-drive.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@langchain/mcp-adapters": minor ---- - -feat(mcp-adapters): Add resource management methods and structured content support diff --git a/.changeset/hungry-dolls-turn.md b/.changeset/hungry-dolls-turn.md deleted file mode 100644 index 7b8b34811d11..000000000000 --- a/.changeset/hungry-dolls-turn.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@langchain/core": patch -"@langchain/openai": patch ---- - -fix(langchain): Fix toJsonSchema mutating underlying zod schema