feat(queries): implemented fast query route for children
and started scaffolding the slower, filter enabled one
This commit is contained in:
@@ -41,7 +41,10 @@ module.exports = {
|
||||
return await getUser( parent.author )
|
||||
},
|
||||
async children( parent, args, context, info ) {
|
||||
console.log( parent.totalChildrenCount )
|
||||
console.log( args )
|
||||
|
||||
|
||||
throw new ApolloError( 'Not implemented' )
|
||||
}
|
||||
},
|
||||
|
||||
@@ -47,22 +47,28 @@ type Object {
|
||||
The full object, with all its props & other things. **NOTE:** If you're requesting objects for the purpose of recreating & displaying, you probably only want to request this specific field.
|
||||
"""
|
||||
data: JSON
|
||||
|
||||
"""
|
||||
Any objects that this object references.
|
||||
|
||||
`offset`: TODO
|
||||
|
||||
`limit`: TODO
|
||||
|
||||
`depth`: TODO
|
||||
|
||||
`query`: TODO
|
||||
"""
|
||||
children(offset: Int! = 0, limit: Int! = 100, depth: Int! = 1, query: String, fields: [String] = [], ): [ObjectCollection]
|
||||
children(
|
||||
limit: Int! = 100,
|
||||
depth: Int! = 50,
|
||||
select: [String],
|
||||
cursor: String,
|
||||
query: String,
|
||||
orderBy: String ): ObjectCollection!
|
||||
|
||||
"""
|
||||
Query the object's childern, so you can receive only the ones you want to.
|
||||
"""
|
||||
childernQuery(limit: Int! = 100, cursor:String, select: [String], depth: Int! = 1 ): ObjectCollection!
|
||||
}
|
||||
|
||||
type ObjectCollection {
|
||||
totalCount: Int!
|
||||
cursor: String!
|
||||
hasMore: Boolean!
|
||||
objects: [Object]!
|
||||
}
|
||||
|
||||
|
||||
@@ -75,9 +75,6 @@ exports.up = async knex => {
|
||||
table.index( [ 'speckle_type' ], 'type_index' )
|
||||
} )
|
||||
|
||||
await knex.raw( 'ALTER TABLE "objects" add column "serial_id" bigserial' )
|
||||
await knex.raw( 'CREATE INDEX serial_idx ON objects(serial_id) ' )
|
||||
|
||||
// Tree inheritance tracker
|
||||
await knex.schema.createTable( 'object_tree_refs', table => {
|
||||
table.increments( 'id' )
|
||||
|
||||
@@ -3,6 +3,7 @@ const bcrypt = require( 'bcrypt' )
|
||||
const crs = require( 'crypto-random-string' )
|
||||
const { performance } = require( 'perf_hooks' )
|
||||
const crypto = require( 'crypto' )
|
||||
const set = require( 'lodash.set' )
|
||||
|
||||
let debug = require( 'debug' )( 'speckle:services' )
|
||||
|
||||
@@ -67,6 +68,8 @@ module.exports = {
|
||||
},
|
||||
|
||||
async createObjects( objects ) {
|
||||
// TODO: Switch to knex batch inserting functionality
|
||||
// see http://knexjs.org/#Utility-BatchInsert
|
||||
let batches = [ ]
|
||||
let maxBatchSize = process.env.MAX_BATCH_SIZE || 250
|
||||
objects = [ ...objects ]
|
||||
@@ -93,13 +96,13 @@ module.exports = {
|
||||
if ( obj.__closure !== null ) {
|
||||
for ( const prop in obj.__closure ) {
|
||||
closures.push( { parent: insertionObject.id, child: prop, minDepth: obj.__closure[ prop ] } )
|
||||
|
||||
|
||||
totalChildrenCountGlobal++
|
||||
|
||||
if( totalChildrenCountByDepth[ obj.__closure[prop].toString() ] )
|
||||
totalChildrenCountByDepth[ obj.__closure[ prop ].toString() ]++
|
||||
else
|
||||
totalChildrenCountByDepth[ obj.__closure[ prop ].toString() ] = 1
|
||||
|
||||
if ( totalChildrenCountByDepth[ obj.__closure[ prop ].toString( ) ] )
|
||||
totalChildrenCountByDepth[ obj.__closure[ prop ].toString( ) ]++
|
||||
else
|
||||
totalChildrenCountByDepth[ obj.__closure[ prop ].toString( ) ] = 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,78 +140,94 @@ module.exports = {
|
||||
return res
|
||||
},
|
||||
|
||||
async getObjectChildren( objectId, offset, limit, depth, query, fields, orderBy ) {
|
||||
offset = Math.abs( offset ) || 0
|
||||
limit = Math.abs( limit ) || 100
|
||||
depth = Math.abs( depth ) || 2
|
||||
async getObjectChildren( { objectId, limit, depth, select, cursor } ) {
|
||||
limit = parseInt( limit ) || 50
|
||||
depth = parseInt( depth ) || 1000
|
||||
|
||||
fields = [ 'text', 'nest.flag', 'nest.what', 'arr[1]', 'arr[2]', 'nest.orderMe' ]
|
||||
let selectFields = `obj_id as id, speckle_type`
|
||||
let unwrapData = false
|
||||
let selectStatements = [ ]
|
||||
|
||||
fields.forEach( f => {
|
||||
selectFields += `, jsonb_path_query(data, '$.${ f }') as "data.${f}"`
|
||||
if ( select && select.length > 0 ) {
|
||||
selectStatements.push( `jsonb_path_query(data, '$.id') as id` )
|
||||
select.forEach( f => {
|
||||
selectStatements += `, jsonb_path_query(data, '$.${ f }') as "${f}"`
|
||||
} )
|
||||
} else {
|
||||
selectStatements.push( '"data"' )
|
||||
unwrapData = true
|
||||
}
|
||||
|
||||
let q = Closures( )
|
||||
.select( knex.raw( selectStatements ) )
|
||||
.rightJoin( 'objects', 'objects.id', 'object_children_closure.child' )
|
||||
.where( knex.raw( 'parent = ?', [ objectId ] ) )
|
||||
.andWhere( knex.raw( '"minDepth" < ?', [ depth ] ) )
|
||||
.andWhere( knex.raw( 'id > ?', [ cursor ? cursor : '0' ] ) )
|
||||
.orderBy( 'objects.id' )
|
||||
.limit( limit )
|
||||
|
||||
let rows = await q
|
||||
|
||||
if ( unwrapData ) rows.forEach( ( o, i, arr ) => arr[ i ] = { ...o.data } )
|
||||
else rows.forEach( ( o, i, arr ) => {
|
||||
let no = {}
|
||||
for ( let key in o ) set( no, key, o[ key ] )
|
||||
arr[ i ] = no
|
||||
} )
|
||||
|
||||
orderBy = { property: 'nest.orderMe', direction: 'desc' }
|
||||
let lastId = rows[ rows.length - 1 ].id
|
||||
return { rows, cursor: lastId }
|
||||
},
|
||||
|
||||
// console.log( Refs( ).where( { parent: objectId } ).select( '*' ).toString() )
|
||||
// TODO: Analyse and optimise query.
|
||||
let rawQuery = knex.raw( `
|
||||
WITH ids AS (
|
||||
SELECT DISTINCT unnest( string_to_array( ltree2text( subltree("path", 1, ${depth}) ), '.') ) as obj_id
|
||||
FROM object_tree_refs
|
||||
WHERE parent = '${objectId}'
|
||||
),
|
||||
objs AS (
|
||||
SELECT ${selectFields}
|
||||
FROM ids
|
||||
JOIN objects ON ids.obj_id = objects.id
|
||||
-- WHERE objects."data" @> '{"text": "This is object 1"}'
|
||||
${ orderBy && orderBy.property && orderBy.direction ? ("ORDER BY jsonb_path_query(data, '$." + orderBy.property + "' ) " + orderBy.direction || "ASC" ) : "ORDER BY obj_id" }
|
||||
)
|
||||
SELECT * from objs
|
||||
RIGHT JOIN (SELECT count(*) FROM objs) d(totalCount) ON TRUE
|
||||
OFFSET ${offset}
|
||||
LIMIT ${limit}
|
||||
` )
|
||||
async getObjectChildrenQuery( { objectId, limit, depth, select, cursor, query } ) {
|
||||
limit = parseInt( limit ) || 50
|
||||
depth = parseInt( depth ) || 1000
|
||||
|
||||
let betterQuery = `
|
||||
WITH ids AS(
|
||||
SELECT unnest( string_to_array( ltree2text( subltree("path", 1, 2) ), '.') ) as obj_id
|
||||
FROM object_tree_refs
|
||||
-- WHERE path ~ '0_hash.*{1}'
|
||||
WHERE nlevel(path) = 2
|
||||
),
|
||||
objs AS(
|
||||
SELECT obj_id, speckle_type, serial_id,
|
||||
jsonb_path_query(data, '$.text') as "data.text",
|
||||
jsonb_path_query(data, '$.nest.flag') as "data.nest.flag",
|
||||
jsonb_path_query(data, '$.nest.what') as "data.nest.what",
|
||||
jsonb_path_query(data, '$.arr[1]') as "data.arr[1]",
|
||||
jsonb_path_query(data, '$.arr[2]') as "data.arr[2]",
|
||||
jsonb_path_query(data, '$.nest.orderMe') as "data.nest.orderMe"
|
||||
FROM ids
|
||||
JOIN objects ON ids.obj_id = objects.id
|
||||
-- WHERE (objects."data" -> 'nest' ->> 'orderMe')::numeric >= 19001
|
||||
-- AND (objects."data"->'nest'->>'what') LIKE '%42%'
|
||||
)
|
||||
SELECT * FROM objs
|
||||
RIGHT JOIN (SELECT count(*) FROM objs ) c(total_count) ON TRUE
|
||||
ORDER BY serial_id desc
|
||||
OFFSET 310
|
||||
LIMIT 1000
|
||||
`
|
||||
let unwrapData = false
|
||||
let q = knex.with( 'objs', qb => {
|
||||
qb.select( 'id' ).from( 'object_children_closure' )
|
||||
if ( select && select.length > 0 ) {
|
||||
select.forEach( ( field, index ) => {
|
||||
qb.select( knex.raw( 'jsonb_path_query(data, :path) as :name:', { path: "$." + field, name: '' + index } ) )
|
||||
} )
|
||||
} else {
|
||||
unwrapData = true
|
||||
qb.select( 'data' )
|
||||
}
|
||||
qb.join( 'objects', 'child', '=', 'objects.id' )
|
||||
.where( 'parent', objectId )
|
||||
|
||||
console.log( rawQuery.toString( ) )
|
||||
if( query && query.length > 0) {
|
||||
query.forEach( statement => {
|
||||
// qb.andWhere()
|
||||
})
|
||||
}
|
||||
|
||||
let t0 = performance.now( )
|
||||
} )
|
||||
.select( '*' ).from( 'objs' )
|
||||
.joinRaw( 'RIGHT JOIN ( SELECT count(*) FROM "objs" ) c(total_count) ON TRUE' )
|
||||
// .orderBy() // TODO
|
||||
.limit( 2 )
|
||||
|
||||
let res = await rawQuery
|
||||
console.log( q.toString( ) )
|
||||
|
||||
let t1 = performance.now( )
|
||||
let rows = await q
|
||||
console.log( rows )
|
||||
|
||||
let totalCount = rows && rows.length > 0 ? rows[ 0 ].total_count : 0
|
||||
|
||||
console.log( `Found ${res.rows.length} in ${t1-t0}ms.` )
|
||||
if ( unwrapData ) rows.forEach( ( o, i, arr ) => arr[ i ] = { ...o.data } )
|
||||
else {
|
||||
rows.forEach( ( o, i, arr ) => {
|
||||
let no = {}
|
||||
let k = 0
|
||||
for ( let field of select ) {
|
||||
set( no, field, o[ k++ ] )
|
||||
}
|
||||
arr[ i ] = no
|
||||
} )
|
||||
}
|
||||
console.log( rows )
|
||||
},
|
||||
|
||||
async getObjects( objectIds ) {
|
||||
|
||||
@@ -12,7 +12,7 @@ chai.use( chaiHttp )
|
||||
|
||||
const { createUser, createToken, revokeToken, revokeTokenById, validateToken, getUserTokens } = require( '../users/services' )
|
||||
const { createStream, getStream, updateStream, deleteStream, getStreamsUser, grantPermissionsStream, revokePermissionsStream } = require( '../streams/services' )
|
||||
const { createCommit, createObject, createObjects, getObject, getObjects, getObjectChildren } = require( '../objects/services' )
|
||||
const { createCommit, createObject, createObjects, getObject, getObjects, getObjectChildren, getObjectChildrenQuery } = require( '../objects/services' )
|
||||
|
||||
const sampleObjects = require( './sampleObjectData' )
|
||||
|
||||
@@ -154,23 +154,57 @@ describe( 'Objects', ( ) => {
|
||||
expect( match2.id ).to.equal( objs[ 2 ].id )
|
||||
} )
|
||||
|
||||
let parentObjectId
|
||||
|
||||
it( 'Should get object children', async ( ) => {
|
||||
|
||||
let objs_1 = createAShitTonOfFuckingObjects( 10000, 'noise__' )
|
||||
let objs_1 = createManyObjects( 100, 'noise__' )
|
||||
let ids = await createObjects( objs_1 )
|
||||
|
||||
// let objs_2 = createAShitTonOfFuckingObjects( 20000, 'noise_2' )
|
||||
// let objs_2 = createManyObjects( 20000, 'noise_2' )
|
||||
// let ids2 = await createObjects( objs_2 )
|
||||
|
||||
// let objs_3 = createAShitTonOfFuckingObjects( 50000, 'noise_3' )
|
||||
// let objs_3 = createManyObjects( 100000, 'noise_3' )
|
||||
// let ids3 = await createObjects( objs_3 )
|
||||
|
||||
console.log( `base id is: ${ids[0]} ` )
|
||||
console.log( `base id is: ${ids2[0]} ` )
|
||||
console.log( `base id is: ${ids3[0]} ` )
|
||||
|
||||
|
||||
|
||||
// let { rows } = await getObjectChildren( { objectId: ids[0], select: ['id', 'name', 'sortValueB'] } )
|
||||
// let { rows } = await getObjectChildren( { objectId: ids[ 0 ] } )
|
||||
let limit = 50
|
||||
let { rows: rows_1, cursor: cursor_1 } = await getObjectChildren( { limit, objectId: ids[ 0 ], select: [ 'nest.mallard', 'test.value', 'test.secondValue', 'nest.arr[0]', 'nest.arr[1]' ] } )
|
||||
|
||||
expect( rows_1.length ).to.equal( limit )
|
||||
expect( rows_1[ 0 ] ).to.be.an( 'object' )
|
||||
expect( rows_1[ 0 ] ).to.have.property( 'id' )
|
||||
expect( rows_1[ 0 ] ).to.have.nested.property( 'test.secondValue' )
|
||||
expect( rows_1[ 0 ] ).to.have.nested.property( 'nest.mallard' )
|
||||
|
||||
expect( cursor_1 ).to.be.a( 'string' )
|
||||
|
||||
let { rows: rows_2, cursor: cursor_2 } = await getObjectChildren( { limit, objectId: ids[ 0 ], select: [ 'nest.mallard', 'test.value', 'test.secondValue', 'nest.arr[0]', 'nest.arr[1]' ], cursor: cursor_1 } )
|
||||
|
||||
expect( rows_2.length ).to.equal( 50 )
|
||||
expect( rows_2[ 0 ] ).to.be.an( 'object' )
|
||||
expect( rows_2[ 0 ] ).to.have.property( 'id' )
|
||||
expect( rows_2[ 0 ] ).to.have.nested.property( 'test.secondValue' )
|
||||
expect( rows_2[ 0 ] ).to.have.nested.property( 'nest.mallard' )
|
||||
|
||||
|
||||
let { rows, cursor } = await getObjectChildren( { objectId: ids[ 0 ], limit: 1000 } )
|
||||
expect( rows.length ).to.equal( 100 )
|
||||
|
||||
parentObjectId = ids[ 0 ]
|
||||
|
||||
} ).timeout( 30000 )
|
||||
|
||||
it( 'should query object children', async ( ) => {
|
||||
// we're assuming the prev test objects exist
|
||||
|
||||
let test = await getObjectChildrenQuery( { objectId: parentObjectId, select: [ 'nest.mallard', 'test.value' ] } )
|
||||
|
||||
} )
|
||||
|
||||
} )
|
||||
|
||||
describe( 'Integration (API)', ( ) => {
|
||||
@@ -256,9 +290,9 @@ describe( 'Objects', ( ) => {
|
||||
|
||||
const crypto = require( 'crypto' )
|
||||
|
||||
function createAShitTonOfFuckingObjects( shitTon, noise ) {
|
||||
function createManyObjects( shitTon, noise ) {
|
||||
shitTon = shitTon || 10000
|
||||
noise = noise || Math.random() * 100
|
||||
noise = noise || Math.random( ) * 100
|
||||
|
||||
let objs = [ ]
|
||||
|
||||
@@ -266,13 +300,21 @@ function createAShitTonOfFuckingObjects( shitTon, noise ) {
|
||||
objs.push( base )
|
||||
|
||||
for ( let i = 0; i < shitTon; i++ ) {
|
||||
let baby = { name: `mr. ${i}`, noise: noise, sortValueA: i, sortValueB: i * 0.42 * i }
|
||||
let baby = {
|
||||
name: `mr. ${i}`,
|
||||
nest: { duck: true, mallard: 'false', arr: [ i + 42, i, i ] },
|
||||
test: { value: i, secondValue: 'mallard ' + i % 10 },
|
||||
objArr: [ { a: i }, { b: i * i }, { c: true } ],
|
||||
noise: noise,
|
||||
sortValueA: i,
|
||||
sortValueB: i * 0.42 * i
|
||||
}
|
||||
getAFuckingId( baby )
|
||||
base.__closure[ baby.id ] = 1
|
||||
|
||||
if( i > 1000 )
|
||||
|
||||
if ( i > 1000 )
|
||||
base.__closure[ baby.id ] = i / 1000
|
||||
|
||||
|
||||
objs.push( baby )
|
||||
}
|
||||
|
||||
|
||||
Generated
+5
@@ -3392,6 +3392,11 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
|
||||
},
|
||||
"lodash.set": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
|
||||
"integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM="
|
||||
},
|
||||
"lodash.sortby": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"graphql-tools": "^4.0.7",
|
||||
"knex": "^0.20.12",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"lodash.set": "^4.3.2",
|
||||
"lodash.values": "^4.3.0",
|
||||
"morgan": "^1.10.0",
|
||||
"morgan-debug": "^2.0.0",
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
with objs as (
|
||||
SELECT
|
||||
-- child as id,
|
||||
id,
|
||||
serial_id, -- just for reference
|
||||
"data"
|
||||
FROM object_children_closure
|
||||
JOIN objects ON objects.id = child
|
||||
WHERE parent = '7919a52c017be262ee0daf1844c376d7'
|
||||
AND "minDepth" < 1000
|
||||
-- AND (objects."data" -> 'sortValueA')::numeric <= 700
|
||||
WHERE parent = '89b42e4109f32b20763243d4313e81b5'
|
||||
-- AND "minDepth" < 1000
|
||||
AND (jsonb_path_query("data", '$.sortValueA.test.value'))::numeric <= 10
|
||||
-- AND (objects."data" -> 'sortValueA')::numeric > 100
|
||||
ORDER BY id
|
||||
)
|
||||
SELECT * FROM objs
|
||||
RIGHT JOIN (SELECT count(*) FROM objs ) c(total_count) ON TRUE
|
||||
OFFSET 100
|
||||
LIMIT 200
|
||||
OFFSET 0
|
||||
LIMIT 200
|
||||
|
||||
|
||||
-- with "objs" as (select *) select * from "objs" RIGHT JOIN ( SELECT count(*) FROM "objs" ) c(total_count) ON TRUE limit 50
|
||||
|
||||
Reference in New Issue
Block a user