Skip to content

Commit

Permalink
Wrap quotes around reserved keywords e.g. "user"
Browse files Browse the repository at this point in the history
  • Loading branch information
martijndeh committed Nov 25, 2020
1 parent 62a613c commit 6366dc0
Show file tree
Hide file tree
Showing 14 changed files with 290 additions and 56 deletions.
57 changes: 51 additions & 6 deletions src/__tests__/select.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@ describe(`select`, () => {
isGreat: boolean().notNull(),
});

const db = defineDb({ foo, bar, listItem }, () =>
const user = defineTable({
id: uuid(),
with: text(),
});

const db = defineDb({ foo, bar, listItem, user }, () =>
Promise.resolve({ rows: [], affectedCount: 0 }),
);

Expand Down Expand Up @@ -131,7 +136,21 @@ describe(`select`, () => {
"parameters": Array [
1,
],
"text": "SELECT baz.id FROM foo \\"baz\\" WHERE baz.value = $1",
"text": "SELECT baz.id FROM foo baz WHERE baz.value = $1",
}
`);
});

it(`should alias a table with a reserved keyword plus reference it in a condition `, () => {
const user = db.foo.as(`user`);
const query = db.select(user.id).from(user).where(user.value.eq(1));

expect(toSnap(query)).toMatchInlineSnapshot(`
Object {
"parameters": Array [
1,
],
"text": "SELECT \\"user\\".id FROM foo \\"user\\" WHERE \\"user\\".value = $1",
}
`);
});
Expand All @@ -144,7 +163,7 @@ describe(`select`, () => {
"parameters": Array [
1,
],
"text": "SELECT foo.id, (foo.value + $1) \\"test\\" FROM foo",
"text": "SELECT foo.id, (foo.value + $1) test FROM foo",
}
`);
});
Expand Down Expand Up @@ -274,7 +293,7 @@ describe(`select`, () => {
expect(toSnap(query)).toMatchInlineSnapshot(`
Object {
"parameters": Array [],
"text": "SELECT foo.id, SUM (foo.value) \\"total\\" FROM foo",
"text": "SELECT foo.id, SUM (foo.value) total FROM foo",
}
`);
});
Expand Down Expand Up @@ -463,7 +482,7 @@ describe(`select`, () => {
expect(toSnap(query)).toMatchInlineSnapshot(`
Object {
"parameters": Array [],
"text": "SELECT COUNT (foo.create_date) \\"test\\" FROM foo",
"text": "SELECT COUNT (foo.create_date) test FROM foo",
}
`);
});
Expand Down Expand Up @@ -763,7 +782,7 @@ describe(`select`, () => {
"great",
"not great",
],
"text": "SELECT foo.id, (WHEN foo.value > $1 THEN $2 ELSE $3) \\"greatness\\" FROM foo",
"text": "SELECT foo.id, (WHEN foo.value > $1 THEN $2 ELSE $3) greatness FROM foo",
}
`);
});
Expand All @@ -778,4 +797,30 @@ describe(`select`, () => {
}
`);
});

it(`should wrap quotes around tables and columns which are reserved keywords`, () => {
const test = db.user.as(`test`);
const query = db
.select(
db.user.id,
db.user.with,
db.user.with.as(`test`),
db.user.with.as(`analyse`),
db.user.with.as(`testMe`),
test.with.as(`with2`),
)
.from(db.user)
.innerJoin(db.user.as(`test`))
.where(db.user.with.eq('test-1').and(test.with.as(`with2`).eq('test-2')));

expect(toSnap(query)).toMatchInlineSnapshot(`
Object {
"parameters": Array [
"test-1",
"test-2",
],
"text": "SELECT \\"user\\".id, \\"user\\".\\"with\\", \\"user\\".\\"with\\" test, \\"user\\".\\"with\\" \\"analyse\\", \\"user\\".\\"with\\" \\"testMe\\", test.\\"with\\" with2 FROM \\"user\\" INNER JOIN \\"user\\" test WHERE \\"user\\".\\"with\\" = $1 AND test.\\"with\\" = $2",
}
`);
});
});
32 changes: 32 additions & 0 deletions src/__tests__/update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ describe(`update`, () => {
id: uuid().primaryKey().default(`gen_random_uuid()`),
fooId: uuid().notNull().references(foo, `id`),
name: text(),
with: text(),
});

