diff --git a/app/rdf/queries.ts b/app/rdf/queries.ts index 027308ecc..0b929ad96 100644 --- a/app/rdf/queries.ts +++ b/app/rdf/queries.ts @@ -9,7 +9,6 @@ import { LRUCache } from "typescript-lru-cache"; import { FilterValueMulti, Filters } from "@/config-types"; import { - DimensionValue, Observation, ObservationValue, parseObservationValue, @@ -31,7 +30,7 @@ import { parseRelatedDimensions, } from "@/rdf/parse"; import { - loadDimensionValuesWithMetadata, + loadDimensionsValuesWithMetadata, loadMaxDimensionValue, loadMinMaxDimensionValues, } from "@/rdf/query-dimension-values"; @@ -195,21 +194,65 @@ const getCubeDimensionsValues = async ( filters?: Filters; cache: LRUCache | undefined; } +) => { + const dimensionIris = resolvedDimensions.map((d) => d.data.iri); + const { minMaxDimensions, regularDimensions } = resolvedDimensions.reduce<{ + minMaxDimensions: ResolvedDimension[]; + regularDimensions: ResolvedDimension[]; + }>( + (acc, dimension) => { + if (shouldLoadMinMaxValues(dimension)) { + acc.minMaxDimensions.push(dimension); + } else { + acc.regularDimensions.push(dimension); + } + + return acc; + }, + { minMaxDimensions: [], regularDimensions: [] } + ); + + const result = await Promise.all([ + getMinMaxDimensionsValues(minMaxDimensions, { + sparqlClient, + cache, + }), + getRegularDimensionsValues(regularDimensions, { + sparqlClient, + filters, + cache, + }), + ]); + + return result + .flat() + .sort( + (a, b) => + dimensionIris.indexOf(a.dimensionIri) - + dimensionIris.indexOf(b.dimensionIri) + ) + .map(({ values }) => values); +}; + +const getMinMaxDimensionsValues = async ( + resolvedDimensions: ResolvedDimension[], + { + sparqlClient, + cache, + }: { + sparqlClient: ParsingClient; + cache: LRUCache | undefined; + } ) => { return await Promise.all( resolvedDimensions.map(async (resolvedDimension) => { - if (shouldLoadMinMaxValues(resolvedDimension)) { - return await getMinMaxDimensionValues(resolvedDimension, { + return { + dimensionIri: resolvedDimension.data.iri, + values: await getMinMaxDimensionValues(resolvedDimension, { sparqlClient, cache, - }); - } else { - return await getRegularDimensionValues(resolvedDimension, { - sparqlClient, - filters, - cache, - }); - } + }), + }; }) ); }; @@ -289,8 +332,8 @@ const getMinMaxDimensionValues = async ( return []; }; -const getRegularDimensionValues = async ( - resolvedDimension: ResolvedDimension, +const getRegularDimensionsValues = async ( + resolvedDimensions: ResolvedDimension[], { sparqlClient, filters, @@ -300,10 +343,12 @@ const getRegularDimensionValues = async ( filters?: Filters; cache: LRUCache | undefined; } -): Promise => { - const { cube, data, locale } = resolvedDimension; - return await loadDimensionValuesWithMetadata(cube.term?.value!, { - dimensionIri: data.iri, +) => { + // `cube` and `locale` are the same for all dimensions + const { cube, locale } = resolvedDimensions[0]; + const cubeIri = cube.term?.value!; + return await loadDimensionsValuesWithMetadata(cubeIri, { + dimensionIris: resolvedDimensions.map((d) => d.data.iri), cubeDimensions: cube.dimensions, sparqlClient, filters, diff --git a/app/rdf/query-dimension-values.ts b/app/rdf/query-dimension-values.ts index 02e8a8f15..b44946bad 100644 --- a/app/rdf/query-dimension-values.ts +++ b/app/rdf/query-dimension-values.ts @@ -74,6 +74,198 @@ const getFilterOrder = (filter: FilterValue) => { return 0; }; +type LoadDimensionsValuesProps = { + dimensionIris: string[]; + cubeDimensions: CubeDimension[]; + sparqlClient: ParsingClient; + filters?: Filters; + locale: string; + cache: LRUCache | undefined; +}; + +export async function loadDimensionsValuesWithMetadata( + cubeIri: string, + props: LoadDimensionsValuesProps +) { + const { + dimensionIris, + cubeDimensions, + sparqlClient, + filters, + locale, + cache, + } = props; + + const dimensionQueries = dimensionIris.map((dimensionIri) => { + const filterList = getFiltersList(filters, dimensionIri); + const queryFilters = getQueryFilters( + filterList, + cubeDimensions, + dimensionIri + ); + + if (!cubeDimensions.find((d) => d.path?.value === dimensionIri)) { + throw new Error(`Dimension not found: ${dimensionIri}`); + } + + return `${ + queryFilters + ? "" + : `{ #pragma evaluate on + SELECT ?dimensionIri ?versionedValue ?unversionedValue WHERE { + VALUES ?dimensionIri { <${dimensionIri}> } + <${cubeIri}> cube:observationConstraint/sh:property ?dimension . + ?dimension sh:path ?dimensionIri . + ?dimension schema:version ?version . + ?dimension sh:in/rdf:rest*/rdf:first ?versionedValue . + ?versionedValue schema:sameAs ?unversionedValue . + } + } UNION { + SELECT ?dimensionIri ?versionedValue ?unversionedValue WHERE { + VALUES ?dimensionIri { <${dimensionIri}> } + <${cubeIri}> cube:observationConstraint/sh:property ?dimension . + ?dimension sh:path ?dimensionIri . + FILTER NOT EXISTS { ?dimension schema:version ?version . } + ?dimension sh:in/rdf:rest*/rdf:first ?versionedValue . + BIND(?versionedValue as ?unversionedValue) + } + } UNION` + } { + { + SELECT DISTINCT ?dimensionIri ?versionedValue ?unversionedValue WHERE { + { #pragma evaluate on + SELECT ?observation WHERE { + ${ + queryFilters + ? `VALUES ?dimensionIri { <${dimensionIri}> } + <${cubeIri}> cube:observationConstraint/sh:property ?dimension . + ?dimension sh:path ?dimensionIri . + ?dimension schema:version ?version .` + : `VALUES ?dimensionIri { <${dimensionIri}> } + <${cubeIri}> cube:observationConstraint/sh:property ?dimension . + ?dimension sh:path ?dimensionIri . + ?dimension schema:version ?version . + FILTER NOT EXISTS { ?dimension sh:in ?in . }` + } + <${cubeIri}> cube:observationSet/cube:observation ?observation . + ${queryFilters} + } + } + ${ + queryFilters + ? `VALUES ?dimensionIri { <${dimensionIri}> } + <${cubeIri}> cube:observationConstraint/sh:property ?dimension . + ?dimension sh:path ?dimensionIri .` + : `VALUES ?dimensionIri { <${dimensionIri}> }` + } + ?observation ?dimensionIri ?versionedValue . + ?versionedValue schema:sameAs ?unversionedValue . + } + } + } UNION { + { + SELECT DISTINCT ?dimensionIri ?versionedValue ?unversionedValue WHERE { + { #pragma evaluate on + SELECT ?observation WHERE { + ${ + queryFilters + ? `VALUES ?dimensionIri { <${dimensionIri}> } + <${cubeIri}> cube:observationConstraint/sh:property ?dimension . + ?dimension sh:path ?dimensionIri . + FILTER NOT EXISTS { ?dimension schema:version ?version . }` + : `VALUES ?dimensionIri { <${dimensionIri}> } + <${cubeIri}> cube:observationConstraint/sh:property ?dimension . + ?dimension sh:path ?dimensionIri . + FILTER NOT EXISTS { ?dimension schema:version ?version . } + FILTER NOT EXISTS { ?dimension sh:in ?in . }` + } + <${cubeIri}> cube:observationSet/cube:observation ?observation . + ${queryFilters} + } + } + ${ + queryFilters + ? `VALUES ?dimensionIri { <${dimensionIri}> } + <${cubeIri}> cube:observationConstraint/sh:property ?dimension . + ?dimension sh:path ?dimensionIri .` + : `VALUES ?dimensionIri { <${dimensionIri}> }` + } + ?observation ?dimensionIri ?versionedValue . + BIND(?versionedValue as ?unversionedValue) + } + } + }`; + }); + + const query = `PREFIX cube: +PREFIX geo: +PREFIX schema: +PREFIX sh: +PREFIX rdf: + +CONSTRUCT { + ?dimensionIri rdf:first ?unversionedValue . + ?unversionedValue + schema:name ?name ; + schema:alternateName ?alternateName ; + schema:description ?description ; + schema:identifier ?identifier ; + schema:position ?position ; + schema:color ?color ; + geo:hasGeometry ?geometry ; + schema:latitude ?latitude ; + schema:longitude ?longitude . +} WHERE { + ${dimensionQueries.join("\nUNION ")} + ${buildLocalizedSubQuery("versionedValue", "schema:name", "name", { + locale, + })} + ${buildLocalizedSubQuery( + "versionedValue", + "schema:description", + "description", + { + locale, + } + )} + ${buildLocalizedSubQuery( + "versionedValue", + "schema:alternateName", + "alternateName", + { + locale, + } + )} + OPTIONAL { ?versionedValue schema:identifier ?identifier . } + OPTIONAL { ?versionedValue schema:position ?position . } + OPTIONAL { ?versionedValue schema:color ?color . } + OPTIONAL { ?versionedValue geo:hasGeometry ?geometry . } + OPTIONAL { ?versionedValue schema:latitude ?latitude . } + OPTIONAL { ?versionedValue schema:longitude ?longitude . } +}`; + + return await executeWithCache( + sparqlClient, + query, + () => sparqlClient.query.construct(query, { operation: "postUrlencoded" }), + (quads) => { + const result: { [dimensionIri: string]: DimensionValue[] } = + Object.fromEntries(dimensionIris.map((iri) => [iri, []])); + + for (const q of quads.filter((q) => q.predicate.equals(ns.rdf.first))) { + const dimensionIri = q.subject.value; + result[dimensionIri]?.push(parseDimensionValue(q, quads)); + } + + return Object.entries(result).map(([dimensionIri, values]) => ({ + dimensionIri, + values, + })); + }, + cache + ); +} + type LoadDimensionValuesProps = { dimensionIri: string; cubeDimensions: CubeDimension[]; @@ -89,10 +281,10 @@ type LoadDimensionValuesProps = { * Filters on other dimensions can be passed. * */ -export async function loadDimensionValuesWithMetadata( +export const loadDimensionValuesWithMetadata = async ( cubeIri: string, props: LoadDimensionValuesProps -): Promise { +): Promise => { const { dimensionIri, cubeDimensions, sparqlClient, filters, locale, cache } = props; const filterList = getFiltersList(filters, dimensionIri); @@ -192,7 +384,7 @@ CONSTRUCT { }, cache ); -} +}; const parseDimensionValue = ( valueQuad: Quad,