Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for ORDER BY #4

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 53 additions & 10 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ function token(name, value) {
return { name, value };
}

lexer.addRule(/order by\b/, (l) => token('ORDER BY', l));
lexer.addRule(whitespaceRegex, () => { /* ignore whitespace */ });
lexer.addRule(/\./, (l) => token('DOT', l));
lexer.addRule(/,/, (l) => token('COMMA', l));
Expand Down Expand Up @@ -62,6 +63,11 @@ lexer.addRule(
new RegExp(`None${reNotFollowedByName}`),
(l) => token('NONE', l),
);
lexer.addRule(new RegExp(`asc${reNotFollowedByName}`), (l) => token('ASC', l));
lexer.addRule(
new RegExp(`desc${reNotFollowedByName}`),
(l) => token('DESC', l)
);
lexer.addRule(nameRegex, (l) => token('NAME', l));
lexer.addRule(
stringRegex,
Expand Down Expand Up @@ -631,17 +637,31 @@ DjangoQL.prototype = {
getContext(text, cursorPos) {
// This function returns an object with the following 4 properties:
let prefix; // text already entered by user in the current scope
let scope = null; // 'field', 'comparison', 'value', 'logical' or null
let scope = null; // 'field', 'comparison', 'value', 'logical',
// 'sortdir', or null
let model = null; // model, set for 'field', 'comparison' and 'value'
let field = null; // field, set for 'comparison' and 'value'
// Stack of models that includes all entered models
let modelStack = [this.currentModel];
let nestingLevel = 0;
let queryPart = 'expression'; // poor man's "grammar": we are either
// within the 'expression' part of the query,
// or within the 'ordering' part

let nameParts;
let resolvedName;
let lastToken = null;
let nextToLastToken = null;
const tokens = this.lexer.setInput(text.slice(0, cursorPos)).lexAll();
tokens.forEach(t => {
if (t.name === 'ORDER BY') {
queryPart = 'ordering';
} else if (t.name === 'PAREN_L') {
nestingLevel++;
} else if (t.name == 'PAREN_R') {
nestingLevel--;
}
});
const allTokens = this.lexer.setInput(text).lexAll();
let currentFullToken = null;
if (tokens.length && tokens[tokens.length - 1].end >= cursorPos) {
Expand Down Expand Up @@ -674,12 +694,14 @@ DjangoQL.prototype = {
const logicalTokens = ['AND', 'OR'];
if (prefix === ')' && !whitespace) {
// Nothing to suggest right after right paren
} else if (!lastToken
|| (logicalTokens.indexOf(lastToken.name) >= 0 && whitespace)
|| (prefix === '.' && lastToken && !whitespace)
|| (lastToken.name === 'PAREN_L'
&& (!nextToLastToken
|| logicalTokens.indexOf(nextToLastToken.name) >= 0))) {
} else if ((queryPart === 'expression' && (!lastToken
|| (logicalTokens.indexOf(lastToken.name) >= 0 && whitespace)
|| (prefix === '.' && lastToken && !whitespace)
|| (lastToken.name === 'PAREN_L'
&& (!nextToLastToken
|| logicalTokens.indexOf(nextToLastToken.name) >= 0))))
|| (queryPart === 'ordering' && lastToken
&& (lastToken.name === 'ORDER BY' || lastToken.name === 'COMMA'))) {
scope = 'field';
model = this.currentModel;
if (prefix === '.') {
Expand All @@ -702,7 +724,8 @@ DjangoQL.prototype = {
model = null;
}
}
} else if (lastToken
} else if (queryPart === 'expression'
&& lastToken
&& whitespace
&& nextToLastToken
&& nextToLastToken.name === 'NAME'
Expand All @@ -719,19 +742,24 @@ DjangoQL.prototype = {
prefix = prefix.slice(1);
}
}
} else if (lastToken && whitespace && lastToken.name === 'NAME') {
} else if (queryPart === 'expression'
&& lastToken && whitespace && lastToken.name === 'NAME') {
resolvedName = this.resolveName(lastToken.value);
if (resolvedName.model) {
scope = 'comparison';
model = resolvedName.model;
field = resolvedName.field;
modelStack = resolvedName.modelStack;
}
} else if (lastToken
} else if (queryPart === 'expression'
&& lastToken
&& whitespace
&& ['PAREN_R', 'INT_VALUE', 'FLOAT_VALUE', 'STRING_VALUE']
.indexOf(lastToken.name) >= 0) {
scope = 'logical';
} else if (queryPart === 'ordering'
&& lastToken && lastToken.name === 'NAME') {
scope = 'sortdir';
}

return {
Expand All @@ -741,6 +769,8 @@ DjangoQL.prototype = {
field,
currentFullToken,
modelStack,
queryPart,
nestingLevel,
};
},

Expand Down Expand Up @@ -928,6 +958,9 @@ DjangoQL.prototype = {
}).map((f) => (
suggestion(f, '', model[f].type === 'relation' ? '.' : ' ')
));
if (context.queryPart === 'expression' && context.nestingLevel === 0) {
this.suggestions.push(suggestion("order by", "", " "));
}
break;

case 'comparison':
Expand Down Expand Up @@ -1007,8 +1040,18 @@ DjangoQL.prototype = {
suggestion('and', '', ' '),
suggestion('or', '', ' '),
];
if (context.nestingLevel === 0) {
this.suggestions.push(suggestion('order by', '', ' '));
}
break;

case 'sortdir':
this.suggestions = [
suggestion('asc', '', ' '),
suggestion('desc', '', ' ')
];
break;

default:
this.prefix = '';
this.suggestions = [];
Expand Down
79 changes: 75 additions & 4 deletions tests/DjangoQL.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,23 +139,24 @@ describe('test DjangoQL completion', () => {
token('CONTAINS', '~'),
token('NOT_CONTAINS', '!~'),
];
djangoQL.lexer.setInput('() ., = != >\t >= < <= ~ !~');
djangoQL.lexer.setInput('() ., = != >\t >= < <= ~ !~ ');
tokens.forEach((t) => {
expect(djangoQL.lexer.lex()).toStrictEqual(t);
});
expect(djangoQL.lexer.lex()).toBeFalsy(); // end of input
});

it('should recognize names', () => {
const names = ['a', 'myVar_42', '__LOL__', '_', '_0'];
const names = ['a', 'myVar_42', '__LOL__', '_', '_0', 'order'];
djangoQL.lexer.setInput(names.join(' '));
names.forEach((name) => {
expect(djangoQL.lexer.lex()).toStrictEqual(token('NAME', name));
});
});

it('should recognize reserved words', () => {
const words = ['True', 'False', 'None', 'or', 'and', 'in'];
const words = ['True', 'False', 'None', 'or', 'and', 'in',
'order by', 'asc', 'desc'];
djangoQL.lexer.setInput(words.join(' '));
words.forEach((word) => {
expect(djangoQL.lexer.lex())
Expand Down Expand Up @@ -257,7 +258,7 @@ describe('test DjangoQL completion', () => {
});

describe('.getScope()', () => {
it('should properly detect scope and prefix', () => {
it('should properly detect scope, prefix, nesting level, and query part', () => {
const book = djangoQL.currentModel;
const examples = [
{
Expand All @@ -267,6 +268,8 @@ describe('test DjangoQL completion', () => {
scope: 'field',
model: book,
field: null,
nestingLevel: 0,
queryPart: 'expression',
},
},
{
Expand All @@ -276,6 +279,8 @@ describe('test DjangoQL completion', () => {
scope: 'field',
model: book,
field: null,
nestingLevel: 0,
queryPart: 'expression',
},
},
{
Expand All @@ -285,6 +290,8 @@ describe('test DjangoQL completion', () => {
scope: 'field',
model: book,
field: null,
nestingLevel: 0,
queryPart: 'expression',
},
},
{
Expand All @@ -294,6 +301,8 @@ describe('test DjangoQL completion', () => {
scope: 'field',
model: book,
field: null,
nestingLevel: 0,
queryPart: 'expression',
},
},
{
Expand All @@ -303,6 +312,8 @@ describe('test DjangoQL completion', () => {
scope: 'field',
model: book,
field: null,
nestingLevel: 0,
queryPart: 'expression',
},
},
{
Expand All @@ -312,6 +323,8 @@ describe('test DjangoQL completion', () => {
scope: 'comparison',
model: book,
field: 'id',
nestingLevel: 0,
queryPart: 'expression',
},
},
{
Expand All @@ -321,6 +334,8 @@ describe('test DjangoQL completion', () => {
scope: 'comparison',
model: book,
field: 'id',
nestingLevel: 0,
queryPart: 'expression',
},
},
{
Expand All @@ -330,6 +345,8 @@ describe('test DjangoQL completion', () => {
scope: 'value',
model: book,
field: 'id',
nestingLevel: 0,
queryPart: 'expression',
},
},
{
Expand All @@ -339,6 +356,8 @@ describe('test DjangoQL completion', () => {
scope: 'value',
model: book,
field: 'id',
nestingLevel: 0,
queryPart: 'expression',
},
},
{
Expand All @@ -348,6 +367,8 @@ describe('test DjangoQL completion', () => {
scope: 'logical',
model: null,
field: null,
nestingLevel: 0,
queryPart: 'expression',
},
},
{
Expand All @@ -357,6 +378,8 @@ describe('test DjangoQL completion', () => {
scope: 'logical',
model: null,
field: null,
nestingLevel: 0,
queryPart: 'expression',
},
},
{
Expand All @@ -366,6 +389,8 @@ describe('test DjangoQL completion', () => {
scope: 'field',
model: book,
field: null,
nestingLevel: 0,
queryPart: 'expression',
},
},
{
Expand All @@ -375,6 +400,8 @@ describe('test DjangoQL completion', () => {
scope: 'field',
model: 'auth.user',
field: null,
nestingLevel: 0,
queryPart: 'expression',
},
},
{
Expand All @@ -384,6 +411,8 @@ describe('test DjangoQL completion', () => {
scope: 'field',
model: 'auth.user',
field: null,
nestingLevel: 0,
queryPart: 'expression',
},
},
{
Expand All @@ -393,6 +422,8 @@ describe('test DjangoQL completion', () => {
scope: 'logical',
model: null,
field: null,
nestingLevel: 0,
queryPart: 'expression',
},
},
{
Expand All @@ -402,6 +433,8 @@ describe('test DjangoQL completion', () => {
scope: 'logical',
model: null,
field: null,
nestingLevel: 0,
queryPart: 'expression',
},
},
{
Expand All @@ -411,6 +444,8 @@ describe('test DjangoQL completion', () => {
scope: 'field',
model: book,
field: null,
nestingLevel: 1,
queryPart: 'expression',
},
},
{
Expand All @@ -420,6 +455,42 @@ describe('test DjangoQL completion', () => {
scope: 'field',
model: book,
field: null,
nestingLevel: 1,
queryPart: 'expression',
},
},
{
args: ['order by foo', 7], // cursor is inside the `order by` clause
result: {
prefix: 'b',
scope: null,
model: null,
field: null,
nestingLevel: 0,
queryPart: 'expression',
},
},
{
args: ['order by foo', 10], // cursor is in field after `order by`
result: {
prefix: 'f',
scope: 'field',
model: book,
field: null,
nestingLevel: 0,
queryPart: 'ordering',
},
},
{
// cursor is in field after `order by`
args: ['id > 10 order by id desc', 7],
result: {
prefix: '10',
scope: 'value',
model: book,
field: 'id',
nestingLevel: 0,
queryPart: 'expression',
},
},
];
Expand Down