Skip to content

Commit 582f718

Browse files
karooolisalvrs
andauthored
feat(store-indexer): format sql queries (#3687)
Co-authored-by: alvarius <alvarius@lattice.xyz>
1 parent d621dc7 commit 582f718

6 files changed

Lines changed: 120 additions & 1070 deletions

File tree

.changeset/happy-melons-own.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@latticexyz/store-indexer": patch
3+
---
4+
5+
The local SQLite indexer now automatically converts camelCase column names to snake_case to comply with the SQL API.

packages/store-indexer/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"start:sqlite": "tsx src/bin/sqlite-indexer",
4646
"start:sqlite:local": "SQLITE_FILENAME=anvil.db RPC_HTTP_URL=http://127.0.0.1:8545 pnpm start:sqlite",
4747
"start:sqlite:testnet": "SQLITE_FILENAME=testnet.db RPC_HTTP_URL=https://rpc.holesky.redstone.xyz pnpm start:sqlite",
48-
"test": "tsc --noEmit",
48+
"test": "tsc --noEmit && vitest --run --passWithNoTests",
4949
"test:ci": "pnpm run test"
5050
},
5151
"dependencies": {
@@ -70,6 +70,7 @@
7070
"koa": "^2.15.4",
7171
"koa-bodyparser": "^4.4.1",
7272
"koa-compose": "^4.1.0",
73+
"node-sql-parser": "^5.3.3",
7374
"postgres": "3.3.5",
7475
"prom-client": "^15.1.2",
7576
"rxjs": "7.5.5",

packages/store-indexer/src/sqlite/apiRoutes.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { debug } from "../debug";
99
import { createBenchmark } from "@latticexyz/common";
1010
import { compress } from "../koa-middleware/compress";
1111
import { getTablesWithRecords } from "./getTablesWithRecords";
12+
import { formatSqlQuery } from "./formatSqlQuery";
1213

1314
type Props = {
1415
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -67,7 +68,8 @@ export function apiRoutes({ database, enableUnsafeQueryApi = false }: Props): Mi
6768

6869
const result = [];
6970
for (const { query } of queries) {
70-
const data = database.all(sql.raw(query)) as Record<string, unknown>[];
71+
const formattedQuery = formatSqlQuery(query);
72+
const data = database.all(sql.raw(formattedQuery)) as Record<string, unknown>[];
7173
if (!data || !Array.isArray(data)) {
7274
throw new Error("Invalid query result");
7375
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { describe, expect, it } from "vitest";
2+
import { formatSqlQuery } from "./formatSqlQuery";
3+
4+
describe("formatSqlQuery", () => {
5+
it("should convert camelCase column names to snake_case", () => {
6+
const input = "SELECT userId, firstName, lastName FROM users";
7+
const expected = 'SELECT "user_id", "first_name", "last_name" FROM "users"';
8+
expect(formatSqlQuery(input)).toBe(expected);
9+
});
10+
11+
it("should not modify non-camelCase column names", () => {
12+
const input = "SELECT user_id, first_name, last_name FROM users";
13+
const expected = 'SELECT "user_id", "first_name", "last_name" FROM "users"';
14+
expect(formatSqlQuery(input)).toBe(expected);
15+
});
16+
17+
it("should not modify camelCase words that are not column names", () => {
18+
const input = "SELECT * FROM users WHERE firstName LIKE '%NameSurname%'";
19+
const expected = 'SELECT * FROM "users" WHERE "first_name" LIKE \'%NameSurname%\'';
20+
expect(formatSqlQuery(input)).toBe(expected);
21+
});
22+
23+
it("should not modify table names", () => {
24+
const input = "SELECT * FROM usersTable";
25+
const expected = 'SELECT * FROM "usersTable"';
26+
expect(formatSqlQuery(input)).toBe(expected);
27+
28+
const input2 = "SELECT * FROM users_table";
29+
const expected2 = 'SELECT * FROM "users_table"';
30+
expect(formatSqlQuery(input2)).toBe(expected2);
31+
});
32+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import sqlParser from "node-sql-parser";
2+
3+
type AstNode = {
4+
type?: string;
5+
[key: string]: unknown;
6+
};
7+
8+
/**
9+
* Transforms camelCase identifiers to snake_case in SQL queries
10+
*
11+
* @param sqlQuery The SQL query to transform
12+
* @returns The transformed SQL query
13+
*/
14+
export function formatSqlQuery(sqlQuery: string): string {
15+
const parser = new sqlParser.Parser();
16+
const ast = parser.astify(sqlQuery);
17+
18+
updateNode(ast);
19+
20+
return parser.sqlify(ast, { database: "sqlite" });
21+
}
22+
23+
function updateNode(node: unknown): void {
24+
if (Array.isArray(node)) {
25+
node.forEach((item) => updateNode(item));
26+
return;
27+
}
28+
29+
if (node && typeof node === "object") {
30+
const astNode = node as AstNode;
31+
32+
if (
33+
astNode.type === "column_ref" &&
34+
typeof astNode.column === "string" &&
35+
/^[a-z]+[A-Z][a-z]*$/.test(astNode.column)
36+
) {
37+
astNode.column = camelToSnakeCase(astNode.column);
38+
}
39+
40+
for (const [, value] of Object.entries(astNode)) {
41+
if (typeof value === "object" && value !== null) {
42+
updateNode(value);
43+
}
44+
}
45+
}
46+
}
47+
48+
function camelToSnakeCase(str: string): string {
49+
return str.replace(/([a-z]+)([A-Z][a-z]*)/g, "$1_$2").toLowerCase();
50+
}

0 commit comments

Comments
 (0)