first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
import type { CompletionResult, CompletionSource } from '@codemirror/autocomplete';
|
||||
import { CompletionContext } from '@codemirror/autocomplete';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
|
||||
import type { SQLConfig } from '../src/sql';
|
||||
import { MySQL, PostgreSQL, schemaCompletionSource } from '../src/sql';
|
||||
|
||||
function get(doc: string, conf: SQLConfig & { explicit?: boolean } = {}) {
|
||||
const cur = doc.indexOf('|');
|
||||
const dialect = conf.dialect || PostgreSQL;
|
||||
doc = doc.slice(0, cur) + doc.slice(cur + 1);
|
||||
|
||||
const state = EditorState.create({
|
||||
doc,
|
||||
selection: { anchor: cur },
|
||||
extensions: [
|
||||
dialect,
|
||||
dialect.sqlLanguage.data.of({
|
||||
autocomplete: schemaCompletionSource(Object.assign({ dialect }, conf)),
|
||||
}),
|
||||
],
|
||||
});
|
||||
const result = state.languageDataAt<CompletionSource>('autocomplete', cur)[0](
|
||||
new CompletionContext(state, cur, !!conf.explicit),
|
||||
);
|
||||
return result as CompletionResult | null;
|
||||
}
|
||||
|
||||
function str(result: CompletionResult | null) {
|
||||
return !result
|
||||
? ''
|
||||
: result.options
|
||||
.slice()
|
||||
.sort((a, b) => (b.boost || 0) - (a.boost || 0) || (a.label < b.label ? -1 : 1))
|
||||
.map((o) => o.label)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
const schema1 = {
|
||||
users: ['name', 'id', 'address'],
|
||||
products: ['name', 'cost', 'description'],
|
||||
};
|
||||
|
||||
const schema2 = {
|
||||
'public.users': ['email', 'id'],
|
||||
'other.users': ['name', 'id'],
|
||||
};
|
||||
|
||||
describe('SQL completion', () => {
|
||||
it('completes table names', () => {
|
||||
expect(str(get('select u|', { schema: schema1 }))).toEqual('products, users');
|
||||
});
|
||||
|
||||
it('completes quoted table names', () => {
|
||||
expect(str(get('select "u|', { schema: schema1 }))).toEqual('"products", "users"');
|
||||
});
|
||||
|
||||
it('completes table names under schema', () => {
|
||||
expect(str(get('select public.u|', { schema: schema2 }))).toEqual('users');
|
||||
});
|
||||
|
||||
it('completes quoted table names under schema', () => {
|
||||
expect(str(get('select public."u|', { schema: schema2 }))).toEqual('"users"');
|
||||
});
|
||||
|
||||
it('completes quoted table names under quoted schema', () => {
|
||||
expect(str(get('select "public"."u|', { schema: schema2 }))).toEqual('"users"');
|
||||
});
|
||||
|
||||
it('completes column names', () => {
|
||||
expect(str(get('select users.|', { schema: schema1 }))).toEqual('address, id, name');
|
||||
});
|
||||
|
||||
it('completes quoted column names', () => {
|
||||
expect(str(get('select users."|', { schema: schema1 }))).toEqual('"address", "id", "name"');
|
||||
});
|
||||
|
||||
it('completes column names in quoted tables', () => {
|
||||
expect(str(get('select "users".|', { schema: schema1 }))).toEqual('address, id, name');
|
||||
});
|
||||
|
||||
it('completes column names in tables for a specific schema', () => {
|
||||
expect(str(get('select public.users.|', { schema: schema2 }))).toEqual('email, id');
|
||||
expect(str(get('select other.users.|', { schema: schema2 }))).toEqual('id, name');
|
||||
});
|
||||
|
||||
it('completes quoted column names in tables for a specific schema', () => {
|
||||
expect(str(get('select public.users."|', { schema: schema2 }))).toEqual('"email", "id"');
|
||||
expect(str(get('select other.users."|', { schema: schema2 }))).toEqual('"id", "name"');
|
||||
});
|
||||
|
||||
it('completes column names in quoted tables for a specific schema', () => {
|
||||
expect(str(get('select public."users".|', { schema: schema2 }))).toEqual('email, id');
|
||||
expect(str(get('select other."users".|', { schema: schema2 }))).toEqual('id, name');
|
||||
});
|
||||
|
||||
it('completes column names in quoted tables for a specific quoted schema', () => {
|
||||
expect(str(get('select "public"."users".|', { schema: schema2 }))).toEqual('email, id');
|
||||
expect(str(get('select "other"."users".|', { schema: schema2 }))).toEqual('id, name');
|
||||
});
|
||||
|
||||
it('completes quoted column names in quoted tables for a specific quoted schema', () => {
|
||||
expect(str(get('select "public"."users"."|', { schema: schema2 }))).toEqual('"email", "id"');
|
||||
expect(str(get('select "other"."users"."|', { schema: schema2 }))).toEqual('"id", "name"');
|
||||
});
|
||||
|
||||
it('completes column names of aliased tables', () => {
|
||||
expect(str(get('select u.| from users u', { schema: schema1 }))).toEqual('address, id, name');
|
||||
expect(str(get('select u.| from users as u', { schema: schema1 }))).toEqual(
|
||||
'address, id, name',
|
||||
);
|
||||
expect(
|
||||
str(get('select u.| from (SELECT * FROM something u) join users u', { schema: schema1 })),
|
||||
).toEqual('address, id, name');
|
||||
expect(str(get('select * from users u where u.|', { schema: schema1 }))).toEqual(
|
||||
'address, id, name',
|
||||
);
|
||||
expect(str(get('select * from users as u where u.|', { schema: schema1 }))).toEqual(
|
||||
'address, id, name',
|
||||
);
|
||||
expect(
|
||||
str(
|
||||
get('select * from (SELECT * FROM something u) join users u where u.|', {
|
||||
schema: schema1,
|
||||
}),
|
||||
),
|
||||
).toEqual('address, id, name');
|
||||
});
|
||||
|
||||
it('completes column names of aliased quoted tables', () => {
|
||||
expect(str(get('select u.| from "users" u', { schema: schema1 }))).toEqual('address, id, name');
|
||||
expect(str(get('select u.| from "users" as u', { schema: schema1 }))).toEqual(
|
||||
'address, id, name',
|
||||
);
|
||||
expect(str(get('select * from "users" u where u.|', { schema: schema1 }))).toEqual(
|
||||
'address, id, name',
|
||||
);
|
||||
expect(str(get('select * from "users" as u where u.|', { schema: schema1 }))).toEqual(
|
||||
'address, id, name',
|
||||
);
|
||||
});
|
||||
|
||||
it('completes column names of aliased tables for a specific schema', () => {
|
||||
expect(str(get('select u.| from public.users u', { schema: schema2 }))).toEqual('email, id');
|
||||
});
|
||||
|
||||
it('completes column names in aliased quoted tables for a specific schema', () => {
|
||||
expect(str(get('select u.| from public."users" u', { schema: schema2 }))).toEqual('email, id');
|
||||
});
|
||||
|
||||
it('completes column names in aliased quoted tables for a specific quoted schema', () => {
|
||||
expect(str(get('select u.| from "public"."users" u', { schema: schema2 }))).toEqual(
|
||||
'email, id',
|
||||
);
|
||||
});
|
||||
|
||||
it('completes aliased table names', () => {
|
||||
expect(str(get('select a| from a.b as ab join auto au', { schema: schema2 }))).toEqual(
|
||||
'ab, au, other, public',
|
||||
);
|
||||
});
|
||||
|
||||
it('includes closing quote in completion', () => {
|
||||
const r = get('select "u|"', { schema: schema1 });
|
||||
expect(r!.to).toEqual(10);
|
||||
});
|
||||
|
||||
it('keeps extra table completion properties', () => {
|
||||
const r = get('select u|', {
|
||||
schema: { users: ['id'] },
|
||||
tables: [{ label: 'users', type: 'keyword' }],
|
||||
});
|
||||
expect(r!.options[0].type).toEqual('keyword');
|
||||
});
|
||||
|
||||
it('keeps extra column completion properties', () => {
|
||||
const r = get('select users.|', { schema: { users: [{ label: 'id', type: 'keyword' }] } });
|
||||
expect(r!.options[0].type).toEqual('keyword');
|
||||
});
|
||||
|
||||
it('supports a default table', () => {
|
||||
expect(str(get('select i|', { schema: schema1, defaultTable: 'users' }))).toEqual(
|
||||
'address, id, name, products, users',
|
||||
);
|
||||
});
|
||||
|
||||
it('supports alternate quoting styles', () => {
|
||||
expect(str(get('select `u|', { dialect: MySQL, schema: schema1 }))).toEqual(
|
||||
'`products`, `users`',
|
||||
);
|
||||
});
|
||||
|
||||
it("doesn't complete without identifier", () => {
|
||||
expect(str(get('select |', { schema: schema1 }))).toEqual('');
|
||||
});
|
||||
|
||||
it('does complete explicitly without identifier', () => {
|
||||
expect(str(get('select |', { schema: schema1, explicit: true }))).toEqual('products, users');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,151 @@
|
||||
import type { LRParser } from '@lezer/lr';
|
||||
|
||||
import { MySQL, PostgreSQL, SQLDialect } from '../src';
|
||||
|
||||
const mysqlTokens = MySQL.language;
|
||||
const postgresqlTokens = PostgreSQL.language;
|
||||
const bigQueryTokens = SQLDialect.define({
|
||||
treatBitsAsBytes: true,
|
||||
}).language;
|
||||
|
||||
const parse = (parser: LRParser, input: string) => {
|
||||
const tree = parser.parse(input);
|
||||
const props: Record<string, { tree: unknown }> = (
|
||||
tree as unknown as { props: Record<string, { tree: unknown }> }
|
||||
).props;
|
||||
const key = Object.keys(props)[0];
|
||||
return String(props[key].tree);
|
||||
};
|
||||
|
||||
const parseMixed = (parser: LRParser, input: string) => {
|
||||
return String(parser.parse(input) as unknown);
|
||||
};
|
||||
|
||||
describe('Parse MySQL tokens', () => {
|
||||
const parser = mysqlTokens.parser;
|
||||
|
||||
it('parses quoted bit-value literals', () => {
|
||||
expect(parse(parser, "SELECT b'0101'")).toEqual('Script(Statement(Keyword,Whitespace,Bits))');
|
||||
});
|
||||
|
||||
it('parses unquoted bit-value literals', () => {
|
||||
expect(parse(parser, 'SELECT 0b01')).toEqual('Script(Statement(Keyword,Whitespace,Bits))');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Parse PostgreSQL tokens', () => {
|
||||
const parser = postgresqlTokens.parser;
|
||||
|
||||
it('parses quoted bit-value literals', () => {
|
||||
expect(parse(parser, "SELECT b'0101'")).toEqual('Script(Statement(Keyword,Whitespace,Bits))');
|
||||
});
|
||||
|
||||
it('parses quoted bit-value literals', () => {
|
||||
expect(parse(parser, "SELECT B'0101'")).toEqual('Script(Statement(Keyword,Whitespace,Bits))');
|
||||
});
|
||||
|
||||
it('parses double dollar quoted Whitespace literals', () => {
|
||||
expect(parse(parser, 'SELECT $$hello$$')).toEqual(
|
||||
'Script(Statement(Keyword,Whitespace,String))',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Parse BigQuery tokens', () => {
|
||||
const parser = bigQueryTokens.parser;
|
||||
|
||||
it('parses quoted bytes literals in single quotes', () => {
|
||||
expect(parse(parser, "SELECT b'abcd'")).toEqual('Script(Statement(Keyword,Whitespace,Bytes))');
|
||||
});
|
||||
|
||||
it('parses quoted bytes literals in double quotes', () => {
|
||||
expect(parse(parser, 'SELECT b"abcd"')).toEqual('Script(Statement(Keyword,Whitespace,Bytes))');
|
||||
});
|
||||
|
||||
it('parses bytes literals in single quotes', () => {
|
||||
expect(parse(parser, "SELECT b'0101'")).toEqual('Script(Statement(Keyword,Whitespace,Bytes))');
|
||||
});
|
||||
|
||||
it('parses bytes literals in double quotes', () => {
|
||||
expect(parse(parser, 'SELECT b"0101"')).toEqual('Script(Statement(Keyword,Whitespace,Bytes))');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Parse n8n resolvables', () => {
|
||||
const parser = postgresqlTokens.parser;
|
||||
|
||||
it('parses resolvables with dots inside composite identifiers', () => {
|
||||
expect(parseMixed(parser, "SELECT my_column FROM {{ 'schema' }}.{{ 'table' }}")).toEqual(
|
||||
'Program(Plaintext,Resolvable,Plaintext,Resolvable)',
|
||||
);
|
||||
expect(
|
||||
parseMixed(parser, "SELECT my_column FROM {{ 'schema' }}.{{ 'table' }}.{{ 'foo' }}"),
|
||||
).toEqual('Program(Plaintext,Resolvable,Plaintext,Resolvable,Plaintext,Resolvable)');
|
||||
expect(parseMixed(parser, "SELECT my_column FROM public.{{ 'table' }}")).toEqual(
|
||||
'Program(Plaintext,Resolvable)',
|
||||
);
|
||||
expect(parseMixed(parser, "SELECT my_column FROM {{ 'schema' }}.users")).toEqual(
|
||||
'Program(Plaintext,Resolvable,Plaintext)',
|
||||
);
|
||||
});
|
||||
|
||||
it('parses 4-node SELECT variants', () => {
|
||||
expect(parseMixed(parser, "{{ 'SELECT' }} my_column FROM my_table")).toEqual(
|
||||
'Program(Resolvable,Plaintext)',
|
||||
);
|
||||
|
||||
expect(parseMixed(parser, "SELECT {{ 'my_column' }} FROM my_table")).toEqual(
|
||||
'Program(Plaintext,Resolvable,Plaintext)',
|
||||
);
|
||||
|
||||
expect(parseMixed(parser, "SELECT my_column {{ 'FROM' }} my_table")).toEqual(
|
||||
'Program(Plaintext,Resolvable,Plaintext)',
|
||||
);
|
||||
|
||||
expect(parseMixed(parser, "SELECT my_column FROM {{ 'my_table' }}")).toEqual(
|
||||
'Program(Plaintext,Resolvable)',
|
||||
);
|
||||
});
|
||||
|
||||
it('parses 5-node SELECT variants (with semicolon)', () => {
|
||||
expect(parseMixed(parser, "{{ 'SELECT' }} my_column FROM my_table;")).toEqual(
|
||||
'Program(Resolvable,Plaintext)',
|
||||
);
|
||||
|
||||
expect(parseMixed(parser, "SELECT {{ 'my_column' }} FROM my_table;")).toEqual(
|
||||
'Program(Plaintext,Resolvable,Plaintext)',
|
||||
);
|
||||
|
||||
expect(parseMixed(parser, "SELECT my_column {{ 'FROM' }} my_table;")).toEqual(
|
||||
'Program(Plaintext,Resolvable,Plaintext)',
|
||||
);
|
||||
|
||||
expect(parseMixed(parser, "SELECT my_column FROM {{ 'my_table' }};")).toEqual(
|
||||
'Program(Plaintext,Resolvable,Plaintext)',
|
||||
);
|
||||
});
|
||||
|
||||
it('parses single-quoted resolvable with no whitespace', () => {
|
||||
expect(parseMixed(parser, "SELECT my_column FROM '{{ 'my_table' }}';")).toEqual(
|
||||
'Program(Plaintext,Resolvable,Plaintext)',
|
||||
);
|
||||
});
|
||||
|
||||
it('parses single-quoted resolvable with leading whitespace', () => {
|
||||
expect(parseMixed(parser, "SELECT my_column FROM ' {{ 'my_table' }}';")).toEqual(
|
||||
'Program(Plaintext,Resolvable,Plaintext)',
|
||||
);
|
||||
});
|
||||
|
||||
it('parses single-quoted resolvable with trailing whitespace', () => {
|
||||
expect(parseMixed(parser, "SELECT my_column FROM '{{ 'my_table' }} ';")).toEqual(
|
||||
'Program(Plaintext,Resolvable,Plaintext)',
|
||||
);
|
||||
});
|
||||
|
||||
it('parses single-quoted resolvable with surrounding whitespace', () => {
|
||||
expect(parseMixed(parser, "SELECT my_column FROM ' {{ 'my_table' }} ';")).toEqual(
|
||||
'Program(Plaintext,Resolvable,Plaintext)',
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user