UNPKG

jinaga

Version:

Data management for web and mobile applications.

426 lines (384 loc) 15.6 kB
export interface Label { name: string; type: string; } export interface Role { name: string; predecessorType: string; } export interface PathCondition { type: "path", rolesLeft: Role[], labelRight: string, rolesRight: Role[] } export interface ExistentialCondition { type: "existential", exists: boolean, matches: Match[] } export type Condition = PathCondition | ExistentialCondition; export function isPathCondition(condition: Condition): condition is PathCondition { return condition.type === "path"; } export function isExistentialCondition(condition: Condition): condition is ExistentialCondition { return condition.type === "existential"; } export interface SpecificationProjection { type: "specification", matches: Match[], projection: Projection } export interface FieldProjection { type: "field", label: string, field: string } export interface HashProjection { type: "hash", label: string } export interface FactProjection { type: "fact", label: string } export interface CompositeProjection { type: "composite", components: NamedComponentProjection[] } export type NamedComponentProjection = { name: string } & ComponentProjection; export type ComponentProjection = SpecificationProjection | SingularProjection; export type SingularProjection = FieldProjection | HashProjection | FactProjection; export type Projection = CompositeProjection | SingularProjection; export interface Match { unknown: Label; conditions: Condition[]; } export interface Specification { given: Label[]; matches: Match[]; projection: Projection; } export const emptySpecification: Specification = { given: [], matches: [], projection: { type: "composite", components: [] } }; export function getAllFactTypes(specification: Specification): string[] { const factTypes: string[] = []; for (const given of specification.given) { factTypes.push(given.type); } factTypes.push(...getAllFactTypesFromMatches(specification.matches)); if (specification.projection.type === "composite") { factTypes.push(...getAllFactTypesFromProjection(specification.projection)); } const distinctFactTypes = Array.from(new Set(factTypes)); return distinctFactTypes; } function getAllFactTypesFromMatches(matches: Match[]): string[] { const factTypes: string[] = []; for (const match of matches) { factTypes.push(match.unknown.type); for (const condition of match.conditions) { if (condition.type === "path") { for (const role of condition.rolesLeft) { factTypes.push(role.predecessorType); } } else if (condition.type === "existential") { factTypes.push(...getAllFactTypesFromMatches(condition.matches)); } } } return factTypes; } function getAllFactTypesFromProjection(projection: CompositeProjection) { const factTypes: string[] = []; for (const component of projection.components) { if (component.type === "specification") { factTypes.push(...getAllFactTypesFromMatches(component.matches)); if (component.projection.type === "composite") { factTypes.push(...getAllFactTypesFromProjection(component.projection)); } } } return factTypes; } interface RoleDescription { successorType: string; name: string; predecessorType: string; } type TypeByLabel = { [label: string]: string; }; export function getAllRoles(specification: Specification): RoleDescription[] { const labels = specification.given .reduce((labels, label) => ({ ...labels, [label.name]: label.type }), {} as TypeByLabel); const { roles: rolesFromMatches, labels: labelsFromMatches } = getAllRolesFromMatches(labels, specification.matches); const components = specification.projection.type === "composite" ? specification.projection.components : []; const rolesFromComponents = getAllRolesFromComponents(labelsFromMatches, components); const roles: RoleDescription[] = [ ...rolesFromMatches, ...rolesFromComponents ]; const distinctRoles = roles.filter((value, index, array) => { return array.findIndex(r => r.successorType === value.successorType && r.name === value.name) === index; }); return distinctRoles; } function getAllRolesFromMatches(labels: TypeByLabel, matches: Match[]): { roles: RoleDescription[], labels: TypeByLabel } { const roles: RoleDescription[] = []; for (const match of matches) { labels = { ...labels, [match.unknown.name]: match.unknown.type }; for (const condition of match.conditions) { if (condition.type === "path") { let type = match.unknown.type; for (const role of condition.rolesLeft) { roles.push({ successorType: type, name: role.name, predecessorType: role.predecessorType }); type = role.predecessorType; } type = labels[condition.labelRight]; if (!type) { throw new Error(`Label ${condition.labelRight} not found`); } for (const role of condition.rolesRight) { roles.push({ successorType: type, name: role.name, predecessorType: role.predecessorType }); type = role.predecessorType; } } else if (condition.type === "existential") { const { roles: newRoleDescriptions } = getAllRolesFromMatches(labels, condition.matches); roles.push(...newRoleDescriptions); } } } return { roles, labels }; } function getAllRolesFromComponents(labels: TypeByLabel, components: ComponentProjection[]): RoleDescription[] { const roles: RoleDescription[] = []; for (const component of components) { if (component.type === "specification") { const { roles: rolesFromMatches, labels: labelsFromMatches } = getAllRolesFromMatches(labels, component.matches); roles.push(...rolesFromMatches); if (component.projection.type === "composite") { roles.push(...getAllRolesFromComponents(labelsFromMatches, component.projection.components)); } } } return roles; } export function specificationIsDeterministic(specification: Specification): boolean { return specification.matches.every(match => match.conditions.every(condition => condition.type === "path" && condition.rolesLeft.length === 0 ) ); } export function specificationIsNotDeterministic(specification: Specification): boolean { return specification.matches.some(match => match.conditions.some(condition => condition.type === "path" && condition.rolesLeft.length > 0 ) ); } export function splitBeforeFirstSuccessor(specification: Specification): { head: Specification | undefined, tail: Specification | undefined } { // Find the first match (if any) that seeks successors or has an existential condition const firstMatchWithSuccessor = specification.matches.findIndex(match => match.conditions.length !== 1 || match.conditions.some(condition => condition.type !== "path" || condition.rolesLeft.length > 0)); if (firstMatchWithSuccessor === -1) { // No match seeks successors, so the whole specification is deterministic return { head: specification, tail: undefined }; } else { // If there is only a single path condition, then split that path. const pivot = specification.matches[firstMatchWithSuccessor]; const pathConditions = pivot.conditions.filter(isPathCondition); if (pathConditions.length !== 1) { // Fall back to running the entire specification in the tail return { head: undefined, tail: specification }; } const existentialConditions = pivot.conditions.filter(isExistentialCondition); const condition = pathConditions[0]; if (condition.rolesRight.length === 0) { // The path contains only successor joins. // Put the entire match in the tail. if (firstMatchWithSuccessor === 0) { // There is nothing to put in the head return { head: undefined, tail: specification }; } else { // Split the matches between the head and tail const headMatches = specification.matches.slice(0, firstMatchWithSuccessor); const tailMatches = specification.matches.slice(firstMatchWithSuccessor); // Compute the givens of the head and tail const headGiven = referencedLabels(headMatches, specification.given); const allLabels = specification.given.concat(specification.matches.map(match => match.unknown)); const tailGiven = referencedLabels(tailMatches, allLabels); // Project the tail givens const headProjection: Projection = tailGiven.length === 1 ? <FactProjection>{ type: "fact", label: tailGiven[0].name } : <CompositeProjection>{ type: "composite", components: tailGiven.map(label => ( <FactProjection>{ type: "fact", label: label.name })) }; const head: Specification = { given: headGiven, matches: headMatches, projection: headProjection }; const tail: Specification = { given: tailGiven, matches: tailMatches, projection: specification.projection }; return { head, tail }; } } else { // The path contains both predecessor and successor joins. // Split the path into two paths. const splitLabel: Label = { name: 's1', type: condition.rolesRight[condition.rolesRight.length - 1].predecessorType }; const headCondition: Condition = { type: "path", labelRight: condition.labelRight, rolesLeft: [], rolesRight: condition.rolesRight }; const headMatch: Match = { unknown: splitLabel, conditions: [headCondition] } const tailCondition: Condition = { type: "path", labelRight: splitLabel.name, rolesLeft: condition.rolesLeft, rolesRight: [] }; const tailMatch: Match = { unknown: pivot.unknown, conditions: [tailCondition, ...existentialConditions] }; // Assemble the head and tail matches const headMatches = specification.matches.slice(0, firstMatchWithSuccessor).concat(headMatch); const tailMatches = [tailMatch].concat(specification.matches.slice(firstMatchWithSuccessor + 1)); // Compute the givens of the head and tail const headGiven = referencedLabels(headMatches, specification.given); const allLabels = specification.given .concat(specification.matches.map(match => match.unknown)) .concat([ splitLabel ]); const tailGiven = referencedLabels(tailMatches, allLabels); // Project the tail givens const headProjection: Projection = tailGiven.length === 1 ? <FactProjection>{ type: "fact", label: tailGiven[0].name } : <CompositeProjection>{ type: "composite", components: tailGiven.map(label => ( <FactProjection>{ type: "fact", label: label.name })) }; const head: Specification = { given: headGiven, matches: headMatches, projection: headProjection }; const tail: Specification = { given: tailGiven, matches: tailMatches, projection: specification.projection }; return { head, tail }; } } } function referencedLabels(matches: Match[], labels: Label[]): Label[] { // Find all labels referenced in the matches const definedLabels = matches.map(match => match.unknown.name); const referencedLabels = matches.flatMap(labelsInMatch) .filter(label => definedLabels.indexOf(label) === -1); return labels.filter(label => referencedLabels.indexOf(label.name) !== -1); } function labelsInMatch(match: Match): string[] { return match.conditions.flatMap(labelsInCondition); } function labelsInCondition(condition: Condition): string[] { if (condition.type === "path") { return [ condition.labelRight ]; } else if (condition.type === "existential") { return condition.matches.flatMap(labelsInMatch); } else { const _exhaustiveCheck: never = condition; throw new Error(`Unexpected condition type ${(_exhaustiveCheck as any).type}`); } } export function specificationIsIdentity(specification: Specification) { return specification.matches.every(match => match.conditions.every(condition => condition.type === "path" && condition.rolesLeft.length === 0 && condition.rolesRight.length === 0 ) ); } export function reduceSpecification(specification: Specification): Specification { // Remove all projections except for specification projections. return { given: specification.given, matches: specification.matches, projection: reduceProjection(specification.projection) }; } function reduceProjection(projection: Projection): Projection { if (projection.type === "composite") { const reducedComponents = projection.components .map(reduceComponent) .filter((component): component is NamedComponentProjection => component !== null); return { type: "composite", components: reducedComponents }; } else { return { type: "composite", components: [] }; } } function reduceComponent(component: NamedComponentProjection): NamedComponentProjection | null { if (component.type === "specification") { return { type: "specification", name: component.name, matches: component.matches, projection: reduceProjection(component.projection) }; } else { return null; } }