Skip to content

Commit

Permalink
feat: view(cube) preview
Browse files Browse the repository at this point in the history
  • Loading branch information
tpluscode committed Nov 9, 2023
1 parent d7bf310 commit a83ec4c
Show file tree
Hide file tree
Showing 12 changed files with 265 additions and 68 deletions.
16 changes: 16 additions & 0 deletions .changeset/cuddly-points-confess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"rdf-cube-view-query": minor
---

Added a preview method to View. The return types is same as that of `View#observations`

```js
let source
const cube = await source.cube('URI')
const view = View.fromCube(cube)

const observationPreview = view.preview({
// limit: 10,
// offset: 100
})
```
9 changes: 7 additions & 2 deletions examples/get-view-from-cube-demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ async function main() {
#pragma join.hash off`

const source = new Source({
endpointUrl: 'https://int.lindas.admin.ch/query',
endpointUrl: 'https://lindas.admin.ch/query',
queryPrefix,
})

const cube = await source.cube('https://ld.stadt-zuerich.ch/statistics/ZUS-BTA-ZSA')
const cube = await source.cube('https://environment.ld.admin.ch/foen/nfi/nfi_C-2207/cube/2023-2')
const view = View.fromCube(cube)

console.log('---------')
Expand All @@ -30,6 +30,11 @@ async function main() {
for (const dimension of view.dimensions) {
console.log('dimensions', dimension.cubeDimensions.map(x => x.path.value), 'from cubes', dimension.cubes.map(x => x.value))
}

console.log('---------')
console.log('Sample observation')
const previewRows = await view.preview({ limit: 1 })
console.log(previewRows)
}

main()
5 changes: 5 additions & 0 deletions lib/Cube.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Node from './Node.js'
import * as ns from './namespaces.js'
import { cubeQuery } from './query/cube.js'
import * as queryFilter from './query/cubesFilter.js'
import View from './View.js'

export default class Cube extends Node {
constructor({ parent, term, dataset, graph, source, ignore = [ns.rdf.type] }) {
Expand Down Expand Up @@ -56,6 +57,10 @@ export default class Cube extends Node {
return `DESCRIBE <${this.ptr.out(ns.cube.observationConstraint).value}> FROM <${this.source.graph.value}>`
}

previewQuery(arg) {
return new View(this).previewQuery(arg)
}

async fetchShape() {
const shapeData = await this.source.client.query.construct(this.shapeQuery())

Expand Down
28 changes: 26 additions & 2 deletions lib/View.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,31 @@ export default class View extends Node {
return new ViewQuery(this.ptr, { disableDistinct })
}

async observations({ disableDistinct } = { disableDistinct: false }) {
/**
* @param {{ limit: (number|undefined), offset: (number|undefined) }} [arg]
* @returns {Promise<Array<Record<string, Term>>>}
*/
async preview(arg) {
return this._getObservations(() => {
const viewQuery = this.observationsQuery()

return {
query: viewQuery.previewQuery(arg).toString(),
dimensions: viewQuery.dimensions,
}
})
}

/**
* @param [Object] arg
* @param {boolean} arg.disableDistinct
* @returns {Promise<Array<Record<string, Term>>>}
*/
observations({ disableDistinct } = { disableDistinct: false }) {
return this._getObservations(() => this.observationsQuery({ disableDistinct }))
}

async _getObservations(getQuery) {
if (!this.dimensions.length) {
throw Error('No dimensions')
}
Expand All @@ -134,7 +158,7 @@ export default class View extends Node {
throw Error('No source with client')
}

const { query, dimensions } = this.observationsQuery({ disableDistinct })
const { query, dimensions } = getQuery()

const columns = dimensions.array.filter(d => d.isResult).map(d => [d.variable, d.property])
const rows = await source.client.query.select(query, { operation: source.queryOperation })
Expand Down
14 changes: 9 additions & 5 deletions lib/query/ViewQuery/Patterns.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,23 @@ export default class Patterns {
})
}

buildPatterns() {
const sourcePatterns = this.viewQuery.sources.array
get sourcePatterns() {
return this.viewQuery.sources.array
.map(source => source.patterns)
.reduce((all, pattern) => all.concat(pattern), [])
}

const dimensionPatterns = this.viewQuery.dimensions.array
get dimensionPatterns() {
return this.viewQuery.dimensions.array
.filter(dimension => !dimension.filterPattern)
.map(dimension => dimension.patterns)
.reduce((all, pattern) => all.concat(pattern), [])
}

buildPatterns() {
return [
...sourcePatterns,
...dimensionPatterns,
...this.sourcePatterns,
...this.dimensionPatterns,
]
}
}
31 changes: 24 additions & 7 deletions lib/query/ViewQuery/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import Result from './Result.js'
import Sources from './Sources.js'

export default class ViewQuery {
constructor(ptr, { disableDistinct } = {}) {
constructor(ptr, { preview, disableDistinct } = {}) {
this.varCounts = {}

this.view = ptr
this.preview = preview
this.disableDistinct = disableDistinct

this.dimensions = new Dimensions(this)
Expand All @@ -30,17 +31,15 @@ export default class ViewQuery {
}

build() {
let queryNoOffsetLimit

if (this.disableDistinct) {
queryNoOffsetLimit = sparql.select(this.result.buildProjection())
this._queryNoOffsetLimit = sparql.select(this.result.buildProjection())
.where([
...this.patterns.buildPatterns(),
...this.filters.buildFilters(),
])
.orderBy(this.result.buildOrderBy())
} else {
queryNoOffsetLimit = sparql.select(this.result.buildProjection(), { distinct: true })
this._queryNoOffsetLimit = sparql.select(this.result.buildProjection(), { distinct: true })
.where([
...this.patterns.buildPatterns(),
...this.filters.buildFilters(),
Expand All @@ -50,8 +49,26 @@ export default class ViewQuery {
.orderBy(this.result.buildOrderBy())
}

this.query = this.result.addOffsetLimit(queryNoOffsetLimit.clone())
this.query = this.result.addOffsetLimit(this._queryNoOffsetLimit.clone())

this.countQuery = sparql.select(['(COUNT(*) AS ?count)']).where([this._queryNoOffsetLimit])
}

previewQuery({ limit = 10, offset = 0 } = {}) {
const sourceVariables = this.sources.array
.filter(source => source.isCubeSource)
.map(source => source.variable)
const sourceSubquery = sparql
.select(sourceVariables)
.where(this.patterns.sourcePatterns)
.limit(limit)
.offset(offset)

this.countQuery = sparql.select(['(COUNT(*) AS ?count)']).where([queryNoOffsetLimit])
return sparql.select(this.result.buildProjection())
.where([
sourceSubquery,
...this.patterns.dimensionPatterns,
...this.filters.buildFilters(),
])
}
}
22 changes: 22 additions & 0 deletions test/Cube.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { jestSnapshotPlugin } from 'mocha-chai-jest-snapshot'
import { cubesQuery } from '../lib/query/cubes.js'
import Cube from '../lib/Cube.js'
import Source from '../lib/Source.js'
import { View } from '../index.js'
import { buildCube } from './support/buildCube.js'
import * as ns from './support/namespaces.js'
import { cleanQuery } from './support/utils.js'
Expand Down Expand Up @@ -302,4 +303,25 @@ describe('Cube', () => {
strictEqual(ns.ex.down.equals(cube.out(ns.ex.predicate).term), true)
})
})

describe('previewQuery', () => {
it('creates a view query from the given cube', () => {
// given
const cube = buildCube({
term: ns.ex.cube,
endpointUrl: ns.ex.endpoint,
dimensions: [{
path: ns.ex.propertyA,
}, {
path: ns.ex.propertyB,
}],
})

// when
const query = View.fromCube(cube).observationsQuery().previewQuery()

// then
expect(cleanQuery(query.toString())).toMatchSnapshot()
})
})
})
86 changes: 49 additions & 37 deletions test/ViewQuery.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,55 +5,67 @@ import { testView } from './support/testView.js'
describe('query/ViewQuery', () => {
chai.use(jestSnapshotPlugin())

it('should generate a query with the given columns in the result set', async () => {
expect(await testView('columns')).query.toMatchSnapshot()
})
describe('.query', () => {
it('should generate a query with the given columns in the result set', async () => {
expect(await testView('columns')).query.toMatchSnapshot()
})

it('should generate a query without distinct if disableDistinct is true', async () => {
expect(await testView.disableDistinct('disableDistinct')).query.matchSnapshot()
})
it('should generate a query without distinct if disableDistinct is true', async () => {
expect(await testView.disableDistinct('disableDistinct')).query.matchSnapshot()
})

it('should generate a day function filter', async () => {
expect(await testView('functionDay')).query.toMatchSnapshot()
})
it('should generate a day function filter', async () => {
expect(await testView('functionDay')).query.toMatchSnapshot()
})

it('should generate a month function filter', async () => {
expect(await testView('functionMonth')).query.toMatchSnapshot()
})
it('should generate a month function filter', async () => {
expect(await testView('functionMonth')).query.toMatchSnapshot()
})

it('should generate a year function filter', async () => {
expect(await testView('functionYear')).query.toMatchSnapshot()
})
it('should generate a year function filter', async () => {
expect(await testView('functionYear')).query.toMatchSnapshot()
})

it('should generate a language filter', async () => {
expect(await testView('language')).query.toMatchSnapshot()
})
it('should generate a language filter', async () => {
expect(await testView('language')).query.toMatchSnapshot()
})

it('should generate a language filter with aggregate', async () => {
expect(await testView('languageMin')).query.toMatchSnapshot()
})
it('should generate a language filter with aggregate', async () => {
expect(await testView('languageMin')).query.toMatchSnapshot()
})

it('should generate LIMIT and OFFSET with the values given in projection/orderBy', async () => {
expect(await testView('limitOffset')).query.toMatchSnapshot()
})
it('should generate LIMIT and OFFSET with the values given in projection/orderBy', async () => {
expect(await testView('limitOffset')).query.toMatchSnapshot()
})

it('should generate ORDER BY in the direction given in projection/orderBy', async () => {
expect(await testView('orderBy')).query.toMatchSnapshot()
})
it('should generate ORDER BY in the direction given in projection/orderBy', async () => {
expect(await testView('orderBy')).query.toMatchSnapshot()
})

it('should generate a count query', async () => {
expect(await testView('simple')).countQuery.toMatchSnapshot()
})
it('should generate a count query', async () => {
expect(await testView('simple')).countQuery.toMatchSnapshot()
})

it('should generate a count query without LIMIT and OFFSET', async () => {
expect(await testView('limitOffset')).countQuery.toMatchSnapshot()
})
it('should generate a count query without LIMIT and OFFSET', async () => {
expect(await testView('limitOffset')).countQuery.toMatchSnapshot()
})

it('should generate a Stardog text search filter', async () => {
expect(await testView('stardogTextSearch')).query.toMatchSnapshot()
})

it('should generate a Stardog text search filter', async () => {
expect(await testView('stardogTextSearch')).query.toMatchSnapshot()
it('should generate only generate query with projected columns', async () => {
expect(await testView('projection')).query.toMatchSnapshot()
})
})

it('should generate only generate query with projected columns', async () => {
expect(await testView('projection')).query.toMatchSnapshot()
describe('.previewQuery', () => {
it('should generate subquery with default limit', async () => {
expect(await testView('projection')).previewQuery().toMatchSnapshot()
})

it('should generate subquery with changed limit/offset', async () => {
expect(await testView('simple')).previewQuery({ limit: 20, offset: 80 }).toMatchSnapshot()
})
})
})
14 changes: 14 additions & 0 deletions test/__snapshots__/Cube.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,17 @@ exports[`Cube filter version should create an eq filter for the given version 1`
FILTER(?v0 = 2 )
}"
`;
exports[`Cube previewQuery creates a view query from the given cube 1`] = `
"SELECT ?dimension0 ?dimension1 WHERE {
{
SELECT ?source0 WHERE {
<http://example.org/cube> <https://cube.link/observationSet> ?observationSet0.
?observationSet0 <https://cube.link/observation> ?source0.
}
LIMIT 10
}
?source0 <http://example.org/propertyA> ?dimension0;
<http://example.org/propertyB> ?dimension1.
}"
`;
Loading

0 comments on commit a83ec4c

Please sign in to comment.