diff --git a/.changeset/seven-wasps-happen.md b/.changeset/seven-wasps-happen.md new file mode 100644 index 0000000..366e79d --- /dev/null +++ b/.changeset/seven-wasps-happen.md @@ -0,0 +1,6 @@ +--- +'@emigrate/postgres': minor +'@emigrate/mysql': minor +--- + +Automatically create the database if it doesn't exist, and the user have the permissions to do so diff --git a/packages/mysql/src/index.ts b/packages/mysql/src/index.ts index 2a1354e..5b97560 100644 --- a/packages/mysql/src/index.ts +++ b/packages/mysql/src/index.ts @@ -9,6 +9,7 @@ import { type Pool, type ResultSetHeader, type RowDataPacket, + type Connection, } from 'mysql2/promise'; import { getTimestampPrefix, sanitizeMigrationName } from '@emigrate/plugin-tools'; import { @@ -155,6 +156,66 @@ const deleteMigration = async (pool: Pool, table: string, migration: MigrationMe return result.affectedRows === 1; }; +const getDatabaseName = (config: ConnectionOptions | string) => { + if (typeof config === 'string') { + const uri = new URL(config); + + return uri.pathname.replace(/^\//u, ''); + } + + return config.database ?? ''; +}; + +const setDatabaseName = (config: T, databaseName: string): T => { + if (typeof config === 'string') { + const uri = new URL(config); + + uri.pathname = `/${databaseName}`; + + return uri.toString() as T; + } + + if (typeof config === 'object') { + return { + ...config, + database: databaseName, + }; + } + + throw new Error('Invalid connection config'); +}; + +const initializeDatabase = async (config: ConnectionOptions | string) => { + let connection: Connection | undefined; + + try { + connection = await getConnection(config); + await connection.query('SELECT 1'); + await connection.end(); + } catch (error) { + await connection?.end(); + + // The ER_BAD_DB_ERROR error code is thrown when the database does not exist but the user might have the permissions to create it + // Otherwise the error code is ER_DBACCESS_DENIED_ERROR + if (error && typeof error === 'object' && 'code' in error && error.code === 'ER_BAD_DB_ERROR') { + const databaseName = getDatabaseName(config); + + const informationSchemaConfig = setDatabaseName(config, 'information_schema'); + + const informationSchemaConnection = await getConnection(informationSchemaConfig); + try { + await informationSchemaConnection.query(`CREATE DATABASE ${escapeId(databaseName)}`); + // Any database creation error here will be propagated + } finally { + await informationSchemaConnection.end(); + } + } else { + // In this case we don't know how to handle the error, so we rethrow it + throw error; + } + } +}; + const initializeTable = async (pool: Pool, table: string) => { const [result] = await pool.execute({ sql: ` @@ -186,6 +247,8 @@ const initializeTable = async (pool: Pool, table: string) => { export const createMysqlStorage = ({ table = defaultTable, connection }: MysqlStorageOptions): EmigrateStorage => { return { async initializeStorage() { + await initializeDatabase(connection); + const pool = getPool(connection); if (process.isBun) { @@ -196,8 +259,6 @@ export const createMysqlStorage = ({ table = defaultTable, connection }: MysqlSt }); } - await pool.query('SELECT 1'); - try { await initializeTable(pool, table); } catch (error) { diff --git a/packages/postgres/src/index.ts b/packages/postgres/src/index.ts index b6e3e4c..44af95b 100644 --- a/packages/postgres/src/index.ts +++ b/packages/postgres/src/index.ts @@ -92,6 +92,64 @@ const deleteMigration = async (sql: Sql, table: string, migration: MigrationMeta return result.count === 1; }; +const getDatabaseName = (config: ConnectionOptions | string) => { + if (typeof config === 'string') { + const uri = new URL(config); + + return uri.pathname.replace(/^\//u, ''); + } + + return config.database ?? ''; +}; + +const setDatabaseName = (config: T, databaseName: string): T => { + if (typeof config === 'string') { + const uri = new URL(config); + + uri.pathname = `/${databaseName}`; + + return uri.toString() as T; + } + + if (typeof config === 'object') { + return { + ...config, + database: databaseName, + }; + } + + throw new Error('Invalid connection config'); +}; + +const initializeDatabase = async (config: ConnectionOptions | string) => { + let sql: Sql | undefined; + + try { + sql = await getPool(config); + await sql.end(); + } catch (error) { + await sql?.end(); + + // The error code 3D000 means that the database does not exist, but the user might have the permissions to create it + if (error && typeof error === 'object' && 'code' in error && error.code === '3D000') { + const databaseName = getDatabaseName(config); + + const postgresConfig = setDatabaseName(config, 'postgres'); + + const postgresSql = await getPool(postgresConfig); + try { + await postgresSql`CREATE DATABASE ${postgresSql(databaseName)}`; + // Any database creation error here will be propagated + } finally { + await postgresSql.end(); + } + } else { + // In this case we don't know how to handle the error, so we rethrow it + throw error; + } + } +}; + const initializeTable = async (sql: Sql, table: string) => { const [row] = await sql>` SELECT 1 as exists @@ -122,6 +180,8 @@ export const createPostgresStorage = ({ }: PostgresStorageOptions): EmigrateStorage => { return { async initializeStorage() { + await initializeDatabase(connection); + const sql = await getPool(connection); try {