Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 2 additions & 1 deletion src/embed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { Printer } from "prettier";
import { Node } from "sql-parser-cst";
import { embedJs } from "./embedJs";
import { embedJson } from "./embedJson";
import { embedSql } from "./embedSql";

export const embed: NonNullable<Printer<Node>["embed"]> = (...args) => {
return embedJson(...args) || embedJs(...args);
return embedJson(...args) || embedJs(...args) || embedSql(...args);
};
56 changes: 56 additions & 0 deletions src/embedSql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Printer } from "prettier";
import { CreateFunctionStmt, Node, StringLiteral } from "sql-parser-cst";
import {
isAsClause,
isCreateFunctionStmt,
isLanguageClause,
isStringLiteral,
} from "./node_utils";
import { hardline, indent, stripTrailingHardline } from "./print_utils";

export const embedSql: NonNullable<Printer<Node>["embed"]> = (path, options) => {
const node = path.node;
const parent = path.getParentNode(0);
const grandParent = path.getParentNode(1);

if (
isStringLiteral(node) &&
isAsClause(parent) &&
isCreateFunctionStmt(grandParent) &&
grandParent.clauses.some(isSqlLanguageClause)
) {
return async (textToDoc) => {
const quote = detectQuote(node);

if (quote) {
const sql = await textToDoc(node.value, options);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Is there an option that can be passed here to tell textToDoc to retain the string quoting when quote === "'"?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Handling the escaped quotes properly is very hard to do and IMHO not worth the effort. We'd need to keep track which quotes were escaped in the original code and then re-escape them later. I don't even know from where to start with when solving this problem.

In practice one generally doesn't use single quotes for these SQL strings. At most one would for quoting a very simple SQL. If quotes are needed inside there, one generally just switches to another quoting style (like the dollar-quotes) instead of using escapes.

What I would suggest is:

  • format SQL inside dollar-quoted strings as you already do
  • convert single-quoted strings to dollar-quoted strings
  • do no formatting when it's not possible to convert a single-quoted string to dollar-quoted one. (Like for start we might only attempt to covert it to the form $$ ... $$, which can't be done when the string already contains $$ as substring).

This is largely the approach taken inside embedJs() function where strings are converted to either r''' ... ''' or r""" ... """ quoting style (specific to BigQuery).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I agree completely!


return [
quote,
indent([hardline, stripTrailingHardline(sql)]),
hardline,
quote,
];
}
};
}

return null;
};

const isSqlLanguageClause = (
clause: CreateFunctionStmt["clauses"][0],
): boolean => isLanguageClause(clause) && clause.name.name === "sql";

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

In PostgreSQL the language name is case-insensitive when unquoted. So LANGUAGE SQL and LANGUAGE SqL are also valid.

However, that case-insensitivity only applies when the language name identifier is not quoted. So LANGUAGE "sql" is valid, but LANGUAGE "SQL" is invalid.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I was following the code in embedJs and presumed the casing was normalised elsewhere, but good to know. Btw just FYI, I noticed that the parser currently fails on single-quoted language identifiers, e.g. LANGUAGE 'sql', which are also valid in postgres -- but I'd wait for someone to request it before spending time fixing that.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for the info.


const detectQuote = (
node: StringLiteral,
): string | undefined => {
const match = node.text.match(/^('|\$[^$]*\$)/);
const quote = match?.[1];

if (quote && node.text.endsWith(quote)) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I wen't with this version over const quote = node.text.match(/^('|\$[^$]*\$).*\1$/s)?.[1] because the s regular expression flag is only supported from ES2018. Also my guess is that endsWith might perform better than a regex backreference but I'm not sure

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

There's no need to check both the start and end of the string to determine its quoting style. When the string starts with ' it must end with '. When the string starts with $$ it must end with $$ and so on.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good to know. I presumed that this is handled by the parser and indicated that in my commit message but let me know if that's not the case and I'll change the commit message. Or ignore this if you squash commits when merging :)

return quote;
}

return undefined;
};
68 changes: 68 additions & 0 deletions test/ddl/function.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,74 @@ describe("function", () => {
AS " return /'''|\\"\\"\\"/.test(x) "
`);
});

it(`formats dollar-quoted SQL function`, async () => {
await testPostgresql(dedent`
CREATE FUNCTION my_func()
RETURNS INT64
LANGUAGE sql
AS $$
SELECT 1;
$$
`);
});

it(`reformats SQL in dollar-quoted SQL function`, async () => {
expect(
await pretty(
dedent`
CREATE FUNCTION my_func()
RETURNS INT64
LANGUAGE sql
AS $body$SELECT 1;
select 2$body$
`,
{ dialect: "postgresql" },
),
).toBe(dedent`
CREATE FUNCTION my_func()
RETURNS INT64
LANGUAGE sql
AS $body$
SELECT 1;
SELECT 2;
$body$
`);
});

it(`formats single-quoted SQL function`, async () => {
await testPostgresql(dedent`
CREATE FUNCTION my_func()
RETURNS TEXT
LANGUAGE sql
AS '
SELECT ''foo'';
'
`);
});

it(`reformats SQL in single-quoted SQL function`, async () => {
expect(
await pretty(
dedent`
CREATE FUNCTION my_func()
RETURNS TEXT
LANGUAGE sql
AS 'SELECT ''foo'';
select ''bar'''
`,
{ dialect: "postgresql" },
),
).toBe(dedent`
CREATE FUNCTION my_func()
RETURNS TEXT
LANGUAGE sql
AS '
SELECT ''foo'';
SELECT ''bar'';
'
`);
});
});

describe("drop function", () => {
Expand Down
Loading