diff --git a/.changeset/young-pandas-explode.md b/.changeset/young-pandas-explode.md new file mode 100644 index 0000000000..4ad3b4e674 --- /dev/null +++ b/.changeset/young-pandas-explode.md @@ -0,0 +1,7 @@ +--- +"@latticexyz/store-sync": major +--- + +Postgres storage adapter now uses snake case for decoded table names and column names. This allows for better SQL ergonomics when querying these tables. + +To avoid naming conflicts for now, schemas are still case-sensitive and need to be queried with double quotes. We may change this in the future with [namespace validation](https://github.com/latticexyz/mud/issues/1991). diff --git a/packages/store-sync/package.json b/packages/store-sync/package.json index e85e8b6481..048c5dde88 100644 --- a/packages/store-sync/package.json +++ b/packages/store-sync/package.json @@ -63,6 +63,7 @@ "@latticexyz/world": "workspace:*", "@trpc/client": "10.34.0", "@trpc/server": "10.34.0", + "change-case": "^5.2.0", "debug": "^4.3.4", "drizzle-orm": "^0.28.5", "kysely": "^0.26.3", diff --git a/packages/store-sync/src/postgres-decoded/buildTable.test.ts b/packages/store-sync/src/postgres-decoded/buildTable.test.ts index f3caa72c97..9f2e75a057 100644 --- a/packages/store-sync/src/postgres-decoded/buildTable.test.ts +++ b/packages/store-sync/src/postgres-decoded/buildTable.test.ts @@ -8,14 +8,14 @@ describe("buildTable", () => { it("should create table from schema", async () => { const table = buildTable({ address: "0xffffffffffffffffffffffffffffffffffffffff", - namespace: "test", - name: "users", + namespace: "testNS", + name: "UsersTable", keySchema: { x: "uint32", y: "uint32" }, - valueSchema: { name: "string", addr: "address" }, + valueSchema: { name: "string", walletAddress: "address" }, }); - expect(getTableConfig(table).schema).toMatch(/^test_\d+__0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF__test$/); - expect(getTableConfig(table).name).toMatchInlineSnapshot('"users"'); + expect(getTableConfig(table).schema).toMatch(/0xffffffffffffffffffffffffffffffffffffffff__testNS$/); + expect(getTableConfig(table).name).toMatchInlineSnapshot('"users_table"'); expect( mapObject(getTableColumns(table), (column) => ({ name: column.name, @@ -37,18 +37,18 @@ describe("buildTable", () => { "notNull": false, "sqlName": "numeric", }, - "addr": { - "dataType": "custom", - "name": "addr", - "notNull": true, - "sqlName": "bytea", - }, "name": { "dataType": "string", "name": "name", "notNull": true, "sqlName": undefined, }, + "walletAddress": { + "dataType": "custom", + "name": "wallet_address", + "notNull": true, + "sqlName": "bytea", + }, "x": { "dataType": "custom", "name": "x", @@ -68,14 +68,14 @@ describe("buildTable", () => { it("can create a singleton table", async () => { const table = buildTable({ address: "0xffffffffffffffffffffffffffffffffffffffff", - namespace: "test", - name: "users", + namespace: "testNS", + name: "UsersTable", keySchema: {}, valueSchema: { addrs: "address[]" }, }); - expect(getTableConfig(table).schema).toMatch(/^test_\d+__0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF__test$/); - expect(getTableConfig(table).name).toMatchInlineSnapshot('"users"'); + expect(getTableConfig(table).schema).toMatch(/0xffffffffffffffffffffffffffffffffffffffff__testNS$/); + expect(getTableConfig(table).name).toMatchInlineSnapshot('"users_table"'); expect( mapObject(getTableColumns(table), (column) => ({ name: column.name, diff --git a/packages/store-sync/src/postgres-decoded/buildTable.ts b/packages/store-sync/src/postgres-decoded/buildTable.ts index 131f5c3d58..2f0a6f336c 100644 --- a/packages/store-sync/src/postgres-decoded/buildTable.ts +++ b/packages/store-sync/src/postgres-decoded/buildTable.ts @@ -1,5 +1,6 @@ import { PgColumnBuilderBase, PgTableWithColumns, pgSchema } from "drizzle-orm/pg-core"; import { Address, getAddress } from "viem"; +import { snakeCase } from "change-case"; import { KeySchema, ValueSchema } from "@latticexyz/protocol-parser"; import { asBigInt, asHex } from "../postgres/columnTypes"; import { transformSchemaName } from "../postgres/transformSchemaName"; @@ -46,14 +47,19 @@ export function buildTable): BuildTableResult { - const schemaName = transformSchemaName(`${getAddress(address)}__${namespace}`); + // We intentionally do not snake case the namespace due to potential conflicts + // with namespaces of a similar name (e.g. `MyNamespace` vs. `my_namespace`). + // TODO: consider snake case when we resolve https://github.com/latticexyz/mud/issues/1991 + const schemaName = transformSchemaName(`${address.toLowerCase()}__${namespace}`); + const tableName = snakeCase(name); + + // Column names, however, are safe to snake case because they're scoped to tables, defined once per table, and there's a limited number of fields in total. const keyColumns = Object.fromEntries( - Object.entries(keySchema).map(([name, type]) => [name, buildColumn(name, type).notNull()]) + Object.entries(keySchema).map(([name, type]) => [name, buildColumn(snakeCase(name), type).notNull()]) ); - const valueColumns = Object.fromEntries( - Object.entries(valueSchema).map(([name, type]) => [name, buildColumn(name, type).notNull()]) + Object.entries(valueSchema).map(([name, type]) => [name, buildColumn(snakeCase(name), type).notNull()]) ); // TODO: make sure there are no meta columns that overlap with key/value columns @@ -64,7 +70,7 @@ export function buildTable; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 29d5e51947..a26c8d9ab8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -933,6 +933,9 @@ importers: '@trpc/server': specifier: 10.34.0 version: 10.34.0 + change-case: + specifier: ^5.2.0 + version: 5.2.0 debug: specifier: ^4.3.4 version: 4.3.4(supports-color@8.1.1) @@ -4561,6 +4564,10 @@ packages: engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} dev: false + /change-case@5.2.0: + resolution: {integrity: sha512-L6VzznESnMIKKdKhVzCG+KPz4+x1FWbjOs1AdhoHStV3qo8aySMRGPUoqC0aL1ThKaQNGhAu6ZfHL/QAyQRuiw==} + dev: false + /char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'}