Skip to content

Commit 6da5e4f

Browse files
authored
Add support for MSSQL-style bracket quoted identifiers
FEATURE: Allow `[` in `identifierQuotes` for MSSQL-style bracketed identifiers.
1 parent 4a74c3d commit 6da5e4f

File tree

5 files changed

+66
-19
lines changed

5 files changed

+66
-19
lines changed

src/complete.ts

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ function tokenBefore(tree: SyntaxNode) {
1313

1414
function idName(doc: Text, node: SyntaxNode): string {
1515
let text = doc.sliceString(node.from, node.to)
16-
let quoted = /^([`'"])(.*)\1$/.exec(text)
16+
let quoted = /^([`'"\[])(.*)([`'"\]])$/.exec(text)
1717
return quoted ? quoted[2] : text
1818
}
1919

@@ -86,12 +86,11 @@ function getAliases(doc: Text, at: SyntaxNode) {
8686
return aliases
8787
}
8888

89-
function maybeQuoteCompletions(quote: string | null, completions: readonly Completion[]) {
90-
if (!quote) return completions
91-
return completions.map(c => ({...c, label: c.label[0] == quote ? c.label : quote + c.label + quote, apply: undefined}))
89+
function maybeQuoteCompletions(openingQuote: string, closingQuote: string, completions: readonly Completion[]) {
90+
return completions.map(c => ({...c, label: c.label[0] == openingQuote ? c.label : openingQuote + c.label + closingQuote, apply: undefined}))
9291
}
9392

94-
const Span = /^\w*$/, QuotedSpan = /^[`'"]?\w*[`'"]?$/
93+
const Span = /^\w*$/, QuotedSpan = /^[`'"\[]?\w*[`'"\]]?$/
9594

9695
function isSelfTag(namespace: SQLNamespace): namespace is {self: Completion, children: SQLNamespace} {
9796
return (namespace as any).self && typeof (namespace as any).self.label == "string"
@@ -156,7 +155,12 @@ class CompletionLevel {
156155

157156
function nameCompletion(label: string, type: string, idQuote: string, idCaseInsensitive: boolean): Completion {
158157
if ((new RegExp("^[a-z_][a-z_\\d]*$", idCaseInsensitive ? "i" : "")).test(label)) return {label, type}
159-
return {label, type, apply: idQuote + label + idQuote}
158+
159+
return {label, type, apply: idQuote + label + getClosingQuote(idQuote)}
160+
}
161+
162+
function getClosingQuote(openingQuote: string) {
163+
return openingQuote === "[" ? "]" : openingQuote;
160164
}
161165

162166
// Some of this is more gnarly than it has to be because we're also
@@ -191,15 +195,29 @@ export function completeFromSchema(schema: SQLNamespace,
191195
if (!next) return null
192196
level = next
193197
}
194-
let quoteAfter = quoted && context.state.sliceDoc(context.pos, context.pos + 1) == quoted
198+
195199
let options = level.list
196200
if (level == top && aliases)
197201
options = options.concat(Object.keys(aliases).map(name => ({label: name, type: "constant"})))
198-
return {
199-
from,
200-
to: quoteAfter ? context.pos + 1 : undefined,
201-
options: maybeQuoteCompletions(quoted, options),
202-
validFor: quoted ? QuotedSpan : Span
202+
203+
if (quoted) {
204+
let openingQuote = quoted[0]
205+
let closingQuote = getClosingQuote(openingQuote);
206+
207+
let quoteAfter = context.state.sliceDoc(context.pos, context.pos + 1) == closingQuote
208+
209+
return {
210+
from,
211+
to: quoteAfter ? context.pos + 1 : undefined,
212+
options: maybeQuoteCompletions(openingQuote, closingQuote, options),
213+
validFor: QuotedSpan,
214+
}
215+
} else {
216+
return {
217+
from,
218+
options: options,
219+
validFor: Span
220+
}
203221
}
204222
}
205223
}

src/sql.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export type SQLDialectSpec = {
7575
/// Defaults to `"?"`.
7676
specialVar?: string,
7777
/// The characters that can be used to quote identifiers. Defaults
78-
/// to `"\""`.
78+
/// to `"\""`. Add `[` for MSSQL-style bracket quoted identifiers.
7979
identifierQuotes?: string
8080
/// Controls whether identifiers are case-insensitive. Identifiers
8181
/// with upper-case letters are quoted when set to false (which is
@@ -318,7 +318,8 @@ export const MSSQL = SQLDialect.define({
318318
types: SQLTypes + "smalldatetime datetimeoffset datetime2 datetime bigint smallint smallmoney tinyint money real text nvarchar ntext varbinary image hierarchyid uniqueidentifier sql_variant xml",
319319
builtin: MSSQLBuiltin,
320320
operatorChars: "*+-%<>!=^&|/",
321-
specialVar: "@"
321+
specialVar: "@",
322+
identifierQuotes: "\"["
322323
})
323324

324325
/// [SQLite](https://sqlite.org/) dialect.

src/tokens.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,10 @@ export function tokensFor(d: Dialect) {
269269
input.advance(2)
270270
readPLSQLQuotedLiteral(input, openDelim)
271271
input.acceptToken(StringToken)
272+
} else if (inString(next, d.identifierQuotes)) {
273+
const endQuote = next == Ch.BracketL ? Ch.BracketR : next
274+
readLiteral(input, endQuote, false)
275+
input.acceptToken(QuotedIdentifier)
272276
} else if (next == Ch.ParenL) {
273277
input.acceptToken(ParenL)
274278
} else if (next == Ch.ParenR) {
@@ -319,9 +323,6 @@ export function tokensFor(d: Dialect) {
319323
if (input.next == next) input.advance()
320324
readWordOrQuoted(input)
321325
input.acceptToken(SpecialVar)
322-
} else if (inString(next, d.identifierQuotes)) {
323-
readLiteral(input, next, false)
324-
input.acceptToken(QuotedIdentifier)
325326
} else if (next == Ch.Colon || next == Ch.Comma) {
326327
input.acceptToken(Punctuation)
327328
} else if (isAlpha(next)) {

test/test-complete.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {EditorState} from "@codemirror/state"
22
import {CompletionContext, CompletionResult, CompletionSource} from "@codemirror/autocomplete"
3-
import {schemaCompletionSource, keywordCompletionSource, PostgreSQL, MySQL, SQLConfig, SQLDialect} from "@codemirror/lang-sql"
3+
import {schemaCompletionSource, keywordCompletionSource, PostgreSQL, MySQL, SQLConfig, SQLDialect, MSSQL} from "@codemirror/lang-sql"
44
import ist from "ist"
55

66
function get(doc: string, conf: SQLConfig & {explicit?: boolean, keywords?: boolean} = {}) {
@@ -94,6 +94,11 @@ describe("SQL completion", () => {
9494
ist(str(get('select "other"."users"."|', {schema: schema2})), '"id", "name"')
9595
})
9696

97+
it("completes column names in bracket quoted tables with MSSQL", () => {
98+
ist(str(get("select [public].[users].|", {schema: schema2, dialect: MSSQL})), "email, id")
99+
ist(str(get("select [other].[users].|", {schema: schema2, dialect: MSSQL})), "id, name")
100+
})
101+
97102
it("completes column names of aliased tables", () => {
98103
ist(str(get("select u.| from users u", {schema: schema1})), "address, id, name")
99104
ist(str(get("select u.| from users as u", {schema: schema1})), "address, id, name")
@@ -126,6 +131,11 @@ describe("SQL completion", () => {
126131
ist(str(get('select a| from a.b as ab join auto au', {schema: schema2})), "ab, au, other, public")
127132
})
128133

134+
it("completes bracket quoted aliases with MSSQL", () => {
135+
ist(str(get("select u.| from public.users [u]", {schema: schema2, dialect: MSSQL})), "email, id")
136+
ist(str(get("select [u].| from public.users u", {schema: schema2, dialect: MSSQL})), "email, id")
137+
})
138+
129139
it("includes closing quote in completion", () => {
130140
let r = get('select "u|"', {schema: schema1})
131141
ist(r!.to, 10)
@@ -147,6 +157,7 @@ describe("SQL completion", () => {
147157

148158
it("supports alternate quoting styles", () => {
149159
ist(str(get("select `u|", {dialect: MySQL, schema: schema1})), "`products`, `users`")
160+
ist(str(get("select [u|", {dialect: MSSQL, schema: schema1})), "[products], [users]")
150161
})
151162

152163
it("doesn't complete without identifier", () => {
@@ -173,6 +184,13 @@ describe("SQL completion", () => {
173184
'Column, cell')
174185
})
175186

187+
it("can add MSSQL-style brackets as identifier quotes", () => {
188+
let dialect = SQLDialect.define({...MSSQL.spec, identifierQuotes: '['});
189+
let config = { schema: {"Foo": ["Bar"]}, dialect: dialect };
190+
191+
ist(str(get("[Foo].b|", config)), "[Bar]");
192+
})
193+
176194
it("supports nesting more than two deep", () => {
177195
let s = {schema: {"one.two.three": ["four"]}}
178196
ist(str(get("o|", s)), "one")

test/test-tokens.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import ist from "ist"
2-
import {PostgreSQL, MySQL, PLSQL, SQLDialect} from "@codemirror/lang-sql"
2+
import {PostgreSQL, MySQL, PLSQL, SQLDialect, MSSQL} from "@codemirror/lang-sql"
33

44
const mysqlTokens = MySQL.language
55
const postgresqlTokens = PostgreSQL.language
66
const bigQueryTokens = SQLDialect.define({
77
treatBitsAsBytes: true
88
}).language
99
const plsqlTokens = PLSQL.language
10+
const mssqlTokens = MSSQL.language
1011

1112
describe("Parse MySQL tokens", () => {
1213
const parser = mysqlTokens.parser
@@ -91,3 +92,11 @@ describe("Parse PL/SQL tokens", () => {
9192
ist(parser.parse("SELECT q'~foo'bar' FROM DUAL"), 'Script(Statement(Keyword,String))')
9293
})
9394
})
95+
96+
describe("Parse MSSQL tokens", () => {
97+
const parser = mssqlTokens.parser;
98+
99+
it("parses brackets as QuotedIdentifier", () => {
100+
ist(parser.parse("SELECT * FROM [tblTest]"), "Script(Statement(Keyword,Operator,Keyword,QuotedIdentifier))")
101+
})
102+
})

0 commit comments

Comments
 (0)