Skip to content

Commit

Permalink
fix: resolve enum comparison with string literals
Browse files Browse the repository at this point in the history
This patch addresses an issue where the SafeQL ESLint plugin would fail when comparing enums with string literals. Additional test cases have been added to ensure proper functionality in various SQL contexts.
  • Loading branch information
Newbie012 committed Dec 7, 2024
1 parent 7e9b259 commit f8abd57
Show file tree
Hide file tree
Showing 5 changed files with 441 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/gentle-trainers-beam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ts-safeql/eslint-plugin": patch
---

fixed an issue where safeql would fail when comparing enum with string literals
92 changes: 90 additions & 2 deletions packages/eslint-plugin/src/rules/check-sql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import {
} from "@ts-safeql/test-utils";
import { InvalidTestCase, RuleTester } from "@typescript-eslint/rule-tester";

import { afterAll, beforeAll, describe, it } from "vitest";
import { normalizeIndent } from "@ts-safeql/shared";
import path from "path";
import { Sql } from "postgres";
import { afterAll, beforeAll, describe, it } from "vitest";
import rules from ".";
import { RuleOptionConnection, RuleOptions } from "./RuleOptions";
import { normalizeIndent } from "@ts-safeql/shared";

const tsconfigRootDir = path.resolve(__dirname, "../../");
const project = "tsconfig.json";
Expand Down Expand Up @@ -57,6 +57,11 @@ const runMigrations1 = <TTypes extends Record<string, unknown>>(sql: Sql<TTypes>
agency_id INT NOT NULL REFERENCES agency(id)
);
CREATE TABLE certification_metadata (
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
certification certification NOT NULL
);
CREATE TABLE test_date_column (
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
date_col DATE NOT NULL,
Expand Down Expand Up @@ -837,6 +842,79 @@ RuleTester.describe("check-sql", () => {
],
});

ruleTester.run("one-of transformation", rules["check-sql"], {
valid: [
{
filename,
name: "control",
options: withConnection(connections.withTag),
code: normalizeIndent`
function union(cert: "HHA" | "RN", cert2: "LPN" | "CNA") {
return sql\`SELECT FROM caregiver WHERE certification = \${cert}\`
}
`,
},
{
filename,
name: "control",
options: withConnection(connections.withTag),
code: normalizeIndent`
function union(cert: "HHA" | "RN", cert2: "LPN" | "CNA") {
return sql\`UPDATE caregiver SET certification = \${cert}::certification WHERE id = 1\`
}
`,
},
{
filename,
name: "join context",
options: withConnection(connections.withTag),
code: normalizeIndent`
function joinTest(cert: "HHA" | "RN") {
return sql\`SELECT FROM caregiver c JOIN certification_metadata ct ON c.certification = \${cert}\`
}
`,
},
{
filename,
name: "case context",
options: withConnection(connections.withTag),
code: normalizeIndent`
function caseTest(cert: "HHA" | "RN") {
return sql<{ is_certified: number }>\`
SELECT CASE WHEN certification = \${cert} THEN 1 ELSE 0 END AS is_certified
FROM caregiver
\`
}
`,
},
{
filename,
name: "having context",
options: withConnection(connections.withTag),
code: normalizeIndent`
function havingTest(cert: "HHA" | "RN") {
return sql\`SELECT FROM caregiver GROUP BY certification HAVING certification = \${cert}\`
}
`,
},
{
filename,
name: "returning context",
options: withConnection(connections.withTag),
code: normalizeIndent`
function returningTest(cert: "HHA" | "RN") {
return sql<{ one_of: boolean | null }>\`
UPDATE caregiver
SET id = DEFAULT
WHERE FALSE
RETURNING certification = \${cert} AS one_of\`
}
`,
},
],
invalid: [],
});

