julesfern / spahql

A query language for Javascript data. Extracted from Spah.
MIT License
325 stars 21 forks source link

Support parent/sibling selection in the query language #1

Open adamschmideg opened 12 years ago

adamschmideg commented 12 years ago

First of all: great work! I like the light-weight database approach and the comprehensive documentation.

I understand it's possible to get the parents of a node:

  child = db.select('/user/handle');
  child.parent(); // -> gives the user node

My feature request is if it's possible to extend the query language so I can write child.select('..') to get the same result.

I also wonder how I could get the previous or next sibling of a node in an array.

julesfern commented 12 years ago

Hey! Very pleasantly surprising to get a feature request before I properly announce the library (I'm working on a decent REPL and API docs before publishing fully).

I actually agree that this would be a neat feature, but it requires some fairly tough changes to the descent operation performed during query operation - while SpahQL currently uses the actual JS call stack for descent, instead we'd have to pass about an array stack which may be popped when encountering a parent operator (".."). I wonder if "+1" would be a valid next-sibling operator as well - what do you think?

// Parent selector
db.select("//status/..") //-> select all parent objects of a "status" key
// Sibling selector
db.select("/timeline/first/+1") //-> equivalent to "/timeline/1"
db.select("/timeline/last/-1") //-> select penultimate status
julesfern commented 12 years ago

Actually, let's revise the sibling selectors. Applying a path component semantic for elements at the same depth seems wildly inconsistent. Here's some possibilities for you to comment on:

db.select("/timeline/first|sibling(+1)") // general pattern of KEY . PIPE . TRAVERSAL_FUNCTION . BRACKET_LEFT . ARGS . BRACKET_RIGHT . FILTER_QUERIES
db.select("/timeline/first+1") // potentially dangerous, as "+1" and "-1" are also valid string object keys
adamschmideg commented 12 years ago

Sorry about my late response. I checked the in-browser REPL which is nice to play with, I also had a look at the source.

I think your PIPE syntax would be great, and it wouldn't break the current operation. Here is my slightly different syntax

db.select("/timeline/first/|next/name") // the next function takes no argument
db.select("/timeline/last/|sibling(-1)") // sibling takes an argument

It could be implemented in a general (an unsafe) way: db.select("/foo/|traversal/bar") would be equivalent with db.select("/foo").traversal().select("/bar"). This would make extension quite easy, the user just has to add their traversal function to the SpahQL prototype. So db.select("/foo/bar").select("|parent") would work out of the box.

julesfern commented 12 years ago

I think including the slash for arbitrary traversal functions isn't quite right - let's assume for a moment that the slash doesn't exist in the language. The query:

/a/b//c

Could be seen as a macro for "descend" and "descendRecursive" traversals:

descend('a')|descend('b')|descendRecursive('c')

However we allow / and // as aliases for baked in descent functions - meaning the slashes have an inherent meaning relating to descent. A pipe is traversal-agnostic and essentially boils down to function-currying:

descend('a')|descend('b')|parent //-> pass output of each function into the next and return the final value
/a/b|parent //-> Truncated version of above

Bear in mind that pipes are not parts of a path component but would actually terminate parsing of the current query and tell the query parser to expect a new path query or traversal function, which will receive the result array from the previous token as input data. Here's an example with filters:

//*[/.type == 'array'] | firstChild //-> Find all arrays and return the first child
//*[/.type == 'array']/0 //-> Equivalent in current syntax

I'm still in two minds about whether or not to actually implement this, but I'm fairly certain the currying pattern is what you're after - which would mean no slashes before pipes.