Skip to content
Merged
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
23 changes: 9 additions & 14 deletions src/column-parser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ColumnReference, Dialect, Token } from './defines';
import { maybeIdentifier, maybeStripQuotes } from './utils';

// States for skipping MSSQL's TOP clause: SELECT TOP n [PERCENT] [WITH TIES]
// The tokenizer emits digits as individual single-character 'unknown' tokens,
Expand Down Expand Up @@ -255,7 +256,7 @@ export class ColumnParser {
prevNonWhitespaceToken?.value !== '.' &&
prevNonWhitespaceToken?.value !== ',' &&
prevToken?.type === 'whitespace' &&
this.maybeIdent(token)
maybeIdentifier(token, this.dialect)
) {
if (!this.alias) {
this.alias = token.value;
Expand Down Expand Up @@ -300,22 +301,22 @@ export class ColumnParser {
if (this.parts.length === 1) {
const name = this.parts[0];
col = {
name,
name: maybeStripQuotes(name, this.dialect),
isWildcard: name === '*',
};
} else if (this.parts.length === 2) {
const [table, name] = this.parts;
col = {
name,
table,
name: maybeStripQuotes(name, this.dialect),
table: maybeStripQuotes(table, this.dialect),
isWildcard: name === '*',
};
} else if (this.parts.length === 3) {
const [schema, table, name] = this.parts;
col = {
name,
table,
schema,
name: maybeStripQuotes(name, this.dialect),
table: maybeStripQuotes(table, this.dialect),
schema: maybeStripQuotes(schema, this.dialect),
isWildcard: name === '*',
};
} else {
Expand All @@ -327,7 +328,7 @@ export class ColumnParser {
}

if (!!this.alias && !!col) {
col.alias = this.alias;
col.alias = maybeStripQuotes(this.alias, this.dialect);
}

return col;
Expand All @@ -346,10 +347,4 @@ export class ColumnParser {
col.alias ?? 'none'
}`;
}

private maybeIdent(token: Token): boolean {
const ch = token.value[0];
const startChars = this.dialect === 'mssql' ? ['"', '['] : ['"', '`'];
return token.type !== 'string' && (startChars.includes(ch) || /[a-zA-Z_]/.test(ch));
}
}
2 changes: 1 addition & 1 deletion src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -856,7 +856,7 @@ function stateMachineStatementParser(
let openBlocks = 0;

const columnParser = new ColumnParser(dialect);
const tableParser = new TableParser();
const tableParser = new TableParser(dialect);

/* eslint arrow-body-style: 0, no-extra-parens: 0 */
const isValidToken = (step: Step, token: Token) => {
Expand Down
19 changes: 11 additions & 8 deletions src/table-parser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TableReference, Token } from './defines';
import { Dialect, TableReference, Token } from './defines';
import { maybeStripQuotes } from './utils';

export class TableParser {
private parts: string[] = [];
Expand All @@ -9,6 +10,8 @@ export class TableParser {
private maybeCommaSep = false;
private parensDepth = 0;

constructor(private dialect: Dialect) {}

// keywords that come directly before a table name.
// v1 - keeping it very simple.
private PRE_TABLE_KEYWORDS = new Set<string>(['FROM', 'JOIN', 'INTO']);
Expand Down Expand Up @@ -140,20 +143,20 @@ export class TableParser {
if (this.parts.length === 1) {
const name = this.parts[0];
table = {
name,
name: maybeStripQuotes(name, this.dialect),
};
} else if (this.parts.length === 2) {
const [schema, name] = this.parts;
table = {
name,
schema,
name: maybeStripQuotes(name, this.dialect),
schema: maybeStripQuotes(schema, this.dialect),
};
} else if (this.parts.length === 3) {
const [database, schema, name] = this.parts;
table = {
name,
schema,
database,
name: maybeStripQuotes(name, this.dialect),
schema: maybeStripQuotes(schema, this.dialect),
database: maybeStripQuotes(database, this.dialect),
};
} else {
const fullName = this.parts.join('.');
Expand All @@ -163,7 +166,7 @@ export class TableParser {
}

if (!!this.alias && !!table) {
table.alias = this.alias;
table.alias = maybeStripQuotes(this.alias, this.dialect);
}

return table;
Expand Down
26 changes: 20 additions & 6 deletions src/tokenizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/

import type { Token, State, Dialect, ParamTypes } from './defines';
import { getStartQuotes } from './utils';

type Char = string | null;

Expand Down Expand Up @@ -117,7 +118,7 @@ export function scanToken(
}

if (isQuotedIdentifier(ch, dialect) && ch !== null) {
return scanQuotedIdentifier(state, ENDTOKENS[ch]);
return scanQuotedIdentifier(state, ENDTOKENS[ch], dialect);
}

if (isLetter(ch)) {
Expand Down Expand Up @@ -385,11 +386,24 @@ function scanCommentBlock(state: State): Token {
};
}

function scanQuotedIdentifier(state: State, endToken: Char): Token {
let nextChar: Char;
do {
function scanQuotedIdentifier(state: State, endToken: Char, dialect: Dialect): Token {
let nextChar: Char = peek(state);
while (nextChar !== null) {
nextChar = read(state);
} while (endToken !== nextChar && nextChar !== null);
if (nextChar === null) break;

if (nextChar === endToken && peek(state) === endToken) {
read(state);
continue;
}

if (dialect === 'bigquery' && nextChar === '\\' && peek(state) === endToken) {
read(state);
continue;
}

if (nextChar === endToken) break;
}

if (nextChar !== null && endToken !== nextChar) {
unread(state);
Expand Down Expand Up @@ -520,7 +534,7 @@ function isDollarQuotedString(state: State): boolean {
}

function isQuotedIdentifier(ch: Char, dialect: Dialect): boolean {
const startQuoteChars: Char[] = dialect === 'mssql' ? ['"', '['] : ['"', '`'];
const startQuoteChars: Char[] = getStartQuotes(dialect);
return startQuoteChars.includes(ch);
}

Expand Down
48 changes: 48 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Dialect, Token } from './defines';

export function getStartQuotes(dialect: Dialect): string[] {
if (dialect === 'mssql') {
return ['"', '['];
} else {
return ['"', '`'];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure backticks are supported by everything? Are " characters supported by everything?

}
}

function endQuoteFor(char: string): string {
if (char === '[') {
return ']';
}
return char;
}

export function maybeIdentifier(token: Token, dialect: Dialect): boolean {
const ch = token.value[0];
const startChars = getStartQuotes(dialect);
return token.type !== 'string' && (startChars.includes(ch) || /[a-zA-Z_]/.test(ch));
}

export function maybeStripQuotes(value: string, dialect: Dialect): string {
if (value.length < 2) {
return value;
}

const start = value[0];
const end = value[value.length - 1];

if (!getStartQuotes(dialect).includes(start)) {
return value;
}

const expectedEnd = endQuoteFor(start);
if (end !== expectedEnd) {
return value;
}

const inner = value.slice(1, -1);

if (dialect === 'bigquery' && start === '`') {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the dialect -> quote character mapping code be centralized somehow, it seems a bit scattered

return inner.replace(/\\`/g, '`');
}

return inner.split(expectedEnd + expectedEnd).join(expectedEnd);
}
Loading
Loading