ruleTester.run("position", rules["check-sql"], {
valid: [
{
Expand Down Expand Up @@ -907,6 +985,16 @@ RuleTester.describe("check-sql", () => {
line: 2,
columns: [61, 69],
}),
invalidPositionTestCase({
code: normalizeIndent`
function run(cert: "HHA" | "RN'") {
return sql\`select id from caregiver where certification = \${cert}\`
}
`,
error: `invalid input value for enum certification: "RN'"`,
line: 2,
columns: [61, 68],
}),
invalidPositionTestCase({
code: "sql`select id, id from caregiver`",
error: `Duplicate columns: caregiver.id, caregiver.id`,
Expand Down
205 changes: 205 additions & 0 deletions packages/eslint-plugin/src/utils/query-context.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import { normalizeIndent } from "@ts-safeql/shared";
import { describe, expect, it } from "vitest";
import { getQueryContext } from "./query-context";

describe("getQueryContext", () => {
it("should handle queries with no keywords", () => {
const query = "";

expect(getQueryContext(query)).toMatchInlineSnapshot(`
[]
`);
});

it("should parse queries with unusual capitalization", () => {
const query = "SeLeCt * FrOm TBL WhErE ID = 10";

expect(getQueryContext(query)).toMatchInlineSnapshot(`
[
"SELECT",
"FROM",
"WHERE",
]
`);
});

it("should handle queries with comments", () => {
const query = `
SELECT id, name -- Select columns
FROM people -- From table
WHERE age > 30 /* age filter */
`;

expect(getQueryContext(query)).toMatchInlineSnapshot(`
[
"SELECT",
"FROM",
"WHERE",
]
`);
});

it("should parse queries with UNION", () => {
const query = normalizeIndent`
SELECT name FROM tbl1
UNION
SELECT name FROM tbl2
ORDER BY name
`;

expect(getQueryContext(query)).toMatchInlineSnapshot(`
[
"SELECT",
"FROM",
"UNION",
"SELECT",
"FROM",
"ORDER BY",
]
`);
});

it("should parse queries with JOINs", () => {
const query = normalizeIndent`
SELECT a.name, b.age
FROM tbl1 a
INNER JOIN tbl2 b ON a.id = b.id
WHERE b.age > 30
`;

expect(getQueryContext(query)).toMatchInlineSnapshot(`
[
"SELECT",
"FROM",
"INNER JOIN",
"ON",
"WHERE",
]
`);
});

it("should parse queries with nested functions", () => {
const query = normalizeIndent`
SELECT id, COUNT(*) AS total
FROM (
SELECT id FROM tbl WHERE col = 5
) subquery
GROUP BY id
`;

expect(getQueryContext(query)).toMatchInlineSnapshot(`
[
"SELECT",
"FROM",
[
"SELECT",
"FROM",
"WHERE",
],
"GROUP BY",
]
`);
});

it("should handle queries with placeholders", () => {
const query = "SELECT * FROM tbl WHERE id = $1 AND name = $2";

expect(getQueryContext(query)).toMatchInlineSnapshot(`
[
"SELECT",
"FROM",
"WHERE",
]
`);
});

it("should parse queries with CASE statements", () => {
const query = normalizeIndent`
SELECT id,
CASE WHEN col1 = 1 THEN 'A'
WHEN col2 = 2 THEN 'B'
ELSE 'C' END AS category
FROM tbl
`;

expect(getQueryContext(query)).toMatchInlineSnapshot(`
[
"SELECT",
"FROM",
]
`);
});

it("should parse queries with window functions", () => {
const query = normalizeIndent`
SELECT id, ROW_NUMBER() OVER (PARTITION BY category ORDER BY created_at) AS row_num
FROM tbl
`;

expect(getQueryContext(query)).toMatchInlineSnapshot(`
[
"SELECT",
[
"PARTITION BY",
"ORDER BY",
],
"FROM",
]
`);
});

it("should parse queries with complex expressions in SELECT", () => {
const query = "SELECT id, (col1 + col2) * col3 AS result FROM tbl";

expect(getQueryContext(query)).toMatchInlineSnapshot(`
[
"SELECT",
"FROM",
]
`);
});

it("should parse queries with DISTINCT ON", () => {
const query = "SELECT DISTINCT ON (col1) col1, col2 FROM tbl ORDER BY col1, col2";

expect(getQueryContext(query)).toMatchInlineSnapshot(`
[
"SELECT",
"ON",
"FROM",
"ORDER BY",
]
`);
});

it("should parse queries with multiple WITH clauses", () => {
const query = normalizeIndent`
WITH cte1 AS (
SELECT id FROM tbl1
),
cte2 AS (
SELECT id FROM tbl2
)
SELECT * FROM cte1
INNER JOIN cte2 ON cte1.id = cte2.id
`;

expect(getQueryContext(query)).toMatchInlineSnapshot(`
[
"WITH",
[
"SELECT",
"FROM",
],
[
"SELECT",
"FROM",
],
"SELECT",
"FROM",
"INNER JOIN",
"ON",
]
`);
});
});
Loading

0 comments on commit f8abd57

Please sign in to comment.