You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
ldo-compact-fork/packages/subscribable-dataset/test/TransactionalDataset.test.ts

361 lines
12 KiB

import { quad, namedNode, literal } from "@ldo/rdf-utils";
import type {
Dataset,
DatasetCoreFactory,
Quad,
DatasetCore,
} from "@rdfjs/types";
import type { ISubscribableDataset } from "../src/index.js";
import { ExtendedDatasetFactory, createDataset } from "@ldo/dataset";
import {
TransactionDataset,
createSubscribableDataset,
createTransactionDatasetFactory,
} from "../src/index.js";
import datasetCoreFactory from "@rdfjs/dataset";
describe("TransactionDataset", () => {
let parentDataset: ISubscribableDataset<Quad>;
let transactionalDataset: TransactionDataset<Quad>;
const tomTypeQuad = quad(
namedNode("http://example.org/cartoons#Tom"),
namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"),
namedNode("http://example.org/cartoons#Cat"),
);
const tomNameQuad = quad(
namedNode("http://example.org/cartoons#Tom"),
namedNode("http://example.org/cartoons#name"),
literal("Tom"),
);
const lickyNameQuad = quad(
namedNode("http://example.org/cartoons#Licky"),
namedNode("http://example.org/cartoons#name"),
literal("Licky"),
);
const lickyTypeQuad = quad(
namedNode("http://example.org/cartoons#Licky"),
namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"),
namedNode("http://example.org/cartoons#Cat"),
);
const datasetFactory: DatasetCoreFactory = {
dataset: (quads?: Dataset | Quad[]): DatasetCore => {
return datasetCoreFactory.dataset(quads ? Array.from(quads) : undefined);
},
};
const extendedDatasetFactory = new ExtendedDatasetFactory(datasetFactory);
const initializeWithExtendedDatasetParent = (quads?: Quad[]) => {
parentDataset = createSubscribableDataset(
quads || [tomTypeQuad, tomNameQuad],
);
transactionalDataset = new TransactionDataset(
parentDataset,
extendedDatasetFactory,
createTransactionDatasetFactory(),
);
};
beforeEach(() => {
initializeWithExtendedDatasetParent();
});
it("Adds without adding to the parent", () => {
const addedQuad = lickyNameQuad;
transactionalDataset.add(addedQuad);
expect(transactionalDataset.has(addedQuad)).toBe(true);
expect(parentDataset.has(addedQuad)).toBe(false);
});
it("Deletes without deleting from the parent", () => {
const deletedQuad = tomTypeQuad;
transactionalDataset.delete(deletedQuad);
expect(transactionalDataset.has(deletedQuad)).toBe(false);
expect(parentDataset.has(deletedQuad)).toBe(true);
});
it("Adds a quad already in the dataset with no duplicates", () => {
const addedQuad = tomTypeQuad;
transactionalDataset.add(addedQuad);
const arr = transactionalDataset.toArray();
expect(arr.length).toBe(2);
expect(arr.some((curQuad) => curQuad.equals(tomNameQuad))).toBe(true);
expect(arr.some((curQuad) => curQuad.equals(tomTypeQuad))).toBe(true);
});
it("Adds multiple quads", () => {
transactionalDataset.add(lickyNameQuad);
transactionalDataset.add(lickyTypeQuad);
expect(transactionalDataset.size).toBe(4);
expect(transactionalDataset.has(lickyNameQuad)).toBe(true);
expect(transactionalDataset.has(lickyTypeQuad)).toBe(true);
});
it("Removes a quad that's not in the dataset and nothing happens", () => {
const removedQuad = lickyNameQuad;
transactionalDataset.delete(removedQuad);
const arr = transactionalDataset.toArray();
expect(arr.length).toBe(2);
expect(arr.some((curQuad) => curQuad.equals(tomNameQuad))).toBe(true);
expect(arr.some((curQuad) => curQuad.equals(tomTypeQuad))).toBe(true);
});
it("Removes multiple quads", () => {
transactionalDataset.delete(tomNameQuad);
transactionalDataset.delete(tomTypeQuad);
expect(transactionalDataset.size).toBe(0);
});
it("Removes then adds a quad and they cancel out", () => {
const removedQuad = tomTypeQuad;
transactionalDataset.delete(removedQuad);
transactionalDataset.add(removedQuad);
const arr = transactionalDataset.toArray();
expect(arr.length).toBe(2);
expect(arr.some((curQuad) => curQuad.equals(tomNameQuad))).toBe(true);
expect(arr.some((curQuad) => curQuad.equals(tomTypeQuad))).toBe(true);
});
it("Adds then removes a quad and they cancel out", () => {
const addedQuad = lickyNameQuad;
transactionalDataset.add(addedQuad);
transactionalDataset.delete(addedQuad);
const arr = transactionalDataset.toArray();
expect(arr.length).toBe(2);
expect(arr.some((curQuad) => curQuad.equals(tomNameQuad))).toBe(true);
expect(arr.some((curQuad) => curQuad.equals(tomTypeQuad))).toBe(true);
});
it("Removes then adds a quad and the quad is still added", () => {
const addedQuad = lickyNameQuad;
transactionalDataset.delete(addedQuad);
transactionalDataset.add(addedQuad);
const arr = transactionalDataset.toArray();
expect(arr.length).toBe(3);
expect(arr.some((curQuad) => curQuad.equals(tomNameQuad))).toBe(true);
expect(arr.some((curQuad) => curQuad.equals(tomTypeQuad))).toBe(true);
expect(arr.some((curQuad) => curQuad.equals(lickyNameQuad))).toBe(true);
});
it("Commits added changes", () => {
transactionalDataset.add(lickyNameQuad);
transactionalDataset.commit();
expect(parentDataset.has(lickyNameQuad)).toBe(true);
});
it("Commits removed changes", () => {
transactionalDataset.delete(tomTypeQuad);
transactionalDataset.commit();
expect(parentDataset.has(tomTypeQuad)).toBe(false);
});
it("Commits added and removed changes", () => {
transactionalDataset.add(lickyNameQuad);
transactionalDataset.delete(tomTypeQuad);
transactionalDataset.commit();
expect(parentDataset.has(lickyNameQuad)).toBe(true);
expect(parentDataset.has(tomTypeQuad)).toBe(false);
});
it("Errors whenever you try to add or remove triples after committing.", () => {
transactionalDataset.commit();
expect(() => transactionalDataset.add(lickyNameQuad)).toThrow(
"Transaction has already committed",
);
expect(() => transactionalDataset.delete(tomTypeQuad)).toThrow(
"Transaction has already committed",
);
expect(() => transactionalDataset.addAll([lickyNameQuad])).toThrow(
"Transaction has already committed",
);
expect(() =>
transactionalDataset.deleteMatches(
tomTypeQuad.subject,
tomTypeQuad.predicate,
tomTypeQuad.object,
),
).toThrow("Transaction has already committed");
// Nothing Matches it should still error
expect(() =>
transactionalDataset.deleteMatches(
lickyNameQuad.subject,
lickyNameQuad.predicate,
lickyNameQuad.object,
),
).toThrow("Transaction has already committed");
});
it("Does not rollback unless it has already committed", () => {
expect(() => transactionalDataset.rollback()).toThrow(
"Cannot rollback. Transaction has not yet been committed",
);
});
it("Rolls back the dataset", () => {
transactionalDataset.add(lickyNameQuad);
transactionalDataset.delete(tomTypeQuad);
transactionalDataset.commit();
transactionalDataset.rollback();
expect(parentDataset.size).toBe(2);
expect(parentDataset.has(tomTypeQuad)).toBe(true);
expect(parentDataset.has(tomNameQuad)).toBe(true);
});
it("Rolls back the dataset when redundant changes are made", () => {
transactionalDataset.add(tomTypeQuad);
transactionalDataset.delete(lickyNameQuad);
transactionalDataset.commit();
transactionalDataset.rollback();
expect(parentDataset.size).toBe(2);
expect(parentDataset.has(tomTypeQuad)).toBe(true);
expect(parentDataset.has(tomNameQuad)).toBe(true);
});
it("Counts the correct size given added quads", () => {
transactionalDataset.add(lickyNameQuad);
expect(transactionalDataset.size).toBe(3);
});
it("Counts the correct size given added redundant quads", () => {
transactionalDataset.add(tomTypeQuad);
expect(transactionalDataset.size).toBe(2);
});
it("Counts the correct size given deleted quads", () => {
transactionalDataset.delete(tomTypeQuad);
expect(transactionalDataset.size).toBe(1);
});
it("Counts the correct size given deleted redundant quads", () => {
transactionalDataset.delete(lickyNameQuad);
expect(transactionalDataset.size).toBe(2);
});
it("Counts the correct size given added and deleted quads", () => {
transactionalDataset.add(lickyNameQuad);
transactionalDataset.delete(tomTypeQuad);
expect(transactionalDataset.size).toBe(2);
});
it("Adds all", () => {
transactionalDataset.addAll([lickyNameQuad, lickyTypeQuad]);
expect(transactionalDataset.has(lickyTypeQuad)).toBe(true);
expect(transactionalDataset.has(lickyNameQuad)).toBe(true);
});
it("Makes changes in bulk", () => {
transactionalDataset.bulk({
added: extendedDatasetFactory.dataset([lickyNameQuad]),
removed: extendedDatasetFactory.dataset([tomTypeQuad]),
});
expect(transactionalDataset.size).toBe(2);
expect(transactionalDataset.has(tomTypeQuad)).toBe(false);
expect(transactionalDataset.has(lickyNameQuad)).toBe(true);
});
it("Deletes matching data", () => {
transactionalDataset.deleteMatches(
tomTypeQuad.subject,
undefined,
undefined,
);
expect(transactionalDataset.size).toBe(0);
});
it("Matches an unmodified dataset", () => {
const matchingDataset = transactionalDataset.match(
tomTypeQuad.subject,
undefined,
undefined,
);
expect(matchingDataset.size).toBe(2);
expect(matchingDataset.has(tomTypeQuad)).toBe(true);
expect(matchingDataset.has(tomNameQuad)).toBe(true);
});
it("Matches a dataset with a removed quad", () => {
initializeWithExtendedDatasetParent([
tomNameQuad,
tomTypeQuad,
lickyNameQuad,
lickyTypeQuad,
]);
transactionalDataset.delete(tomNameQuad);
const matchingDataset = transactionalDataset.match(
tomTypeQuad.subject,
undefined,
undefined,
);
expect(matchingDataset.size).toBe(1);
expect(matchingDataset.has(tomTypeQuad)).toBe(true);
expect(matchingDataset.has(tomNameQuad)).toBe(false);
});
it("matches a dataset with an added quad", () => {
transactionalDataset.add(lickyNameQuad);
const matchingDataset = transactionalDataset.match(
undefined,
tomNameQuad.predicate,
undefined,
);
expect(matchingDataset.size).toBe(2);
expect(matchingDataset.has(lickyNameQuad)).toBe(true);
expect(matchingDataset.has(tomNameQuad)).toBe(true);
});
it("Uses bulk update on commit when the parent dataset is bulk updatable", () => {
// Disable for tests
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const mockParent: ISubscribableDataset<Quad> = {
bulk: jest.fn(),
has: (curQuad) => parentDataset.has(curQuad),
[Symbol.iterator]: () => parentDataset[Symbol.iterator](),
};
transactionalDataset = new TransactionDataset<Quad>(
mockParent,
extendedDatasetFactory,
createTransactionDatasetFactory(),
);
transactionalDataset.add(lickyNameQuad);
transactionalDataset.delete(tomTypeQuad);
transactionalDataset.commit();
expect(mockParent.bulk).toHaveBeenCalled();
});
it("Uses bulk update on commit when the parent dataset is not bulk updatable", () => {
// Disable for tests
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const mockParent: Dataset<Quad> = createDataset([tomTypeQuad]);
transactionalDataset = new TransactionDataset<Quad>(
mockParent,
extendedDatasetFactory,
createTransactionDatasetFactory(),
);
transactionalDataset.add(lickyNameQuad);
transactionalDataset.delete(tomTypeQuad);
transactionalDataset.commit();
expect(mockParent.has(lickyNameQuad)).toBe(true);
expect(mockParent.has(tomTypeQuad)).toBe(false);
});
it("Returns a transactional dataset", () => {
expect(
transactionalDataset.startTransaction() instanceof TransactionDataset,
).toBe(true);
});
it("Checks if it has a quad when it wasn't added to the transaction but was in the original dataset", () => {
expect(transactionalDataset.has(tomTypeQuad)).toBe(true);
});
it("returns the dataset changes", () => {
const addedQuad = lickyNameQuad;
transactionalDataset.add(addedQuad);
const datasetChanges = transactionalDataset.getChanges();
expect(datasetChanges.added?.size).toBe(1);
expect(datasetChanges.removed).toBe(undefined);
});
});