Skip to content

Commit faac980

Browse files
committed
perf: don't use database for input parameters
1 parent 6b20c70 commit faac980

File tree

6 files changed

+114
-170
lines changed

6 files changed

+114
-170
lines changed

src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { TSESLint } from "@typescript-eslint/utils";
33
import SQLite from "better-sqlite3";
44
import { createValidQueryRule } from "./rules/valid-query.js";
55
import { createTypedResultRule } from "./rules/typed-result.js";
6-
import { createTypedInputRule } from "./rules/typed-input.js";
6+
import { typedInputRule } from "./rules/typed-input.js";
77
import { GetDatabaseOptions, RuleOptions } from "./ruleOptions.js";
88

99
export interface CreatePluginOptions {
@@ -64,7 +64,7 @@ export function createSqlitePlugin(options: CreatePluginOptions) {
6464
rules: {
6565
"valid-query": createValidQueryRule(ruleOptions),
6666
"typed-result": createTypedResultRule(ruleOptions),
67-
"typed-input": createTypedInputRule(ruleOptions),
67+
"typed-input": typedInputRule,
6868
},
6969
} satisfies TSESLint.FlatConfig.Plugin;
7070

src/inferQueryInput.ts

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,11 @@
1-
import { Database } from "better-sqlite3";
21
import { parse_query_parameters } from "./parser/parser.js";
32

43
export interface QueryInput {
54
count: number;
65
names: string[];
76
}
87

9-
export function inferQueryInput(
10-
query: string,
11-
db: Database,
12-
): QueryInput | null {
13-
// Check that the query is valid
14-
try {
15-
db.prepare(query);
16-
} catch {
17-
return null;
18-
}
19-
8+
export function inferQueryInput(query: string): QueryInput | null {
209
const parameters = parse_query_parameters(query);
2110
if (parameters == null) {
2211
return null;

src/inferQueryResult.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export function inferQueryResult(
135135
)
136136
) {
137137
// Workaround for https://github.com/WiseLibs/better-sqlite3/issues/1243
138-
const inputs = inferQueryInput(query, db);
138+
const inputs = inferQueryInput(query);
139139
const args = [];
140140
if (inputs) {
141141
args.push(new Array(inputs.count - inputs.names.length).fill(0));

src/rules/typed-input.ts

Lines changed: 102 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,129 +1,116 @@
11
import { ESLintUtils, TSESTree, ASTUtils } from "@typescript-eslint/utils";
2-
import { RuleOptions } from "../ruleOptions.js";
3-
import { getQueryValue, stringifyNode } from "../utils.js";
2+
import { getQueryValue } from "../utils.js";
43
import { inferQueryInput, QueryInput } from "../inferQueryInput.js";
54

6-
export function createTypedInputRule(options: RuleOptions) {
7-
return ESLintUtils.RuleCreator.withoutDocs({
8-
create(context) {
9-
return {
10-
'CallExpression[callee.type=MemberExpression][callee.property.name="prepare"][arguments.length=1]'(
11-
node: Omit<TSESTree.CallExpression, "arguments" | "callee"> & {
12-
arguments: [TSESTree.CallExpression["arguments"][0]];
13-
callee: TSESTree.MemberExpression;
14-
},
15-
) {
16-
const val = getQueryValue(
17-
node.arguments[0],
18-
context.sourceCode.getScope(node.arguments[0]),
19-
);
20-
if (typeof val?.value !== "string") {
21-
return;
22-
}
23-
24-
const databaseName = stringifyNode(node.callee.object);
25-
if (!databaseName) {
26-
return;
27-
}
28-
29-
const db = options.getDatabase({
30-
filename: context.filename,
31-
name: databaseName,
32-
});
33-
34-
const queryInput = inferQueryInput(val.value, db);
35-
if (queryInput == null) {
36-
return;
37-
}
38-
39-
const typeArguments = node.typeArguments;
40-
const inputParam = typeArguments?.params[0];
41-
if (!typeArguments || !inputParam) {
42-
context.report({
43-
node: node,
44-
messageId: "missingInputType",
45-
*fix(fixer) {
46-
if (typeArguments && !inputParam) {
47-
yield fixer.replaceText(
48-
typeArguments,
49-
`<${queryInputToText(queryInput)}>`,
50-
);
51-
} else {
52-
yield fixer.insertTextAfter(
53-
node.callee,
54-
`<${queryInputToText(queryInput)}>`,
55-
);
56-
}
57-
},
58-
});
59-
return;
60-
}
61-
62-
if (isDeclaredTypeCorrect(queryInput, inputParam)) {
63-
return;
64-
}
65-
66-
const members =
67-
inputParam.type === TSESTree.AST_NODE_TYPES.TSTypeLiteral
68-
? inputParam.members
69-
: inputParam.type === TSESTree.AST_NODE_TYPES.TSTupleType
70-
? inputParam.elementTypes.find(
71-
(element) =>
72-
element.type === TSESTree.AST_NODE_TYPES.TSTypeLiteral,
73-
)?.members
74-
: null;
75-
76-
const userDeclaredTypes = new Map<string, string>();
77-
78-
if (members) {
79-
for (const member of members) {
80-
if (member.type !== TSESTree.AST_NODE_TYPES.TSPropertySignature) {
81-
continue;
82-
}
83-
84-
if (!member.typeAnnotation) {
85-
continue;
5+
export const typedInputRule = ESLintUtils.RuleCreator.withoutDocs({
6+
create(context) {
7+
return {
8+
'CallExpression[callee.type=MemberExpression][callee.property.name="prepare"][arguments.length=1]'(
9+
node: Omit<TSESTree.CallExpression, "arguments" | "callee"> & {
10+
arguments: [TSESTree.CallExpression["arguments"][0]];
11+
callee: TSESTree.MemberExpression;
12+
},
13+
) {
14+
const val = getQueryValue(
15+
node.arguments[0],
16+
context.sourceCode.getScope(node.arguments[0]),
17+
);
18+
if (typeof val?.value !== "string") {
19+
return;
20+
}
21+
22+
const queryInput = inferQueryInput(val.value);
23+
if (queryInput == null) {
24+
return;
25+
}
26+
27+
const typeArguments = node.typeArguments;
28+
const inputParam = typeArguments?.params[0];
29+
if (!typeArguments || !inputParam) {
30+
context.report({
31+
node: node,
32+
messageId: "missingInputType",
33+
*fix(fixer) {
34+
if (typeArguments && !inputParam) {
35+
yield fixer.replaceText(
36+
typeArguments,
37+
`<${queryInputToText(queryInput)}>`,
38+
);
39+
} else {
40+
yield fixer.insertTextAfter(
41+
node.callee,
42+
`<${queryInputToText(queryInput)}>`,
43+
);
8644
}
45+
},
46+
});
47+
return;
48+
}
49+
50+
if (isDeclaredTypeCorrect(queryInput, inputParam)) {
51+
return;
52+
}
53+
54+
const members =
55+
inputParam.type === TSESTree.AST_NODE_TYPES.TSTypeLiteral
56+
? inputParam.members
57+
: inputParam.type === TSESTree.AST_NODE_TYPES.TSTupleType
58+
? inputParam.elementTypes.find(
59+
(element) =>
60+
element.type === TSESTree.AST_NODE_TYPES.TSTypeLiteral,
61+
)?.members
62+
: null;
63+
64+
const userDeclaredTypes = new Map<string, string>();
65+
66+
if (members) {
67+
for (const member of members) {
68+
if (member.type !== TSESTree.AST_NODE_TYPES.TSPropertySignature) {
69+
continue;
70+
}
8771

88-
const name =
89-
member.key.type === TSESTree.AST_NODE_TYPES.Identifier
90-
? member.key.name
91-
: ASTUtils.getStringIfConstant(member.key);
72+
if (!member.typeAnnotation) {
73+
continue;
74+
}
9275

93-
if (!name) {
94-
continue;
95-
}
76+
const name =
77+
member.key.type === TSESTree.AST_NODE_TYPES.Identifier
78+
? member.key.name
79+
: ASTUtils.getStringIfConstant(member.key);
9680

97-
const type = context.sourceCode.getText(member.typeAnnotation);
98-
userDeclaredTypes.set(name, type);
81+
if (!name) {
82+
continue;
9983
}
100-
}
10184

102-
context.report({
103-
node: inputParam,
104-
messageId: "incorrectInputType",
105-
*fix(fixer) {
106-
yield fixer.replaceText(
107-
inputParam,
108-
queryInputToText(queryInput, userDeclaredTypes),
109-
);
110-
},
111-
});
112-
},
113-
};
114-
},
115-
meta: {
116-
messages: {
117-
missingInputType: "Missing input type for query",
118-
incorrectInputType: "Incorrect input type for query",
85+
const type = context.sourceCode.getText(member.typeAnnotation);
86+
userDeclaredTypes.set(name, type);
87+
}
88+
}
89+
90+
context.report({
91+
node: inputParam,
92+
messageId: "incorrectInputType",
93+
*fix(fixer) {
94+
yield fixer.replaceText(
95+
inputParam,
96+
queryInputToText(queryInput, userDeclaredTypes),
97+
);
98+
},
99+
});
119100
},
120-
schema: [],
121-
type: "suggestion",
122-
fixable: "code",
101+
};
102+
},
103+
meta: {
104+
messages: {
105+
missingInputType: "Missing input type for query",
106+
incorrectInputType: "Incorrect input type for query",
123107
},
124-
defaultOptions: [],
125-
});
126-
}
108+
schema: [],
109+
type: "suggestion",
110+
fixable: "code",
111+
},
112+
defaultOptions: [],
113+
});
127114

128115
function queryInputToText(
129116
queryInput: QueryInput,

tests/inferQueryInput.test.ts

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,14 @@
11
import { inferQueryInput } from "../src/inferQueryInput.js";
22
import { it, expect } from "vitest";
3-
import SQLite from "better-sqlite3";
4-
5-
function testInferQueryInput(source: string, query: string) {
6-
const db = new SQLite(":memory:");
7-
8-
if (source) {
9-
db.exec(source);
10-
}
11-
12-
const result = inferQueryInput(query, db);
13-
db.close();
14-
return result;
15-
}
163

174
it("should ignore invalid queries", () => {
18-
const result = testInferQueryInput("", "SELECT * FROM");
5+
const result = inferQueryInput("SELECT * FROM");
196

207
expect(result).toStrictEqual<typeof result>(null);
218
});
229

2310
it("should support anonymous parameters", () => {
24-
const result = testInferQueryInput(
25-
"CREATE TABLE foo (bar int)",
26-
"SELECT * FROM foo WHERE bar = ?",
27-
);
11+
const result = inferQueryInput("SELECT * FROM foo WHERE bar = ?");
2812

2913
expect(result).toStrictEqual<typeof result>({
3014
count: 1,
@@ -33,8 +17,7 @@ it("should support anonymous parameters", () => {
3317
});
3418

3519
it("should support named parameters", () => {
36-
const result = testInferQueryInput(
37-
"CREATE TABLE foo (bar int)",
20+
const result = inferQueryInput(
3821
"SELECT * FROM foo WHERE bar = :bar or bar = :bar2",
3922
);
4023

@@ -45,8 +28,7 @@ it("should support named parameters", () => {
4528
});
4629

4730
it("should support both anonymous and named parameters", () => {
48-
const result = testInferQueryInput(
49-
"CREATE TABLE foo (bar int)",
31+
const result = inferQueryInput(
5032
"SELECT * FROM foo WHERE bar = ? or bar = :bar",
5133
);
5234

@@ -57,8 +39,7 @@ it("should support both anonymous and named parameters", () => {
5739
});
5840

5941
it("should deduplicate named parameters", () => {
60-
const result = testInferQueryInput(
61-
"CREATE TABLE foo (bar int)",
42+
const result = inferQueryInput(
6243
"SELECT * FROM foo WHERE bar = $bar or bar = :bar or bar = @bar",
6344
);
6445

@@ -69,8 +50,7 @@ it("should deduplicate named parameters", () => {
6950
});
7051

7152
it("should handle ?NNN parameters", () => {
72-
const result = testInferQueryInput(
73-
"CREATE TABLE foo (bar int)",
53+
const result = inferQueryInput(
7454
"SELECT * FROM foo WHERE bar = ? or bar = ?1 or bar = ?2",
7555
);
7656

tests/rules/typed-input.test.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,7 @@
1-
import { createTypedInputRule } from "../../src/rules/typed-input.js";
2-
import SQLite from "better-sqlite3";
1+
import { typedInputRule } from "../../src/rules/typed-input.js";
32
import { ruleTester } from "./rule-tester.js";
43

5-
const db = new SQLite(":memory:");
6-
db.exec(`
7-
CREATE TABLE users (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL);
8-
`);
9-
10-
const rule = createTypedInputRule({
11-
getDatabase() {
12-
return db;
13-
},
14-
});
15-
16-
ruleTester.run("typed-input", rule, {
4+
ruleTester.run("typed-input", typedInputRule, {
175
valid: [
186
// Shouldn't match if query can't be determined
197
"db.prepare(true)",

0 commit comments

Comments
 (0)