const db = defineDb({ foo, bar }, () => Promise.resolve({ rows: [], affectedCount: 0 }));
Expand Down Expand Up @@ -52,6 +53,24 @@ describe(`update`, () => {
`);
});

it(`should update-from foo with reserved keyword alias`, () => {
const test = db.bar.as('user');
const query = db
.update(db.foo)
.set({ name: `Test` })
.from(test)
.where(test.fooId.eq(db.foo.id).and(test.name.isNotNull()));

expect(toSnap(query)).toMatchInlineSnapshot(`
Object {
"parameters": Array [
"Test",
],
"text": "UPDATE foo SET name = $1 FROM bar \\"user\\" WHERE \\"user\\".foo_id = foo.id AND \\"user\\".name IS NOT NULL",
}
`);
});

it(`should update where current of foo`, () => {
const query = db.update(db.foo).set({ name: `Test` }).whereCurrentOf(`cursor1`);

Expand All @@ -65,4 +84,17 @@ describe(`update`, () => {
}
`);
});

it(`should update reserved keyword column`, () => {
const query = db.update(db.bar).set({ with: `Test` });

expect(toSnap(query)).toMatchInlineSnapshot(`
Object {
"parameters": Array [
"Test",
],
"text": "UPDATE bar SET \\"with\\" = $1",
}
`);
});
});
45 changes: 31 additions & 14 deletions src/column.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { GroupToken, ParameterToken, SeparatorToken, StringToken, Token } from './tokens';
import { toSnakeCase, wrapQuotes } from './naming/snake-case';
import { toSnakeCase, wrapQuotes } from './naming';

import { Expression } from './expression';
import { TableDefinition } from './table';
Expand Down Expand Up @@ -152,8 +152,20 @@ export class Column<
) {
super(
originalColumnName
? [new StringToken(`${tableName}.${toSnakeCase(originalColumnName)}`)]
: [new StringToken(`${tableName}.${toSnakeCase((columnName as unknown) as string)}`)],
? [
new StringToken(
`${wrapQuotes((tableName as unknown) as string)}.${wrapQuotes(
toSnakeCase(originalColumnName),
)}`,
),
]
: [
new StringToken(
`${wrapQuotes((tableName as unknown) as string)}.${wrapQuotes(
toSnakeCase(columnName),
)}`,
),
],
columnName as any,
);
}
Expand All @@ -167,23 +179,28 @@ export class Column<
/** @internal */
toTokens(includeAlias?: boolean): Token[] {
const snakeCaseColumnName = toSnakeCase((this.columnName as unknown) as string);
const toStringTokens = (tableName: TableName, columnName: string, alias?: string) => {
const initialToken = new StringToken(
`${wrapQuotes((tableName as unknown) as string)}.${wrapQuotes(columnName)}`,
);

if (!alias) {
return [initialToken];
}

return [initialToken, new StringToken(wrapQuotes(alias))];
};

if (includeAlias) {
return this.originalColumnName
? [
new StringToken(`${this.tableName}.${toSnakeCase(this.originalColumnName)}`),
new StringToken(wrapQuotes(this.columnName)),
]
? toStringTokens(this.tableName, toSnakeCase(this.originalColumnName), this.columnName)
: snakeCaseColumnName === (this.columnName as unknown)
? [new StringToken(`${this.tableName}.${snakeCaseColumnName}`)]
: [
new StringToken(`${this.tableName}.${snakeCaseColumnName}`),
new StringToken(wrapQuotes(this.columnName)),
];
? toStringTokens(this.tableName, snakeCaseColumnName)
: toStringTokens(this.tableName, snakeCaseColumnName, this.columnName);
}

return this.originalColumnName
? [new StringToken(`${this.tableName}.${toSnakeCase(this.originalColumnName)}`)]
: [new StringToken(`${this.tableName}.${snakeCaseColumnName}`)];
? toStringTokens(this.tableName, toSnakeCase(this.originalColumnName))
: toStringTokens(this.tableName, snakeCaseColumnName);
}
}
2 changes: 1 addition & 1 deletion src/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { QueryExecutorFn } from './types';
import { makeDeleteFrom } from './delete';
import { makeUpdate } from './update';
import { makeWith } from './with';
import { toSnakeCase } from './naming/snake-case';
import { toSnakeCase } from './naming';

