blob: abd50d6cd6d680b4794e117655dfbaa7f6b9588f [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ColumnMetadata } from '../utils';
import { getPossibleSqlReferences, getSqlCompletions, getSqlLiterals } from './sql-completions';
describe('sql-completions', () => {
describe('getSqlCompletions', () => {
const baseOptions = {
allText: '',
lineBeforePrefix: '',
charBeforePrefix: '',
prefix: '',
};
it('returns empty array when in a single line comment', () => {
const completions = getSqlCompletions({
...baseOptions,
lineBeforePrefix: '-- This is a comment',
prefix: 'SEL',
});
expect(completions).toEqual([]);
});
it('returns empty array when typing after comment marker', () => {
const completions = getSqlCompletions({
...baseOptions,
lineBeforePrefix: 'SELECT * FROM table -- ',
prefix: 'com',
});
expect(completions).toEqual([]);
});
it('returns empty array when comment is in middle of line', () => {
const completions = getSqlCompletions({
...baseOptions,
lineBeforePrefix: 'SELECT -- some comment',
prefix: 'FR',
});
expect(completions).toEqual([]);
});
it('returns empty array when prefix is a number', () => {
const completions = getSqlCompletions({
...baseOptions,
prefix: '123',
});
expect(completions).toEqual([]);
});
it('returns completions before comment marker on same line', () => {
const completions = getSqlCompletions({
...baseOptions,
lineBeforePrefix: 'SELECT ',
prefix: 'FR',
});
expect(completions.length).toBeGreaterThan(0);
const keywordCompletions = completions.filter(c => c.meta === 'keyword');
expect(keywordCompletions.length).toBeGreaterThan(0);
});
it('returns empty array even when comment contains SQL keywords', () => {
const completions = getSqlCompletions({
...baseOptions,
lineBeforePrefix: '-- TODO: need to SELECT * FROM',
prefix: 'WHE',
});
expect(completions).toEqual([]);
});
it('returns empty array for indented comments', () => {
const completions = getSqlCompletions({
...baseOptions,
lineBeforePrefix: ' -- indented comment',
prefix: 'FRO',
});
expect(completions).toEqual([]);
});
it('returns only literals when charBeforePrefix is single quote', () => {
const completions = getSqlCompletions({
...baseOptions,
allText: "SELECT * FROM table WHERE country = 'France' OR country = 'Germany'",
charBeforePrefix: "'",
prefix: 'Fr',
});
expect(completions).toHaveLength(2);
expect(completions[0]).toEqual({
value: 'France',
score: 1,
meta: 'local',
});
expect(completions[1]).toEqual({
value: 'Germany',
score: 1,
meta: 'local',
});
});
it('returns keyword suggestions after SELECT', () => {
const completions = getSqlCompletions({
...baseOptions,
lineBeforePrefix: 'SELECT',
});
const keywordCompletions = completions.filter(c => c.meta === 'keyword');
const keywordValues = keywordCompletions.map(c => c.value);
expect(keywordValues).toContain('DISTINCT');
expect(keywordValues).toContain('ALL');
expect(keywordValues).not.toContain('SELECT');
});
it('does not include functions after keywords that cannot be followed by functions', () => {
const completions = getSqlCompletions({
...baseOptions,
lineBeforePrefix: 'LIMIT',
});
const functionCompletions = completions.filter(c => c.meta === 'function');
expect(functionCompletions).toHaveLength(0);
const dynamicCompletions = completions.filter(c => c.meta === 'dynamic');
expect(dynamicCompletions).toHaveLength(0);
});
it('includes functions after keywords that can be followed by functions', () => {
const completions = getSqlCompletions({
...baseOptions,
lineBeforePrefix: 'WHERE',
});
const functionCompletions = completions.filter(c => c.meta === 'function');
expect(functionCompletions.length).toBeGreaterThan(0);
const dynamicCompletions = completions.filter(c => c.meta === 'dynamic');
expect(dynamicCompletions.length).toBeGreaterThan(0);
});
it('does not include column references after keywords that cannot be followed by refs', () => {
const columnMetadata: ColumnMetadata[] = [
{
TABLE_SCHEMA: 'druid',
TABLE_NAME: 'wikipedia',
COLUMN_NAME: 'page',
DATA_TYPE: 'VARCHAR',
},
{
TABLE_SCHEMA: 'druid',
TABLE_NAME: 'wikipedia',
COLUMN_NAME: 'user',
DATA_TYPE: 'VARCHAR',
},
];
const completions = getSqlCompletions({
...baseOptions,
lineBeforePrefix: 'DESC',
columnMetadata,
});
const columnCompletions = completions.filter(c => c.meta === 'column');
expect(columnCompletions).toHaveLength(0);
const tableCompletions = completions.filter(c => c.meta === 'table');
expect(tableCompletions).toHaveLength(0);
});
it('includes column references after keywords that can be followed by refs', () => {
const columnMetadata: ColumnMetadata[] = [
{
TABLE_SCHEMA: 'druid',
TABLE_NAME: 'wikipedia',
COLUMN_NAME: 'page',
DATA_TYPE: 'VARCHAR',
},
{
TABLE_SCHEMA: 'druid',
TABLE_NAME: 'wikipedia',
COLUMN_NAME: 'user',
DATA_TYPE: 'VARCHAR',
},
];
const completions = getSqlCompletions({
...baseOptions,
allText: 'SELECT * FROM "wikipedia"',
lineBeforePrefix: 'WHERE',
columnMetadata,
});
const columnCompletions = completions.filter(c => c.meta === 'column');
expect(columnCompletions.length).toBeGreaterThan(0);
expect(columnCompletions.map(c => c.value)).toContain('"page"');
expect(columnCompletions.map(c => c.value)).toContain('"user"');
});
it('includes context keys after SET keyword', () => {
const completions = getSqlCompletions({
...baseOptions,
lineBeforePrefix: 'SET',
});
const contextCompletions = completions.filter(c => c.meta === 'context');
expect(contextCompletions.length).toBeGreaterThan(0);
});
it('handles column names correctly with quotes', () => {
const columns = ['column name with spaces', 'normal_column'];
const completionsWithoutQuote = getSqlCompletions({
...baseOptions,
columns,
});
const completionsWithQuote = getSqlCompletions({
...baseOptions,
charBeforePrefix: '"',
columns,
});
const columnWithoutQuote = completionsWithoutQuote.find(c =>
c.value.includes('column name with spaces'),
);
expect(columnWithoutQuote?.value).toBe('"column name with spaces"');
const columnWithQuote = completionsWithQuote.find(c => c.value === 'column name with spaces');
expect(columnWithQuote?.value).toBe('column name with spaces');
});
it('includes constants and data types in completions', () => {
const completions = getSqlCompletions(baseOptions);
const constantCompletions = completions.filter(c => c.meta === 'constant');
expect(constantCompletions.map(c => c.value)).toContain('NULL');
expect(constantCompletions.map(c => c.value)).toContain('TRUE');
expect(constantCompletions.map(c => c.value)).toContain('FALSE');
const typeCompletions = completions.filter(c => c.meta === 'type');
expect(typeCompletions.length).toBeGreaterThan(0);
});
it('includes local references from the query text', () => {
const completions = getSqlCompletions({
...baseOptions,
allText: 'SELECT my_column FROM my_table WHERE other_column = 1',
});
const localCompletions = completions.filter(c => c.meta === 'local');
const localValues = localCompletions.map(c => c.value);
expect(localValues).toContain('my_column');
expect(localValues).toContain('my_table');
expect(localValues).toContain('other_column');
});
});
describe('getSqlLiterals', () => {
it('extracts string literals from SQL text', () => {
const sqlText =
"SELECT * FROM table WHERE country = 'France' OR city = 'Paris' OR code = 'FR'";
const literals = getSqlLiterals(sqlText, 10);
expect(literals).toEqual(['France', 'Paris', 'FR']);
});
it('respects maxWords limit', () => {
const sqlText = "SELECT 'one', 'two', 'three', 'four', 'five'";
const literals = getSqlLiterals(sqlText, 3);
expect(literals).toHaveLength(3);
});
});
describe('getPossibleSqlReferences', () => {
it('extracts quoted identifiers', () => {
const sqlText = 'SELECT "myColumn" FROM "myTable"';
const references = getPossibleSqlReferences(sqlText, 10);
expect(references).toContain('myColumn');
expect(references).toContain('myTable');
});
it('extracts unquoted identifiers', () => {
const sqlText = 'SELECT column1, column2 FROM table1 WHERE column3 = 1';
const references = getPossibleSqlReferences(sqlText, 10);
expect(references).toContain('column1');
expect(references).toContain('column2');
expect(references).toContain('table1');
expect(references).toContain('column3');
});
it('filters out known SQL keywords', () => {
const sqlText = 'SELECT column1 FROM table1 WHERE column2 = 1 ORDER BY column3';
const references = getPossibleSqlReferences(sqlText, 20);
expect(references).not.toContain('SELECT');
expect(references).not.toContain('FROM');
expect(references).not.toContain('WHERE');
expect(references).not.toContain('ORDER');
expect(references).not.toContain('BY');
});
it('handles identifiers in expressions', () => {
const sqlText = 'SELECT (column1 + column2) * column3 FROM table1';
const references = getPossibleSqlReferences(sqlText, 10);
expect(references).toContain('column1');
expect(references).toContain('column2');
expect(references).toContain('column3');
expect(references).toContain('table1');
});
});
});