feat(db): support postgresql database (#3643)

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
Yuichi Nakai
2025-08-30 03:30:03 +09:00
committed by GitHub
parent a4aa2aea90
commit 5168cba8e4
22 changed files with 3603 additions and 32 deletions

View File

@@ -0,0 +1,46 @@
import path from "path";
import { PostgreSqlContainer } from "@testcontainers/postgresql";
import { drizzle } from "drizzle-orm/node-postgres";
import { migrate } from "drizzle-orm/node-postgres/migrator";
import { Pool } from "pg";
import { describe, test } from "vitest";
import * as pgSchema from "../schema/postgresql";
describe("PostgreSql Migration", () => {
test("should add all tables and keys specified in migration files", async () => {
const container = new PostgreSqlContainer("postgres:latest");
const postgreSqlContainer = await container.start();
const pool = new Pool({
user: postgreSqlContainer.getUsername(),
database: postgreSqlContainer.getDatabase(),
password: postgreSqlContainer.getPassword(),
port: postgreSqlContainer.getPort(),
host: postgreSqlContainer.getHost(),
keepAlive: true,
max: 0,
idleTimeoutMillis: 60000,
allowExitOnIdle: false,
});
const database = drizzle({
schema: pgSchema,
casing: "snake_case",
client: pool,
});
// Run migrations and check if it works
await migrate(database, {
migrationsFolder: path.join(__dirname, "..", "migrations", "postgresql"),
});
// Check if users table exists
await database.query.users.findMany();
// Close the pool to release resources
await pool.end();
// Stop the container
await postgreSqlContainer.stop();
}, 40_000);
});

View File

@@ -1,15 +1,17 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type { Column, InferSelectModel } from "drizzle-orm";
import type { ForeignKey as MysqlForeignKey, MySqlTableWithColumns } from "drizzle-orm/mysql-core";
import type { PgTableWithColumns, ForeignKey as PostgresqlForeignKey } from "drizzle-orm/pg-core";
import type { ForeignKey as SqliteForeignKey, SQLiteTableWithColumns } from "drizzle-orm/sqlite-core";
import { expect, expectTypeOf, test } from "vitest";
import { objectEntries } from "@homarr/common";
import * as mysqlSchema from "../schema/mysql";
import * as postgresqlSchema from "../schema/postgresql";
import * as sqliteSchema from "../schema/sqlite";
// We need the following two types as there is currently no support for Buffer in mysql and
// We need the following three types as there is currently no support for Buffer in mysql & pg and
// so we use a custom type which results in the config beeing different
type FixedMysqlConfig = {
[key in keyof MysqlConfig]: {
@@ -22,6 +24,22 @@ type FixedMysqlConfig = {
};
};
type FixedPostgresqlConfig = {
[key in keyof PostgreisqlConfig]: {
[column in keyof PostgreisqlConfig[key]]: {
[property in Exclude<
keyof PostgreisqlConfig[key][column],
"dataType" | "data"
>]: PostgreisqlConfig[key][column][property];
} & {
dataType: PostgreisqlConfig[key][column]["data"] extends Buffer
? "buffer"
: PostgreisqlConfig[key][column]["dataType"];
data: PostgreisqlConfig[key][column]["data"] extends Buffer ? Buffer : PostgreisqlConfig[key][column]["data"];
};
};
};
type FixedSqliteConfig = {
[key in keyof SqliteConfig]: {
[column in keyof SqliteConfig[key]]: {
@@ -117,6 +135,91 @@ test("schemas should match", () => {
});
});
test("schemas should match for postgresql", () => {
expectTypeOf<SqliteTables>().toEqualTypeOf<PostgresqlTables>();
expectTypeOf<PostgresqlTables>().toEqualTypeOf<SqliteTables>();
expectTypeOf<FixedSqliteConfig>().toEqualTypeOf<FixedPostgresqlConfig>();
expectTypeOf<FixedPostgresqlConfig>().toEqualTypeOf<FixedSqliteConfig>();
objectEntries(sqliteSchema).forEach(([tableName, sqliteTable]) => {
// keys of sqliteSchema and postgresqlSchema are the same, so we can safely use tableName as key
// skipcq: JS-E1007
const postgresqlTable = postgresqlSchema[tableName];
Object.entries(sqliteTable).forEach(([columnName, sqliteColumn]: [string, object]) => {
if (!("isUnique" in sqliteColumn)) return;
if (!("uniqueName" in sqliteColumn)) return;
if (!("primary" in sqliteColumn)) return;
const postgresqlColumn = postgresqlTable[columnName as keyof typeof postgresqlTable] as object;
if (!("isUnique" in postgresqlColumn)) return;
if (!("uniqueName" in postgresqlColumn)) return;
if (!("primary" in postgresqlColumn)) return;
expect(
sqliteColumn.isUnique,
`expect unique of column ${columnName} in table ${tableName} to be the same for both schemas`,
).toEqual(postgresqlColumn.isUnique);
expect(
sqliteColumn.uniqueName,
`expect uniqueName of column ${columnName} in table ${tableName} to be the same for both schemas`,
).toEqual(postgresqlColumn.uniqueName);
expect(
sqliteColumn.primary,
`expect primary of column ${columnName} in table ${tableName} to be the same for both schemas`,
).toEqual(postgresqlColumn.primary);
});
const sqliteForeignKeys = sqliteTable[Symbol.for("drizzle:SQLiteInlineForeignKeys") as keyof typeof sqliteTable] as
| SqliteForeignKey[]
| undefined;
const postgresqlForeignKeys = postgresqlTable[
Symbol.for("drizzle:PgInlineForeignKeys") as keyof typeof postgresqlTable
] as PostgresqlForeignKey[] | undefined;
if (!sqliteForeignKeys && !postgresqlForeignKeys) return;
expect(postgresqlForeignKeys, `postgresql foreign key for ${tableName} to be defined`).toBeDefined();
expect(sqliteForeignKeys, `sqlite foreign key for ${tableName} to be defined`).toBeDefined();
expect(
sqliteForeignKeys!.length,
`expect number of foreign keys in table ${tableName} to be the same for both schemas`,
).toEqual(postgresqlForeignKeys?.length);
sqliteForeignKeys?.forEach((sqliteForeignKey) => {
sqliteForeignKey.getName();
const postgresqlForeignKey = postgresqlForeignKeys!.find((key) => key.getName() === sqliteForeignKey.getName());
expect(
postgresqlForeignKey,
`expect foreign key ${sqliteForeignKey.getName()} to be defined in postgresql schema`,
).toBeDefined();
// In PostgreSql, onDelete is "no action" by default, so it is treated as undefined to match Sqlite.
expect(
sqliteForeignKey.onDelete,
`expect foreign key (${sqliteForeignKey.getName()}) onDelete to be the same for both schemas`,
).toEqual(postgresqlForeignKey!.onDelete === "no action" ? undefined : postgresqlForeignKey!.onDelete);
// In PostgreSql, onUpdate is "no action" by default, so it is treated as undefined to match Sqlite.
expect(
sqliteForeignKey.onUpdate,
`expect foreign key (${sqliteForeignKey.getName()}) onUpdate to be the same for both schemas`,
).toEqual(postgresqlForeignKey!.onUpdate === "no action" ? undefined : postgresqlForeignKey!.onUpdate);
sqliteForeignKey.reference().foreignColumns.forEach((column) => {
expect(
postgresqlForeignKey!.reference().foreignColumns.map((column) => column.name),
`expect foreign key (${sqliteForeignKey.getName()}) columns to be the same for both schemas`,
).toContainEqual(column.name);
});
expect(
Object.keys(sqliteForeignKey.reference().foreignTable),
`expect foreign key (${sqliteForeignKey.getName()}) table to be the same for both schemas`,
).toEqual(Object.keys(postgresqlForeignKey!.reference().foreignTable).filter((key) => key !== "enableRLS"));
});
});
});
type SqliteTables = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[K in keyof typeof sqliteSchema]: (typeof sqliteSchema)[K] extends SQLiteTableWithColumns<any>
@@ -130,6 +233,13 @@ type MysqlTables = {
: never;
};
type PostgresqlTables = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[K in keyof typeof postgresqlSchema]: (typeof postgresqlSchema)[K] extends PgTableWithColumns<any>
? InferSelectModel<(typeof postgresqlSchema)[K]>
: never;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type InferColumnConfig<T extends Column<any, object>> =
T extends Column<infer C, object> ? Omit<C, "columnType" | "enumValues" | "driverParam"> : never;
@@ -155,3 +265,14 @@ type MysqlConfig = {
}
: never;
};
type PostgreisqlConfig = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[K in keyof typeof postgresqlSchema]: (typeof postgresqlSchema)[K] extends PgTableWithColumns<any>
? {
[C in keyof (typeof postgresqlSchema)[K]["_"]["config"]["columns"]]: InferColumnConfig<
(typeof postgresqlSchema)[K]["_"]["config"]["columns"][C]
>;
}
: never;
};