const createTables = <TableDefinitions extends { [key: string]: TableDefinition<any> }>(
tableDefinitions: TableDefinitions,
Expand Down
5 changes: 3 additions & 2 deletions src/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Column } from './column';
import { Expression } from './expression';
import { Query } from './query';
import type { ResultSet } from './result-set';
import { wrapQuotes } from './naming';

export const makeDeleteFrom = (queryExecutor: QueryExecutorFn) => <T extends Table<any, any>>(
table: T,
Expand Down Expand Up @@ -309,9 +310,9 @@ export class DeleteQuery<
const column = (this.table as any)[alias] as Column<any, any, any, any, any, any>;

if (alias !== column.getSnakeCaseName()) {
return new StringToken(`${column.getSnakeCaseName()} "${alias}"`);
return new StringToken(`${wrapQuotes(column.getSnakeCaseName())} ${wrapQuotes(alias)}`);
} else {
return new StringToken(column.getSnakeCaseName());
return new StringToken(wrapQuotes(column.getSnakeCaseName()));
}
}),
),
Expand Down
4 changes: 3 additions & 1 deletion src/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
Token,
} from './tokens';

import { wrapQuotes } from './naming';

export class Expression<DataType, IsNotNull extends boolean, Name extends string> {
private _expressionBrand: any;

Expand Down Expand Up @@ -363,7 +365,7 @@ export class Expression<DataType, IsNotNull extends boolean, Name extends string
if (includeAlias && (this.nameIsAlias || this.name.match(/[A-Z]/))) {
// Some expression return a train_case name by default such as string_agg. We automatically
// convert these to camelCase equivalents e.g. stringAgg.
return [...this.tokens, new StringToken(`"${this.name}"`)];
return [...this.tokens, new StringToken(`${wrapQuotes(this.name)}`)];
}

return this.tokens;
Expand Down
3 changes: 2 additions & 1 deletion src/insert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { Expression } from './expression';
import { Query } from './query';
import { ResultSet } from './result-set';
import { UpdateQuery } from './update';
import { wrapQuotes } from './naming';

// https://www.postgresql.org/docs/12/sql-insert.html
export class InsertQuery<
Expand Down Expand Up @@ -277,7 +278,7 @@ export class InsertQuery<
const column = (this.table as any)[alias] as Column<any, any, any, any, any, any>;

if (alias !== column.getSnakeCaseName()) {
return new StringToken(`${column.getSnakeCaseName()} "${alias}"`);
return new StringToken(`${column.getSnakeCaseName()} ${wrapQuotes(alias)}`);
} else {
return new StringToken(column.getSnakeCaseName());
}
Expand Down
16 changes: 16 additions & 0 deletions src/naming/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { reservedKeywords } from './reserved-keywords';

export const wrapQuotes = (string: string) => {
const isCamelCase = string.match(/[A-Z]/);
const isReserved = reservedKeywords.has(string);
const shouldWrap = isReserved || isCamelCase;

return shouldWrap ? `"${string}"` : string;
};

export const toSnakeCase = (string: string) =>
string
.replace(/\W+/g, ' ')
.split(/ |\B(?=[A-Z])/)
.map((word) => word.toLowerCase())
.join('_');
Loading

0 comments on commit 6366dc0

Please sign in to comment.