From 288fd2ad49876cf572921c22d7d58dc11ac383a5 Mon Sep 17 00:00:00 2001 From: Agus Putra Dana Date: Tue, 4 Feb 2025 15:55:26 +0800 Subject: [PATCH] Add alternative tests for postgres adapter --- tests/adapters/postgresDB/mocks/data.sql | 31 ++- tests/adapters/postgresDB/mocks/schema.sql | 42 +++- tests/mocks/schema.ts | 150 ++++++++++++ tests/postgres.sh | 6 +- tests/unit/queries/query.ts | 268 +++++++++++++++++++++ 5 files changed, 492 insertions(+), 5 deletions(-) diff --git a/tests/adapters/postgresDB/mocks/data.sql b/tests/adapters/postgresDB/mocks/data.sql index e1c638d..e464d8d 100644 --- a/tests/adapters/postgresDB/mocks/data.sql +++ b/tests/adapters/postgresDB/mocks/data.sql @@ -119,4 +119,33 @@ VALUES ('hook2', 'b'), ('hook3', 'c'), ('hook4', 'a'), -('hook5', 'b'); \ No newline at end of file +('hook5', 'b'); + +-- Hotel +------------------------------------------------------------------------------- + +INSERT INTO "Hotel" ("id", "name", "location") VALUES +('h1', 'Grand Palace Hotel', 'New York, USA'), +('h2', 'Ocean View Resort', 'Miami, USA'), +('h3', 'Mountain Lodge', 'Denver, USA'); + +INSERT INTO "Room" ("id", "hotelId", "pricePerNight", "isAvailable") VALUES +('r1', 'h1', 150.00, TRUE), +('r2', 'h1', 200.00, FALSE), +('r3', 'h2', 180.00, TRUE), +('r4', 'h2', 220.00, TRUE), +('r5', 'h3', 100.00, FALSE); + +INSERT INTO "Guest" ("id", "name", "email", "phone") VALUES +('g1', 'John Doe', 'john.doe@example.com', '123-456-7890'), +('g2', 'Jane Smith', 'jane.smith@example.com', '987-654-3210'), +('g3', 'Robert Brown', 'robert.brown@example.com', NULL); + +INSERT INTO "Booking" ("id", "roomId", "guestId", "checkIn", "checkOut", "status", "totalCost") VALUES +('b1', 'r2', 'g1', '2024-02-01', '2024-02-05', 'checked-in', 800.00), +('b2', 'r5', 'g2', '2024-03-10', '2024-03-15', 'reserved', 500.00), +('b3', 'r3', 'g3', '2024-01-20', '2024-01-25', 'checked-out', 900.00); + +INSERT INTO "Payment" ("id", "bookingId", "amountPaid", "paymentDate") VALUES +('p1', 'b1', 800.00, '2024-02-01 14:30:00'), +('p2', 'b3', 900.00, '2024-01-20 10:00:00'); diff --git a/tests/adapters/postgresDB/mocks/schema.sql b/tests/adapters/postgresDB/mocks/schema.sql index 94ee04b..ba9cbfe 100644 --- a/tests/adapters/postgresDB/mocks/schema.sql +++ b/tests/adapters/postgresDB/mocks/schema.sql @@ -205,4 +205,44 @@ CREATE TABLE IF NOT EXISTS "Employee" ( "id" TEXT PRIMARY KEY, "name" TEXT NOT NULL, "companyId" TEXT REFERENCES "Company"("id") -); \ No newline at end of file +); + +-- Hotel +------------------------------------------------------------------------------- + +CREATE TABLE IF NOT EXISTS "Hotel" ( + "id" TEXT PRIMARY KEY, + "name" TEXT NOT NULL, + "location" VARCHAR(255) +); + +CREATE TABLE IF NOT EXISTS "Room" ( + "id" TEXT PRIMARY KEY, + "hotelId" TEXT REFERENCES "Hotel"("id"), + "pricePerNight" DECIMAL(10,2) NOT NULL, + "isAvailable" BOOLEAN DEFAULT TRUE +); + +CREATE TABLE IF NOT EXISTS "Guest" ( + "id" TEXT PRIMARY KEY, + "name" VARCHAR(255) NOT NULL, + "email" VARCHAR(255) UNIQUE, + "phone" VARCHAR(20) +); + +CREATE TABLE IF NOT EXISTS "Booking" ( + "id" TEXT PRIMARY KEY, + "roomId" TEXT REFERENCES "Room"("id"), + "guestId" TEXT REFERENCES "Guest"("id"), + "checkIn" DATE NOT NULL, + "checkOut" DATE NOT NULL, + "status" VARCHAR(20) NOT NULL DEFAULT 'reserved', -- 'reserved', 'checked-in', 'checked-out', 'canceled' + "totalCost" DECIMAL(10,2) NOT NULL +); + +CREATE TABLE IF NOT EXISTS "Payment" ( + "id" TEXT PRIMARY KEY, + "bookingId" TEXT REFERENCES "Booking"("id"), + "amountPaid" DECIMAL(10,2) NOT NULL, + "paymentDate" TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); diff --git a/tests/mocks/schema.ts b/tests/mocks/schema.ts index 38fea69..b192d90 100644 --- a/tests/mocks/schema.ts +++ b/tests/mocks/schema.ts @@ -1143,5 +1143,155 @@ export const schema: BormSchema = { company: { cardinality: 'ONE', dbConfig: { db: 'postgres', fields: [{ path: 'companyId', type: 'TEXT' }] } }, }, }, + 'Hotel': { + idFields: ['id'], + defaultDBConnector: { id: 'default', path: 'Hotel' }, + dataFields: [ + { ...id }, + { ...name, validations: { required: true } }, + { + path: 'location', + contentType: 'TEXT', + }, + ], + linkFields: [ + { + path: 'rooms', + cardinality: 'MANY', + relation: 'Room', + plays: 'hotel', + target: 'relation', + }, + ], + }, + 'Room': { + idFields: ['id'], + defaultDBConnector: { id: 'default', path: 'Room' }, + dataFields: [ + { ...id }, + { + path: 'pricePerNight', + contentType: 'NUMBER_DECIMAL', + validations: { required: true }, + }, + { + path: 'isAvailable', + contentType: 'BOOLEAN', + validations: { required: true }, + }, + ], + linkFields: [ + { + path: 'bookings', + cardinality: 'MANY', + relation: 'Booking', + plays: 'room', + target: 'relation', + }, + { + path: 'guests', + cardinality: 'MANY', + relation: 'Booking', + plays: 'room', + target: 'role', + }, + ], + roles: { + hotel: { cardinality: 'ONE', dbConfig: { db: 'postgres', fields: [{ path: 'hotelId', type: 'TEXT' }] } }, + }, + }, + 'Guest': { + idFields: ['id'], + defaultDBConnector: { id: 'default', path: 'Guest' }, + dataFields: [ + { ...id }, + { ...name, validations: { required: true } }, + { + path: 'email', + contentType: 'TEXT', + validations: { required: true }, + }, + { + path: 'phone', + contentType: 'TEXT', + }, + ], + linkFields: [ + { + path: 'bookings', + cardinality: 'MANY', + relation: 'Booking', + plays: 'guest', + target: 'relation', + }, + { + path: 'rooms', + cardinality: 'MANY', + relation: 'Booking', + plays: 'guest', + target: 'role', + }, + ], + }, + 'Booking': { + idFields: ['id'], + defaultDBConnector: { id: 'default', path: 'Booking' }, + dataFields: [ + { ...id }, + { + path: 'checkIn', + contentType: 'DATE', + validations: { required: true }, + }, + { + path: 'checkOut', + contentType: 'DATE', + validations: { required: true }, + }, + { + path: 'status', + contentType: 'TEXT', + validations: { required: true, enum: ['reserved', 'checked-in', 'checked-out', 'canceled'] }, + }, + { + path: 'totalCost', + contentType: 'NUMBER_DECIMAL', + validations: { required: true }, + }, + ], + linkFields: [ + { + path: 'payments', + cardinality: 'MANY', + relation: 'Payment', + plays: 'booking', + target: 'relation', + }, + ], + roles: { + room: { cardinality: 'ONE', dbConfig: { db: 'postgres', fields: [{ path: 'roomId', type: 'TEXT' }] } }, + guest: { cardinality: 'ONE', dbConfig: { db: 'postgres', fields: [{ path: 'guestId', type: 'TEXT' }] } }, + }, + }, + 'Payment': { + idFields: ['id'], + defaultDBConnector: { id: 'default', path: 'Payment' }, + dataFields: [ + { ...id }, + { + path: 'amountPaid', + contentType: 'NUMBER_DECIMAL', + validations: { required: true }, + }, + { + path: 'paymentDate', + contentType: 'DATE', + validations: { required: true }, + }, + ], + roles: { + booking: { cardinality: 'ONE', dbConfig: { db: 'postgres', fields: [{ path: 'bookingId', type: 'TEXT' }] } }, + }, + }, }, } as const; diff --git a/tests/postgres.sh b/tests/postgres.sh index 23402d6..853bfb9 100755 --- a/tests/postgres.sh +++ b/tests/postgres.sh @@ -46,7 +46,7 @@ docker run \ -d \ postgres -sleep 2 +sleep 1 # Create the tables docker run \ @@ -58,7 +58,7 @@ docker run \ postgres \ psql -h localhost -p 5432 -U ${USER} -d ${DB} -f /tests/schema.sql -sleep 2 +sleep 1 # Insert data docker run \ @@ -70,7 +70,7 @@ docker run \ postgres \ psql -h localhost -p 5432 -U ${USER} -d ${DB} -f /tests/data.sql -sleep 2 +sleep 1 # Run tests and capture output if CONTAINER_NAME=${CONTAINER_NAME} npx vitest run "${VITEST_ARGS[@]}"; then diff --git a/tests/unit/queries/query.ts b/tests/unit/queries/query.ts index 9221173..746286e 100644 --- a/tests/unit/queries/query.ts +++ b/tests/unit/queries/query.ts @@ -625,6 +625,23 @@ export const testQuery = createTest('Query', (ctx) => { expect(deepSort(resWithoutMetadata, 'id')).toEqual(deepRemoveMetaData(expectedRes)); }); + it('TODO{ST}:r4.alt[relation, nested, direct] - nested relation direct on relation', async () => { + // Surreal & TypeDB: The schema and the data are not added yet + const query = { + $relation: 'Room', + $fields: ['id', { $path: 'bookings', $fields: ['id'] }, { $path: 'guests', $fields: ['id'] }], + }; + const expected = [ + { id: 'r1' }, + { id: 'r2', bookings: [{ id: 'b1' }], guests: [{ id: 'g1' }] }, + { id: 'r3', bookings: [{ id: 'b3' }], guests: [{ id: 'g3' }] }, + { id: 'r4' }, + { id: 'r5', bookings: [{ id: 'b2' }], guests: [{ id: 'g2' }] }, + ]; + const res = await ctx.query(query, { noMetadata: true }); + expect(deepSort(res, 'id')).toEqual(expected); + }); + it('TODO{P}:r5[relation nested] - that has both role, and linkfield pointing to same role', async () => { // Postgres: // - Role field UserTagGroup.tagId cannot reference multiple records. @@ -668,6 +685,23 @@ export const testQuery = createTest('Query', (ctx) => { expect(deepSort(resWithoutMetadata, 'id')).toEqual(deepRemoveMetaData(expectedRes)); }); + it('TODO{ST}:r5.alt[relation nested] - that has both role, and linkfield pointing to same role', async () => { + // Surreal & TypeDB: The schema and the data are not added yet + const query = { + $relation: 'Room', + $fields: ['id', 'bookings', 'guests'], + }; + const expected = [ + { id: 'r1' }, + { id: 'r2', bookings: ['b1'], guests: ['g1'] }, + { id: 'r3', bookings: ['b3'], guests: ['g3'] }, + { id: 'r4' }, + { id: 'r5', bookings: ['b2'], guests: ['g2'] }, + ]; + const res = await ctx.query(query, { noMetadata: true }); + expect(deepSort(res, 'id')).toEqual(expected); + }); + it('TODO{P}:r6[relation nested] - relation connected to relation and a tunneled relation', async () => { // Postgres: // - Role field UserTagGroup.tagId and UserTag.tagId cannot reference multiple records. @@ -722,6 +756,20 @@ export const testQuery = createTest('Query', (ctx) => { expect(deepSort(resWithoutMetadata, 'id')).toEqual(deepRemoveMetaData(expectedRes)); }); + it('TODO{ST}:r6.alt[relation nested] - relation connected to relation and a tunneled relation', async () => { + // Surreal & TypeDB: The schema and the data are not added yet + const query = { $relation: 'Room' }; + const expected = [ + { id: 'r1', pricePerNight: '150.00', isAvailable: true, hotel: 'h1' }, + { id: 'r2', pricePerNight: '200.00', isAvailable: false, hotel: 'h1', bookings: ['b1'], guests: ['g1'] }, + { id: 'r3', pricePerNight: '180.00', isAvailable: true, hotel: 'h2', bookings: ['b3'], guests: ['g3'] }, + { id: 'r4', pricePerNight: '220.00', isAvailable: true, hotel: 'h2' }, + { id: 'r5', pricePerNight: '100.00', isAvailable: false, hotel: 'h3', bookings: ['b2'], guests: ['g2'] }, + ]; + const res = await ctx.query(query, { noMetadata: true }); + expect(deepSort(res, 'id')).toEqual(expected); + }); + it('TODO{P}:r7[relation, nested, direct] - nested on nested', async () => { // Postgres: // - Inherited entity/relation is not supported (God and SuperUser are inherited from Used). @@ -859,6 +907,27 @@ export const testQuery = createTest('Query', (ctx) => { expect(deepSort(resWithoutMetadata, 'id')).toEqual(deepRemoveMetaData(expectedRes)); }); + it('TODO{ST}:r7.alt[relation, nested, direct] - nested on nested', async () => { + // Surreal & TypeDB: The schema and the data are not added yet + const query = { + $relation: 'Room', + $fields: [ + 'id', + { $path: 'bookings', $fields: ['id', 'guest'] }, + { $path: 'guests', $fields: ['id', 'bookings'] }, + ], + }; + const expected = [ + { id: 'r1' }, + { id: 'r2', bookings: [{ id: 'b1', guest: 'g1' }], guests: [{ id: 'g1', bookings: ['b1'] }] }, + { id: 'r3', bookings: [{ id: 'b3', guest: 'g3' }], guests: [{ id: 'g3', bookings: ['b3'] }] }, + { id: 'r4' }, + { id: 'r5', bookings: [{ id: 'b2', guest: 'g2' }], guests: [{ id: 'g2', bookings: ['b2'] }] }, + ]; + const res = await ctx.query(query, { noMetadata: true }); + expect(deepSort(res, 'id')).toEqual(expected); + }); + it('TODO{P}:r8[relation, nested, deep] - deep nested', async () => { // Postgres: // - Inherited entity/relation is not supported (God and SuperUser are inherited from Used). @@ -924,6 +993,25 @@ export const testQuery = createTest('Query', (ctx) => { expect(deepSort(resWithoutMetadata, 'id')).toEqual(deepRemoveMetaData(expectedRes)); }); + it('TODO{ST}:r8.alt[relation, nested, deep] - deep nested', async () => { + // Surreal & TypeDB: The schema and the data are not added yet + const query = { + $relation: 'Hotel', + $id: 'h1', + $fields: [ + 'id', + { + $path: 'rooms', + $id: 'r2', + $fields: ['id', { $path: 'bookings', $fields: ['id', { $path: 'guest', $fields: ['id', 'bookings'] }] }], + }, + ], + }; + const expected = { id: 'h1', rooms: { id: 'r2', bookings: [{ id: 'b1', guest: { id: 'g1', bookings: ['b1'] } }] } }; + const res = await ctx.query(query, { noMetadata: true }); + expect(deepSort(res, 'id')).toEqual(expected); + }); + it('TODO{P}:r9[relation, nested, ids]', async () => { // Postgres: Role field UserTagGroup.tagId cannot reference multiple records. const query = { @@ -944,6 +1032,18 @@ export const testQuery = createTest('Query', (ctx) => { }); }); + it('TODO{ST}:r9.alt[relation, nested, ids]', async () => { + // Surreal & TypeDB: The schema and the data are not added yet + const query = { + $relation: 'Hotel', + $id: 'h1', + $fields: ['id', 'rooms'], + }; + const expected = { id: 'h1', rooms: ['r1', 'r2'] }; + const res = await ctx.query(query, { noMetadata: true }); + expect(deepSort(res, 'id')).toEqual(expected); + }); + it('ef1[entity] - $id single', async () => { const wrongRes = await ctx.query({ $entity: 'User', $id: uuidv4() }); expect(wrongRes).toEqual(null); @@ -997,6 +1097,17 @@ export const testQuery = createTest('Query', (ctx) => { ]); }); + it('TODO{ST}:ef3.alt[entity] - $fields single', async () => { + // Surreal & TypeDB: The schema and the data are not added yet + const query = { + $relation: 'Hotel', + $fields: ['id'], + }; + const expected = [{ id: 'h1' }, { id: 'h2' }, { id: 'h3' }]; + const res = await ctx.query(query, { noMetadata: true }); + expect(deepSort(res, 'id')).toEqual(expected); + }); + it('ef4[entity] - $fields multiple', async () => { const res = await ctx.query( { @@ -1151,6 +1262,24 @@ export const testQuery = createTest('Query', (ctx) => { }); }); + it('TODO{ST}:n2.alt[nested] First level all fields', async () => { + // Surreal & TypeDB: The schema and the data are not added yet + const query = { + $relation: 'Hotel', + $id: 'h1', + $fields: ['id', { $path: 'rooms' }], + }; + const expected = { + id: 'h1', + rooms: [ + { id: 'r1', pricePerNight: '150.00', isAvailable: true, hotel: 'h1' }, + { id: 'r2', pricePerNight: '200.00', isAvailable: false, hotel: 'h1', bookings: ['b1'], guests: ['g1'] }, + ], + }; + const res = await ctx.query(query, { noMetadata: true }); + expect(deepSort(res, 'id')).toEqual(expected); + }); + it('n3[nested, $fields] First level filtered fields', async () => { const res = await ctx.query( { @@ -1272,6 +1401,21 @@ export const testQuery = createTest('Query', (ctx) => { }); }); + it('TODO{ST}:nf1.alt[nested, $filters] Local filter on nested, single id', async () => { + // Surreal & TypeDB: The schema and the data are not added yet + const query = { + $relation: 'Hotel', + $id: 'h1', + $fields: ['id', { $path: 'rooms', $filter: { isAvailable: true } }], + }; + const expected = { + id: 'h1', + rooms: [{ id: 'r1', pricePerNight: '150.00', isAvailable: true, hotel: 'h1' }], + }; + const res = await ctx.query(query, { noMetadata: true }); + expect(deepSort(res, 'id')).toEqual(expected); + }); + it('nf2[nested, $filters] Local filter on nested, by field, multiple sources, some are empty', async () => { const res = await ctx.query( { @@ -1791,6 +1935,38 @@ export const testQuery = createTest('Query', (ctx) => { expect(deepSort(resWithoutMetadata, 'id')).toEqual(deepRemoveMetaData(expectedRes)); }); + it('TODO{ST}:xf2.alt[excludedFields, deep] - deep nested', async () => { + // Surreal & TypeDB: The schema and the data are not added yet + const query = { + $relation: 'Hotel', + $id: 'h1', + $fields: [ + 'id', + { $path: 'rooms', $id: 'r2', $fields: ['id', { $path: 'bookings', $excludedFields: ['roomId'] }] }, + ], + }; + const expected = { + id: 'h1', + rooms: { + id: 'r2', + bookings: [ + { + id: 'b1', + checkIn: '2024-01-31T16:00:00.000Z', + checkOut: '2024-02-04T16:00:00.000Z', + status: 'checked-in', + totalCost: '800.00', + room: 'r2', + guest: 'g1', + payments: ['p1'], + }, + ], + }, + }; + const res = await ctx.query(query, { noMetadata: true }); + expect(deepSort(JSON.parse(JSON.stringify(res)), 'id')).toEqual(expected); + }); + it('TODO{P}:xf3[excludedFields, deep] - Exclude virtual field', async () => { // Postgres: Computed data field (freeForAll) is not supported const query = { @@ -2177,6 +2353,35 @@ export const testQuery = createTest('Query', (ctx) => { expect(deepSort(res, 'id')).toEqual(expectedRes); }); + it('TODO{ST}:a1.alt[$as] - as for attributes and roles and links', async () => { + // Surreal & TypeDB: The schema and the data are not added yet + const query = { + $relation: 'Booking', + $id: 'b1', + $fields: [ + 'id', + { $path: 'status', $as: 'currentStatus' }, + { $path: 'payments', $as: 'allPayments' }, + { $path: 'guest', $as: 'currentGuest' }, + ], + }; + const expected = { + id: 'b1', + currentStatus: 'checked-in', + currentGuest: { + id: 'g1', + name: 'John Doe', + email: 'john.doe@example.com', + phone: '123-456-7890', + bookings: ['b1'], + rooms: ['r2'], + }, + allPayments: [{ id: 'p1', amountPaid: '800.00', paymentDate: '2024-02-01T06:30:00.000Z', booking: 'b1' }], + }; + const res = await ctx.query(query, { noMetadata: true }); + expect(deepSort(JSON.parse(JSON.stringify(res)), 'id')).toEqual(expected); + }); + it('bq1[batched query] - as for attributes and roles and links', async () => { const expectedRes = [ { @@ -2696,6 +2901,39 @@ export const testQuery = createTest('Query', (ctx) => { ]); }); + it('TODO{ST}:dn1.alt[deep nested] ridiculously deep nested query', async () => { + // Surreal & TypeDB: The schema and the data are not added yet + const query = { + $relation: 'Hotel', + $fields: [ + 'id', + { + $path: 'rooms', + $fields: [ + 'id', + { + $path: 'bookings', + $fields: [ + 'id', + { + $path: 'payments', + $fields: ['id'], + }, + ], + }, + ], + }, + ], + }; + const expected = [ + { id: 'h1', rooms: [{ id: 'r1' }, { id: 'r2', bookings: [{ id: 'b1', payments: [{ id: 'p1' }] }] }] }, + { id: 'h2', rooms: [{ id: 'r3', bookings: [{ id: 'b3', payments: [{ id: 'p2' }] }] }, { id: 'r4' }] }, + { id: 'h3', rooms: [{ id: 'r5', bookings: [{ id: 'b2' }] }] }, + ]; + const res = await ctx.query(query, { noMetadata: true }); + expect(deepSort(JSON.parse(JSON.stringify(res)), 'id')).toEqual(expected); + }); + it('TODO{PT}:dn2[deep numbers] Big numbers', async () => { const res = await ctx.query( { @@ -2850,4 +3088,34 @@ export const testQuery = createTest('Query', (ctx) => { }, ]); }); + + it('TODO{ST}:fk2[filter, keywords, exists], filter by undefined/null property', async () => { + // Surreal & TypeDB: The schema and the data are not added yet + const query = { + $relation: 'Guest', + $filter: { + phone: { $exists: true }, + }, + }; + const expected = [ + { + id: 'g1', + name: 'John Doe', + email: 'john.doe@example.com', + phone: '123-456-7890', + bookings: ['b1'], + rooms: ['r2'], + }, + { + id: 'g2', + name: 'Jane Smith', + email: 'jane.smith@example.com', + phone: '987-654-3210', + bookings: ['b2'], + rooms: ['r5'], + }, + ]; + const res = await ctx.query(query, { noMetadata: true }); + expect(deepSort(JSON.parse(JSON.stringify(res)), 'id')).toEqual(expected); + }); });