Closed stemey closed 10 years ago
@stemey i use rql with the Rest store already...
var Query = require('rql/query').Query,
query = '' + new Query().eq(['user', 'id'], id);
store.filter(query).fetch().then( ... );
you can also use the provided rql queryEngine
(https://github.com/SitePen/dstore/blob/master/extensions/rqlQueryEngine.js) with a Memory store.
@neonstalwart that does not seem to solve my problem with gridx and FilteringSelect dijits that don't use RQL. I want to use existing dijits and create a new store. If I consume my own store I am fine without a query abstraction.
i don't know about gridx at all but i was able to previously have FilteringSelect produce an rql query by instantiating the FilteringSelect with queryExpr: "glob:${0}*"
. at that time i was using a dojo/store JsonRest store that was wrapped to change query
so that it produced an rql formatted string as the query. the wrapper would update store.query
and store.queryEngine
so that they understood rql and also translated the usual object queries into rql
e.g.
if (typeof query === 'object') {
for (prop in query) {
if (query.hasOwnProperty(prop)) {
q.push(prop + '=' + query[prop]);
}
}
q = q.join('&');
}
i guess the equivalent place to do this in dstore would be _renderQueryParams
that does not seem to solve my problem with gridx and FilteringSelect dijits that don't use RQL
also, how does dstore fix this since the issue is that these consumers are not using a consistent query format? i agree that a unified query format across all of dojo would be good. i think rql is a really good starting point for that and dstore embraces rql - the next step would seem to be to adjust the consumers to produce queries that are rql.
Surely I can fix every dijit to work with every other store. But that is not the idea of the store api, is it?
I created a generic application whose underlying store should be easily exchangeable - turns out that is harder than I thought. As you said, a query abstraction would be great. Unfortunatly not even the provided standard stores Memory and JsonRest agree on a query format.
I think that dstore is a chance to address this shortcoming.
dstore does include RQL support, but it is not included by default in the base Memory store, to avoid potentially unnecessary modules from being loaded. However, as you point out, this is problem for achieving any type of consistent way of querying beyond simple property equality filtering. Perhaps another idea might be to allow for defining your query format/engine in your call to filter(). What if we allowed this:
require(['dstore/extensions/rqlQueryEngine'], function (rqlQueryEngine) {
var filtered = store.filter('some query', {queryEngine: rqlQueryEngine});
I don't know that this is terribly elegant, it is kind of an uncomfortable tight coupling with a dstore module, but perhaps it is a feasible solution for a more consistent means of advanced store querying.
@kriszyp I don't really understand your idea. My concern is to make it easier to implement a store by making the clients behave in a standard way.
I see the following ways to address this issue:
I am not sure I understand, I thought the goal was to unify querying so that widgets didn't have to worry about different formats, and having two query formats seems to defeat that purpose. Also, I am curious if something like an IndexedDB store would use 'client' or 'server'. It is clearly on the client, but the query mechanism, using indices, is much similar to server stores than the Memory store.
Well, it seems to be unrealistic to have a standard query format. Distinguishing client and server is also not a satisfactory solution.
The idea of a queryBuilder still seems to be a good one. Using a queryBuilder which is retrieved from the store is better than a standard query format because a store does not need to parse the standard query and then create a new query native to its query engine (IndexedDb, InMemory, MongoDb etc.).
The dijit implementors also don't need to worry about different store implementations.
var query = this.store.queryBuilder().like("name","Will*").create();
var results = this.store.filter(query);
rql (almost) already provides what you're looking for.
require([ 'rql/Query' ], function (rqlQuery) {
var Query = rqlQuery.Query,
query = new Query().match('name', 'glob:Will*'),
results = store.filter(query);
});
in order for this to happen i see 3 main points to be addressed
for point 1, rql is a syntax that can express complex queries and has a dsl (via rql/query.Query
) to support building those queries. the one thing that may need to be addressed here is to have more people look at rql and provide their feedback about how the dsl (and the rql syntax) might be improved, add more tests, etc.
for point 2, dstore provides dstore/rqlQueryEngine
to support the ability to use rql as the queryEngine
. of course this doesn't address server-backed stores but those simply depend on having a parser on the server that will parse the serialized query. if you're using JavaScript on your server, rql already provides rql/parser
. i think i might be coming around to the idea of associating the Query
constructor with the queryEngine
so that the dsl can be translated to any query syntax (e.g rql, key-value pairs, mongo, etc) without the consumer needing to know the difference.
the big missing piece is to get store consumers to use a dsl for creating queries. for this you should probably approach @wkeese to see if it's something that could be worked into https://github.com/ibm-js/delite. if he's not happy with the rql dsl then let's find something that everyone would be happy with but i really think that the key to getting what you're looking for lies in finding consensus among the consumers of the store API to agree on a dsl to construct queries - i'm suggesting the rql/query.Query
dsl as a good place to start.
That sounds promising. Can you direct me to the docs (or source code) of the RQL-Querybuilder API (unsuccessfully searched the persrvr/rql repo).
Still the client should retrieve the queryBuilder instance from the store instead of instantiating rql/Query
himself. So there needs to be a function on the store like store.queryBuilder()
.
the readme for rql lists all the operators (https://github.com/persvr/rql)
I think store.Query
would be a good option but the name is a bikeshed.
https://github.com/persvr/rql/blob/master/query.js#L34-L36 also lists all the possible operators
Sorry for my english, guys, but i must comment this.
Here is several facts:
or
not supported): and(or(eq(foo,3),eq(foo,bar)),lt(price,10)))
{ field: 'value' }
So, it's classical problem of object-oriented programming, i think. Here is pseudocode:
// It's about third case (with simple queries).
interface IStore {
function filter(query: SimpleQuery): ExpectedDataFormat;
}
class SimpleStore implements IStore {
function filter(query: SimpleQuery): ExpectedDataFormat {
// do some simple filtering
}
}
// So, all simple widgets (like FilteringSelect) will declare that they expect IStore
class FileringSelect {
store: IStore;
}
// This interface supports second case (advanced conditions but not nested expressions):
interface ISupportAdvancedConditions extends IStore {
// JS allow us to declare "query: AdvancedQuery|SimpleQuery", it's just convention, hehe
// Exptected data format does not change
function filter(query: AdvancedQuery|SimpleQuery): ExpectedDataFormat;
}
// Pseudo implementation
class AdvancedStore extends SimpleStore implements ISupportAdvancedConditions {
function filter(query: AdvancedQuery|SimpleQuery): ExpectedDataFormat {
if (isAdvancedQuery(query)) {
// do some advanced filtering
} else {
return this.inherited(arguments) // SimpleStore#filter
}
}
}
// Or
class AdvancedStore implements ISupportAdvancedConditions {
function filter(query: AdvancedQuery|SimpleQuery): ExpectedDataFormat {
if (isAdvancedQuery(query)) {
// do some advanced filtering
} else {
// do my own simple filtering
}
}
}
// And we can create the "AdvancedStore" and put it to FileringSelect. It will still work
// Similarly with full-rql store
interface ISupportRql extends ISupportAdvancedConditions { /* ... */ }
class RqlStore extends AdvancedStore implements ISupportRql { /* ... */ }
// or with custom implementation
class RqlStore implements ISupportRql { /* ... */ }
// For inmemory store
interface ISupportFunctions { /* ... */ }
class MemoryStore extends SimpleStore implements ISupportFunctions { /* ... */ }
// So, simple widgets does not have to worry about different formats (any store
// can implement third case) but advanced widgets expect advanced stores:
class FileringSelect {
store: IStore;
}
class CoolGridWithConditionalFiltering {
store: IAdvancedStore;
}
class CoolSearchStringWithFullRqlSupport {
store: IRqlStore;
}
// We can now create store for our app
class CrmStore implements ISupportRql {
function filter(query: SimpleQuery|AdvancedQuery|FullRqlQuery): ExpectedDataFormat {
// it may be the same code for all types of query
var queryParams = this.generateQueryParams(query)
return api.call('someMethod', queryParams).then(toExpectedDataFormat)
}
}
var store = new CrmStore(),
filteringSelect = new FilterinSelect({ store: store }),
coolGrid = new CoolGridWithConditionalFiltering({ store: store }),
searchString = new CoolSearchStringWithFullRqlSupport({ store: store })
@anatoliyarkhipov You are right, but type declarations on properties don't exist in javascript. In order to check that the store supports the necessary features the client might check the features one by one as in "duck typing": if (!store.supports('like', 'greaterThan') {throw "wrong store";}
@neonstalwart we just need to confirm the name for the method and a better documentation of the queryBuilder api.
How can I help?
@stemey Yes, here is no type declarations, I talk about conventions. My convention is "every advanced store must support query format of previous store".
This is not necessary if (!store.supports('like', 'greaterThan') {throw "wrong store";}
. You do not check that the store has a "query" method, you just use it. So, why you are going to check supported query type? If you expect a simple store then just use it as a simple store. If your widget will receive an advanced store it will continue to use it as a simple store.
The other approach, which may be more in-line with the current spirit of the dstore API, would be to simply start adding more query methods on the collections. We could add support for things like (and even align it with RQL):
collection.gt('propertyName', minimumValue).lt('propertyName', maxValue).forEach(...);
and:
collection.filter({'priority':'high'}).or(collection.filter({'priority':'critical'})).forEach(...);
This seems like it would be the simplest approach from an API perspective, no need to go through a separate query builder, and detecting support for features is easy (check for the presence of a method). And our query, lazy-execution model would work well with this as well. However, I think the downside (in contrast to the previous suggestion), is that the base memory store has to load a potentially much larger query engine, to support all of this. We could leverage the RQL query engine, since it already has all these query capabilities, but do we want to always load that? Do we want users to opt-in to more sophisticated query capabilities, or provide it in the base stores?
this is an interesting idea.
i don't think that the Memory store would need to have the full support of rql by default. a limited subset that translates to the current capabilities of dstore/objectQueryEngine should be ok and hopefully not increase the size too much.
i want to say that users should opt-in to more sophisticated query capabilities but i know that in my case i'll probably tend to pull in rqlQueryEngine most times since i get a lot of benefit from it so making it opt-in is probably more of a theoretical benefit but i suppose it's worth being conservative in this regard if it helps get support for the idea.
@kriszyp since the possible querying capabilities depend on the queryEngine
being used, how would the store expose the capability of the queryEngine
?
currently a Memory store can be made to understand rql via
// declare store
var RqlMemoryStore = declare(Memory, {
queryEngine: rqlQueryEngine
});
// how can i know the query capabilities of this store?
var store = new RqlMemoryStore();
this doesn't expose the capabilities of the queryEngine
through the store. i tend to think that either a queryEngine
needs to become a mixin
var RqlQueryEngine = declare(null, {
queryEngine: ..., // this would be what is currently dstore/extensions/rqlQueryEngine
Query: ... // this would be rql/query.Query
});
var RqlMemoryStore = declare([ Memory, RqlQueryEngine ]);
var store = new RqlMemoryStore();
// capabilities of the queryEngine can now be determined by methods available
var query = new store.Query();
store.filter(query.eq('foo', 'bar'));
a variation - the queryEngine
becomes a mixin that directly exposes all its capabilities to be mixed into the store
var RqlQueryEngine = declare(null, {
queryEngine: ..., // this would be what is currently dstore/extensions/rqlQueryEngine
eq: ...,
// etc
});
var RqlMemoryStore = declare([ Memory, RqlQueryEngine ]);
var store = new RqlMemoryStore();
// capabilities of the queryEngine can now be determined by methods available on the store
store.filter(store.eq('foo', 'bar'));
or extend the current queryEngine
API to include a Query
property
// declare store
var RqlMemoryStore = declare(Memory, {
queryEngine: rqlQueryEngine // includes a Query property for construction queries
});
var store = new RqlMemoryStore();
var query = new store.queryEngine.Query()
my thinking up to this point had been along the lines of the first option - making queryEngine
a mixin with a Query
property exposed on the store. each have some trade-offs in discoverability and how to compose stores and queries so i'm open to considering the options. i like the idea of seeing if we can go with the current spirit of the dstore API.
Adding the query building method to the collection seems like a good idea.
The 'or'-syntax seems cumbersome in that the original collection needs to be referenced again. Maybe it could be more like this:
collection.filter({'priority':'high'}).or().filter({'priority':'critical'})).forEach(...);
I assume that a query is being constructed until a result-processing method (like forEach
) is called. Is the query reset after that or can I reset the query programmatically to get all data in the collection?
I agree that variant .or(collection.filter(...)).forEach(...)
is not intuitively. I expect an lazy array from filter
method but not query-like object that can be used as argument for or
method.
But @stemey how you intend to implement nested conditions in your variant?
or(and(...), filter)
In a flat style it must be something like this:
.or().and().filter().closeAnd().filter().closeOr().forEach(...)
or shorter:
.or().and().filter()._and().filter()._or().forEach(...)
It looks very strange.
Why we cannot just store a Query constructor in collection?
var q = new collection.Query
collection.filter(q.or(q.gt(...), q.lt(...))).forEach(...)
Hm, you are probably right, it is not feasible to create every possible query with a 'train wreck' :)
As far as I understand Collection.filter
returns a new Collection. Using the proposed method would create a lot of Subcollections. That might be a waste that should be prevented by a queryBuilder as a separate instance.
I think that providing a store.Query() builder/constructor is a reasonable path forward. Any suggestions on what functionality should be included in the base stores (Memory and Rest (which would consist of serialization to query strings))?
Another thought. If we are to truly define a "Query" builder, should Query be defined to include things like sorting (which is certainly part of the resulting query results)? And if so, it seems like we would want to try to make unary query operations symmetrical between the collection and the Query build (available on both), and then the Query should be executed with a query() method. However, if we are actually always passing this constructed thing into filter(), than we should have a "Filter()" builder instead, which I think implies only operations that help create a subset of objects (no mapping, aggregation, etc., as those would belong on the collection, I presume). So are we wanting a "Query" or "Filter" builder?
We should probably have a "Filter" builder then. I think, the sorting can be done via the available sort method.
I agree, we need a "Filter" instead of "Query". It must have the logical and comparison operators, no more. If we will provide a Query builder with other methods (like "sort", "group_by", etc.) then:
function getClosedTasks() {
var f = new tasks.Filter
return tasks.filter(f.eq('status', 'closed'))
}
function getLastClosedTasks() {
return getClosedTasks().sort('-date')
}
getLastClosedTasks().fetchRange(0, 10).then( /* ... */ )
// I can not find a clear way to do something like this if the
// "sort" method will be in an builder.
// It might be .query(...).query(...), but it is confusing me:
// ...
function getLastClosedTasks() {
var q = new Tasks.Query
return getClosedTasks()
.query(q.sort('-date')) // what? why query? i just want sort the tasks
}
// ...
i'm going to say we should still be doing Query. the reason is because of serialization of the query. abstracting the complete serialization of the query seems necessary to gain all the benefits. if we just do Filter then how should we allow for different representations/serializations of sorting and paging? i think that whatever the answer is, it's going to effectively just be a way to implement the difference between Filter and Query so let's just go for Query.
The serialization isn't necessarily the concern of the the filter builder though, is it? Isn't it only the Request store that needs to be responsible for serializing queries (and it can already do this for sorting and paging)?
The original Idea is to provide an api to build queries, that go beyond simple comparisons. I don't see any ambiguities in the sort API. So really we are only talking about filtering.
Reusing the store and exchanging the Filter/Query Builder is not a design goal that I feel is necessary. The builder is provided by the store and an integral part of it.
The way the responsibilities are split between queryBuilder and store is up to the store developer.
it seems we are close to moving on this idea so i'm supportive of going for just Filter in order to move forward. i think that if it becomes evident that there is some reason that it needs to be Query instead then we can consider the options at that time.
I suggest that we also add a "like" expression. That directly maps to queries in SQL, Lucene and others. This is different from "match" which takes a regex pattern as argument. The wildcard should be asteriks or can be defined as another argument. The escaping of the wildcard pattern should probably just be a duplication (e.g. "Will**" matches anything that starts with "Will".)
Also a like expression can be translated to regex but not vice versa.
I guess the only drawback I see with the suggested API in 38bb92e is that currently you can have components (such as the delite(ful) ones) leveraging dstore API without actually requiring dstore to be loaded (for example users that have their own store implementation, as soon as they comply with the dstore API all will be fine).
If the components have to build their queries using a Filter object, then this forces the components to actually loaded dstore/Filter irrespective of whether the user is coming up with is own store not requiring the dstore project. If the syntax was available on the store itself, then the user's store could implement it itself and obviously dstore implementation would just leverage dstore/Filter.
@cjolif I understand the api differently. A dstore implementation provides a Filter constructor in the property Filter. So there can be an individual Filter class for every store class.
// this widget has a store property provided by client code
var filter = new this.store.Filter().eq("name","william");
this.store.filter(filter).forEach(...)
ok, in that case it should be fine. I guess I was fooled by this test case:
https://github.com/SitePen/dstore/blob/38bb92ee9f0dd653d801549b8f0f3e779cfc951a/tests/Memory.js#L58
where the filter is instantiated directly from the dstore/Filter import.
Yes, that test case should be fixed, and I think I did in the query-mixin branch I was considering, but we will get it fixed when we merge. On Jul 8, 2014 7:20 AM, "Christophe Jolif" notifications@github.com wrote:
ok, in that case it should be fine. I guess I was fooled by this test case:
https://github.com/SitePen/dstore/blob/38bb92ee9f0dd653d801549b8f0f3e779cfc951a/tests/Memory.js#L58
where the filter is instantiated directly from the dstore/Filter import.
— Reply to this email directly or view it on GitHub https://github.com/SitePen/dstore/issues/34#issuecomment-48342253.
And back to the like operator, I would like to avoid having multiple like-ish operators. One can certainly translate a subset of regex to the like format (that roughly corresponds to the subset of the regex functionality that is supported by like).
Hi,
I re-read this thread a few times while sick. Here is my 2c -- and that's how I 'fixed" the issue on my end.
I frankly think that asking the clients to do anything more than simple URL fields (the way it is now) will basically lead to very complex code on the client side. The beauty of stores is that they can be consumed pretty much by anybody; the fact that you can only pass key/value to them for filtering, to me, is a plus.
What a bout complex queries?
This is the way I deal with this on my application (which isn't even released yet); the server defines stores as ever, using JsonRestStores (which uses callbacks rather than promises, wooops :D ). Say that you have a table with:
name: string(30)
surname: string(60)
Now: the "default" search fields for this, server side, are "name" and "surname", and they need to match 100%. This means that if you call the store /some/path/to/store?name=tony
, it will only return records where the name is exactly Tony
.
However, in your server, you can define a searchSchema
:
var UsersInfo = declare( [ HotStore, MultiHomePermsMixin, PrivateUserDataMixin ], {
schema: new HotSchema({
aredValidator: 'email', trim: 70, min: 4 }, /surname : { type: 'string', required: true, default: "Your surname", notEmpty: true}, /name : { type: 'string', required: true, default: "Your name", notEmpty: true }, }),
onlineSearchSchema: new Schema({
name : { type: 'string', trim: 20, searchable: true, searchOptions: { type: 'is' } },
surname : { type: 'string', trim: 20, searchable: true, searchOptions: { type: 'is' } },
nameContains : { type: 'string', trim: 4, searchable: true, searchOptions: { type: 'contains', field: 'name' } },
surnameContains : { type: 'string', trim: 4, searchable: true, searchOptions: { type: 'contains', field: 'surname' } },
surnameStartsWith: { type: 'string', trim: 4, searchable: true, searchOptions: { type: 'startsWith', field: 'surname' } },
nameOrSurnameStartsWith: { type: 'string', trim: 4, searchable: true, searchOptions: [ { field: 'surname', type: 'startsWith', condition: 'or' }, { field: 'name', type: 'startsWith', condition: 'or' } ] },
}),
handlePut: true,
handleGet: true,
storeName: 'usersInfo',
publicURL: '/config/users/:userId',
});
Basically, I define a set of "custom" search fields, each one with a specific meaning in terms of what it searches.
The painful part is the one where you need memory's queryEngine to implement these client-side. However, I honestly think that when using Memory as Cache to Json Rest, you nearly always:
Please note that while the server side is already done and dusted, the client side isn't. I was about to get into it a few weeks back, and then I got derailed by the whole beforeId
thing in stores, as well as this very thread which I was observing.
My point is that the client side would benefit by being easy... super easy. It should never pass a complex query over, and expect the server to always deal with it. Instead, it should simply pass simple simple key/value fields -- and those keys can have a very loaded meaning, and even triggered very complex queries on the server -- with the client living in blissful ignorance of how complex things are.
Sorry about the overly long message -- just my 2c.
Having said all this, I might be completely full of crap and my solution might be completely ass. How ready is the query-mixin branch, Kris? Does it at least "kind of" work? (If it does, and if that's the direction dstore is going, I will follow right through and scrap the code on JsonRestStores for the "fancy filtering" fields, and embrace query-mixin now that I am in dstore-land...)
Here is the branch with the filtering building functionality if anyone wants to take a look before I potentially merge it: https://github.com/SitePen/dstore/tree/filter-builder
I really, really wants to see how this works before I make any (hopefully meaningful) comments. But I need to know how to use the thing! So...
Sorry, this is probably something I should know already...
On 29 August 2014 23:55, Kris Zyp notifications@github.com wrote:
Here is the branch with the filtering building functionality if anyone wants to take a look before I potentially merge it: https://github.com/SitePen/dstore/tree/filter-builder
— Reply to this email directly or view it on GitHub https://github.com/SitePen/dstore/issues/34#issuecomment-53894747.
@mercmobily - comparing actually hard since the branch isn't rebased against the latest master, but https://github.com/Sitepen/dstore/compare/f34284a...filter-builder seems to work.
Thanks Kris. Two questions:
1) With this implementation, how do .and() and .or() actually work in terms of syntax? It's been discussed here, but I can't figure it out from the code.
2) At this point in my JsonRestStores server module I need to implement RQL, and also need to get specific layers (MongoDB, MySQL) to translate RQL to DB-specific queries. Is that right?
On 30 August 2014 06:58, Bill Keese notifications@github.com wrote:
@mercmobily https://github.com/mercmobily - comparing actually hard since the branch isn't rebased against the latest master, but f34284a...filter-builder https://github.com/SitePen/dstore/compare/f34284a...filter-builder seems to work.
— Reply to this email directly or view it on GitHub https://github.com/SitePen/dstore/issues/34#issuecomment-53938885.
@mercmobily
filter.ne('id', 2).or(filter.eq('foo', true), filter.eq('foo'));
As @anatoliyarkhipov pointed out, translation can occur client, side. The serialization of the filter can be customized by overriding _rendreFilterParams (from dstore/Request).
I implemented a server-side dojo/store which connects to a MongoDB. The most difficult part was actually transforming the query passed to
query()
into a mongoDB query. In my opinion this is difficult because of the lack of a query abstraction in dojo/store.Actually I had to adapt my store implementation to the clients using the store. Specifically diji/form/FilteringSelect and gridx. Both have different ways to build queries. Looking at the comments in dijit/form/FilteringSelect the lack of abstraction becomes evident. The comment mentions two Stores that were specifically handled (JsonRest and Memory). The problem arises because a like-query is not standardized. Similary Gridx creates very complex queries with logical operators which cannot be expressed by simple http-parameters
I suggest you introduce a query abstraction like rql or MongoDb's query in dstore. That makes store and client implementors' lifes easier.