diff --git a/.changeset/hip-camels-learn.md b/.changeset/hip-camels-learn.md
new file mode 100644
index 0000000..ca95dbf
--- /dev/null
+++ b/.changeset/hip-camels-learn.md
@@ -0,0 +1,5 @@
+---
+'@emigrate/postgres': minor
+---
+
+Implement the first version of the @emigrate/postgres plugin
diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs
index 00937fe..6d0a145 100644
--- a/docs/astro.config.mjs
+++ b/docs/astro.config.mjs
@@ -117,6 +117,10 @@ export default defineConfig({
label: 'File System',
link: '/plugins/storage/file-system/',
},
+ {
+ label: 'PostgreSQL',
+ link: '/plugins/storage/postgres/',
+ },
{
label: 'MySQL',
link: '/plugins/storage/mysql/',
@@ -135,6 +139,10 @@ export default defineConfig({
label: 'Default Loader',
link: '/plugins/loaders/default/',
},
+ {
+ label: 'PostgreSQL Loader',
+ link: '/plugins/loaders/postgres/',
+ },
{
label: 'MySQL Loader',
link: '/plugins/loaders/mysql/',
@@ -171,6 +179,10 @@ export default defineConfig({
label: 'JavaScript Generator',
link: '/plugins/generators/js/',
},
+ {
+ label: 'PostgreSQL Generator',
+ link: '/plugins/generators/postgres/',
+ },
{
label: 'MySQL Generator',
link: '/plugins/generators/mysql/',
diff --git a/docs/src/content/docs/intro/quick-start.mdx b/docs/src/content/docs/intro/quick-start.mdx
index 5017867..76cf108 100644
--- a/docs/src/content/docs/intro/quick-start.mdx
+++ b/docs/src/content/docs/intro/quick-start.mdx
@@ -41,22 +41,22 @@ But for now, this is the way to go.
Emigrate uses a storage plugin to store the migration history.
-Install the plugin you want to use, for example:
+Install the plugin you want to use, for example the PostgreSQL Storage:
```bash
- npm install @emigrate/mysql
+ npm install @emigrate/postgres
```
```bash
- pnpm add @emigrate/mysql
+ pnpm add @emigrate/postgres
```
```bash
- yarn add @emigrate/mysql
+ yarn add @emigrate/postgres
```
@@ -66,7 +66,7 @@ Install the plugin you want to use, for example:
Create a new migration file in your project using:
```bash title="Create a new migration file"
-npx emigrate new --plugin mysql create users table
+npx emigrate new --plugin postgres create users table
```
```txt title="Output"
@@ -79,12 +79,12 @@ Emigrate new v0.10.0 /your/project/path
```
:::note
-The `mysql` plugin is used here to generate a migration file with the `.sql` extension.
+The `postgres` plugin is used here to generate a migration file with the `.sql` extension.
Otherwise the file would have the `.js` extension by default.
:::
:::tip[Did you know?]
-You can avoid typing `--plugin mysql` by configuring Emigrate using an `emigrate.config.js` file.
+You can avoid typing `--plugin postgres` by configuring Emigrate using an `emigrate.config.js` file.
See Configuration for more information.
:::
@@ -95,10 +95,9 @@ Open the migration file in your editor and fill it with your SQL query:
```sql title="migrations/20231215125421364_create_users_table.sql" {2-7}
-- Migration: create users table
CREATE TABLE users (
- id INT NOT NULL AUTO_INCREMENT,
+ id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
- email VARCHAR(255) NOT NULL,
- PRIMARY KEY (id)
+ email VARCHAR(255) NOT NULL
);
```
@@ -111,7 +110,7 @@ There's no magic about the first line comment as when using Liquibase, it's just
To show both pending and already applied migrations (or previously failed), use the `list` command:
```bash title="Show all migrations"
-npx emigrate list --storage mysql
+npx emigrate list --storage postgres
```
```txt title="Example output"
@@ -129,7 +128,7 @@ Emigrate list v0.10.0 /your/project/path
A good way to test your configuration is to run the migrations in dry mode:
```bash title="Show pending migrations"
-npx emigrate up --storage mysql --plugin mysql --dry
+npx emigrate up --storage postgres --plugin postgres --dry
```
:::note
@@ -142,6 +141,6 @@ Be sure to configure the connection correctly and use the `--dry` flag to test y
:::
:::tip[Did you know?]
-In the example above the `@emigrate/mysql` plugin is used twice, once for the `--storage` option as a Storage Plugin
+In the example above the `@emigrate/postgres` plugin is used twice, once for the `--storage` option as a Storage Plugin
and once for the `--plugin` option as a Loader Plugin to be able to read `.sql` files.
:::
diff --git a/docs/src/content/docs/plugins/generators/index.mdx b/docs/src/content/docs/plugins/generators/index.mdx
index 3d737ca..de8c0da 100644
--- a/docs/src/content/docs/plugins/generators/index.mdx
+++ b/docs/src/content/docs/plugins/generators/index.mdx
@@ -17,6 +17,7 @@ The generator is responsible for generating migration files in a specific format
+
diff --git a/docs/src/content/docs/plugins/generators/postgres.mdx b/docs/src/content/docs/plugins/generators/postgres.mdx
new file mode 100644
index 0000000..ef7686d
--- /dev/null
+++ b/docs/src/content/docs/plugins/generators/postgres.mdx
@@ -0,0 +1,36 @@
+---
+title: "PostgreSQL Generator"
+---
+
+import { Tabs, TabItem } from '@astrojs/starlight/components';
+import Link from '@components/Link.astro';
+
+The PostgreSQL generator creates new migration files with the `.sql` extension. In the same package you can find the PostgreSQL Loader and the PostgreSQL Storage.
+
+## Installation
+
+
+
+ ```bash
+ npm install @emigrate/postgres
+ ```
+
+
+ ```bash
+ pnpm add @emigrate/postgres
+ ```
+
+
+ ```bash
+ yarn add @emigrate/postgres
+ ```
+
+
+
+## Usage
+
+```bash
+emigrate new --plugin postgres create some fancy table
+```
+
+For more information see the `new` command's documentation.
diff --git a/docs/src/content/docs/plugins/loaders/index.mdx b/docs/src/content/docs/plugins/loaders/index.mdx
index 288393a..bca094b 100644
--- a/docs/src/content/docs/plugins/loaders/index.mdx
+++ b/docs/src/content/docs/plugins/loaders/index.mdx
@@ -21,7 +21,7 @@ Or set it up in your configuration file, see default loader will be used for all other file types, and doesn't need to be specified.
:::
@@ -29,5 +29,6 @@ The default loader will be used fo
+
diff --git a/docs/src/content/docs/plugins/loaders/postgres.mdx b/docs/src/content/docs/plugins/loaders/postgres.mdx
new file mode 100644
index 0000000..38ec8ac
--- /dev/null
+++ b/docs/src/content/docs/plugins/loaders/postgres.mdx
@@ -0,0 +1,86 @@
+---
+title: PostgreSQL Loader Plugin
+---
+
+import { Tabs, TabItem } from '@astrojs/starlight/components';
+import Link from '@components/Link.astro';
+
+The PostgreSQL loader plugin transforms `.sql` files into JavaScript functions that Emigrate can use to execute the migrations. In the same package you can find the PostgreSQL Generator and the PostgreSQL Storage.
+
+## Installation
+
+
+
+ ```bash
+ npm install @emigrate/postgres
+ ```
+
+
+ ```bash
+ pnpm add @emigrate/postgres
+ ```
+
+
+ ```bash
+ yarn add @emigrate/postgres
+ ```
+
+
+
+## Configuration
+
+The PostgreSQL loader plugin can be configured either using environment variables or by configuring the plugin directly in the `emigrate.config.js` file.
+
+### Configuration file
+
+```js title="emigrate.config.js" {1,4-8}
+import { createPostgresLoader } from '@emigrate/postgres';
+
+export default {
+ plugins: [
+ createPostgresLoader({
+ connection: { ... },
+ }),
+ ],
+};
+```
+
+#### Options
+
+##### `connection` (required)
+
+**type:** `object | string`
+
+The connection options to use for connecting to the PostgreSQL database when the SQL statements from the migration files are executed. This can either be a connection URI or an object with connection options.
+For a list of supported connection options, see the [postgres documentation](https://github.com/porsager/postgres#connection).
+
+### Environment variables
+
+The following environment variables are supported:
+
+| Variable | Description | Default |
+| ------------------- | ----------------------------------------------------------------------------------------------------------- | ------------- |
+| `POSTGRES_URL` | The full URI for connecting to a PostgreSQL database, e.g: `"postgres://user:pass@127.0.0.1:3306/database"` | |
+| `POSTGRES_HOST` | The host on which the PostgreSQL server instance is running | `"localhost"` |
+| `POSTGRES_USER` | The PostgreSQL user account to use for the authentication | |
+| `POSTGRES_PASSWORD` | The PostgreSQL user password to use for the authentication | |
+| `POSTGRES_PORT` | The network port on which the PostgreSQL server is listening | `5432` |
+| `POSTGRES_DB` | The PostgreSQL database to use for the connection | |
+
+:::note
+The `POSTGRES_URL` environment variable takes precedence over the other environment variables. If `POSTGRES_URL` is set, the other environment variables are ignored.
+:::
+
+The environment variables are used when the plugin is used using the `--plugin` command line option:
+
+```bash
+npx emigrate list --plugin postgres
+```
+
+Or when specifying the plugin in the `emigrate.config.js` file as a string:
+
+```js title="emigrate.config.js" {2}
+export default {
+ plugins: ['postgres'],
+};
+```
diff --git a/docs/src/content/docs/plugins/storage/index.mdx b/docs/src/content/docs/plugins/storage/index.mdx
index 4188084..319969c 100644
--- a/docs/src/content/docs/plugins/storage/index.mdx
+++ b/docs/src/content/docs/plugins/storage/index.mdx
@@ -13,7 +13,7 @@ Usually you'll want to store the migration history in the same database as the o
You can specify a storage plugin via the `--storage` (or `-s` for short) option:
```bash
-npx emigrate list --storage mysql
+npx emigrate list --storage postgres
```
Or set it up in your configuration file, see Storage configuration for more information.
@@ -22,6 +22,7 @@ Or set it up in your configuration file, see
+
diff --git a/docs/src/content/docs/plugins/storage/postgres.mdx b/docs/src/content/docs/plugins/storage/postgres.mdx
new file mode 100644
index 0000000..368e894
--- /dev/null
+++ b/docs/src/content/docs/plugins/storage/postgres.mdx
@@ -0,0 +1,93 @@
+---
+title: PostgreSQL Storage
+---
+
+import { Tabs, TabItem } from '@astrojs/starlight/components';
+import Link from '@components/Link.astro';
+
+The PostgreSQL storage plugin uses a PostgreSQL database to store the migration history (*duh*). In the same package you can find the PostgreSQL Loader and the PostgreSQL Generator.
+
+## Installation
+
+
+
+ ```bash
+ npm install @emigrate/postgres
+ ```
+
+
+ ```bash
+ pnpm add @emigrate/postgres
+ ```
+
+
+ ```bash
+ yarn add @emigrate/postgres
+ ```
+
+
+
+## Configuration
+
+The PostgreSQL storage can be configured either using environment variables or by configuring the plugin directly in the `emigrate.config.js` file.
+
+### Configuration file
+
+```js title="emigrate.config.js" {1,4-7}
+import { createPostgresStorage } from '@emigrate/postgres';
+
+export default {
+ storage: createPostgresStorage({
+ table: 'migrations',
+ connection: { ... },
+ }),
+};
+```
+
+#### Options
+
+##### `table`
+
+**type:** `string`
+**default:** `"migrations"`
+
+The name of the table to use for storing the migrations.
+
+##### `connection` (required)
+
+**type:** `object | string`
+
+The connection options to use for connecting to the PostgreSQL database. This can either be a connection URI or an object with connection options.
+For a list of supported connection options, see the [postgres documentation](https://github.com/porsager/postgres#connection).
+
+### Environment variables
+
+The following environment variables are supported:
+
+| Variable | Description | Default |
+| ------------------- | ----------------------------------------------------------------------------------------------------------- | -------------- |
+| `POSTGRES_TABLE` | The name of the table to use for storing the migrations | `"migrations"` |
+| `POSTGRES_URL` | The full URI for connecting to a PostgreSQL database, e.g: `"postgres://user:pass@127.0.0.1:3306/database"` | |
+| `POSTGRES_HOST` | The host on which the PostgreSQL server instance is running | `"localhost"` |
+| `POSTGRES_USER` | The PostgreSQL user account to use for the authentication | |
+| `POSTGRES_PASSWORD` | The PostgreSQL user password to use for the authentication | |
+| `POSTGRES_PORT` | The network port on which the PostgreSQL server is listening | `5432` |
+| `POSTGRES_DB` | The PostgreSQL database to use for the connection | |
+
+:::note
+The `POSTGRES_URL` environment variable takes precedence over the other environment variables. If `POSTGRES_URL` is set, the other environment variables are ignored, except for `POSTGRES_TABLE`.
+:::
+
+The environment variables are used when the storage plugin is used using the `--storage` command line option:
+
+```bash
+npx emigrate list --storage postgres
+```
+
+Or when specifying the storage in the `emigrate.config.js` file as a string:
+
+```js title="emigrate.config.js" {2}
+export default {
+ storage: 'postgres',
+};
+```
diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts
index 53494b9..0ea469c 100644
--- a/packages/cli/src/cli.ts
+++ b/packages/cli/src/cli.ts
@@ -56,7 +56,7 @@ Options:
Examples:
emigrate up --directory src/migrations -s fs
- emigrate up -d ./migrations --storage @emigrate/storage-mysql
+ emigrate up -d ./migrations --storage @emigrate/mysql
emigrate up -d src/migrations -s postgres -r json --dry
`;
diff --git a/packages/mysql/README.md b/packages/mysql/README.md
index 4cf64f8..54abfe7 100644
--- a/packages/mysql/README.md
+++ b/packages/mysql/README.md
@@ -1,6 +1,6 @@
-# @emigrate/storage-mysql
+# @emigrate/mysql
-A MySQL plugin for Emigrate. Uses a MySQL database for storing migration history. Can load and generate .sql migration files.
+A MySQL plugin for Emigrate. Uses a MySQL database for storing the migration history. Can load and generate .sql migration files.
The table used for storing the migration history is compatible with the [immigration-mysql](https://github.com/joakimbeng/immigration-mysql) package, so you can use this together with the [@emigrate/cli](../cli) as a drop-in replacement for that package.
diff --git a/packages/postgres/README.md b/packages/postgres/README.md
new file mode 100644
index 0000000..1e397c3
--- /dev/null
+++ b/packages/postgres/README.md
@@ -0,0 +1,177 @@
+# @emigrate/postgres
+
+A PostgreSQL plugin for Emigrate. Uses a PostgreSQL database for storing the migration history. Can load and generate .sql migration files.
+
+The table used for storing the migration history is compatible with the [immigration-postgres](https://github.com/aboviq/immigration-postgres) package, so you can use this together with the [@emigrate/cli](../cli) as a drop-in replacement for that package.
+
+## Description
+
+This plugin is actually three different Emigrate plugins in one:
+
+1. A [storage plugin](#using-the-storage-plugin) for storing the migration history in a PostgreSQL database.
+2. A [loader plugin](#using-the-loader-plugin) for loading .sql migration files and be able to execute them as part of the migration process.
+3. A [generator plugin](#using-the-generator-plugin) for generating .sql migration files.
+
+## Installation
+
+Install the plugin in your project, alongside the Emigrate CLI:
+
+```bash
+npm install --save-dev @emigrate/cli @emigrate/postgres
+```
+
+## Usage
+
+### Using the storage plugin
+
+See [Options](#options) below for the default values and how to configure the plugin using environment variables.
+
+Configure the storage in your `emigrate.config.js` file:
+
+```js
+export default {
+ directory: 'migrations',
+ storage: 'postgres', // the @emigrate/ prefix is optional
+};
+```
+
+Or use the CLI options `--storage` (or `-s`)
+
+```bash
+emigrate up --storage postgres # the @emigrate/ prefix is optional
+```
+
+#### Storage plugin with custom options
+
+Configure the storage in your `emigrate.config.js` file by importing the `createPostgresStorage` function (see [Options](#options) for available options).
+
+In this mode the plugin will _not_ use any of the environment variables for configuration.
+
+```js
+import { createPostgresStorage } from '@emigrate/postgres';
+
+export default {
+ directory: 'migrations',
+ storage: createPostgresStorage({ table: 'migrations', connection: { ... } }), // All connection options are passed to postgres()
+};
+```
+
+Or use the CLI option `--storage` (or `-s`) and use environment variables (see [Options](#options) for available variables).
+
+```bash
+POSTGRES_URL=postgres://user:pass@host/db emigrate up --storage postgres # the @emigrate/ prefix is optional
+```
+
+### Using the loader plugin
+
+The loader plugin is used to transform .sql migration files into JavaScript functions that can be executed by the "up" command.
+
+See [Options](#options) below for the default values and how to configure the plugin using environment variables.
+
+Configure the loader in your `emigrate.config.js` file:
+
+```js
+export default {
+ directory: 'migrations',
+ plugins: ['postgres'], // the @emigrate/ prefix is optional
+};
+```
+
+Or by importing the default export from the plugin:
+
+```js
+import postgresPlugin from '@emigrate/postgres';
+
+export default {
+ directory: 'migrations',
+ plugins: [postgresPlugin],
+};
+```
+
+**NOTE:** Using the root level `plugins` option will load the plugin for all commands, which means the [generator plugin](#using-the-generator-plugin) will be used by default for the "new" command as well. If you only want to use the loader plugin, use the `up.plugins` option instead:
+
+```js
+export default {
+ directory: 'migrations',
+ up: {
+ plugins: ['postgres'], // the @emigrate/ prefix is optional
+ // or:
+ plugins: [import('@emigrate/postgres')],
+ },
+};
+```
+
+The loader plugin can also be loaded using the CLI option `--plugin` (or `-p`) together with the "up" command:
+
+```bash
+emigrate up --plugin postgres # the @emigrate/ prefix is optional
+```
+
+### Using the generator plugin
+
+The generator plugin is used to generate skeleton .sql migration files inside your migration directory.
+
+Configure the generator in your `emigrate.config.js` file:
+
+```js
+export default {
+ directory: 'migrations',
+ plugins: ['postgres'], // the @emigrate/ prefix is optional
+};
+```
+
+Or by importing the default export from the plugin:
+
+```js
+import postgresPlugin from '@emigrate/postgres';
+
+export default {
+ directory: 'migrations',
+ plugins: [postgresPlugin],
+};
+```
+
+**NOTE:** Using the root level `plugins` option will load the plugin for all commands, which means the [loader plugin](#using-the-loader-plugin) will be used by default for the "up" command as well. If you only want to use the generator plugin, use the `new.plugins` option instead:
+
+```js
+export default {
+ directory: 'migrations',
+ new: {
+ plugins: ['postgres'], // the @emigrate/ prefix is optional
+ // or:
+ plugins: [import('@emigrate/postgres')],
+ },
+};
+```
+
+The generator plugin can also be loaded using the CLI option `--plugin` (or `-p`) together with the "new" command:
+
+```bash
+emigrate new --plugin postgres My new migration file # the @emigrate/ prefix is optional
+```
+
+#### Loader plugin with custom options
+
+Configure the loader in your `emigrate.config.js` file by importing the `createPostgresLoader` function (see [Options](#options) for available options).
+
+In this mode the plugin will _not_ use any of the environment variables for configuration.
+
+```js
+import { createPostgresLoader } from '@emigrate/postgres';
+
+export default {
+ directory: 'migrations',
+ plugins: [
+ createPostgresLoader({ connection: { ... } }), // All connection options are passed to postgres()
+ ],
+};
+```
+
+## Options
+
+The storage plugin accepts the following options:
+
+| Option | Applies to | Description | Default | Environment variable |
+| ------------ | -------------------------- | -------------------------------------------------------------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------- |
+| `table` | storage plugin | The name of the table to use for storing the migrations. | `migrations` | `POSTGRES_TABLE` |
+| `connection` | storage and loader plugins | The connection options to pass to [`postgres()`](https://github.com/porsager/postgres#connection). | `{}` | `POSTGRES_URL` or `POSTGRES_HOST`, `POSTGRES_PORT`, `POSTGRES_USER`, `POSTGRES_PASSWORD` and `POSTGRES_DB` |
diff --git a/packages/postgres/package.json b/packages/postgres/package.json
new file mode 100644
index 0000000..82a6cb8
--- /dev/null
+++ b/packages/postgres/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "@emigrate/postgres",
+ "version": "0.0.0",
+ "publishConfig": {
+ "access": "public"
+ },
+ "description": "A PostgreSQL plugin for Emigrate. Uses a PostgreSQL database for storing migration history. Can load and generate .sql migration files.",
+ "main": "dist/index.js",
+ "types": "dist/index.d.js",
+ "type": "module",
+ "exports": {
+ ".": {
+ "import": "./dist/index.js",
+ "types": "./dist/index.d.ts"
+ }
+ },
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "build": "tsc --pretty",
+ "build:watch": "tsc --pretty --watch",
+ "lint": "xo --cwd=../.. $(pwd)"
+ },
+ "keywords": [
+ "emigrate",
+ "emigrate-storage",
+ "emigrate-loader",
+ "emigrate-plugin",
+ "emigrate-generator",
+ "migrations",
+ "postgres",
+ "postgresql"
+ ],
+ "author": "Aboviq AB (https://www.aboviq.com)",
+ "homepage": "https://github.com/aboviq/emigrate/tree/main/packages/postgres#readme",
+ "repository": "https://github.com/aboviq/emigrate/tree/main/packages/postgres",
+ "bugs": "https://github.com/aboviq/emigrate/issues",
+ "license": "MIT",
+ "dependencies": {
+ "@emigrate/plugin-tools": "workspace:*",
+ "@emigrate/types": "workspace:*",
+ "postgres": "3.4.3"
+ },
+ "devDependencies": {
+ "@emigrate/tsconfig": "workspace:*"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/packages/postgres/src/index.ts b/packages/postgres/src/index.ts
new file mode 100644
index 0000000..bcbe9e1
--- /dev/null
+++ b/packages/postgres/src/index.ts
@@ -0,0 +1,252 @@
+import process from 'node:process';
+import postgres, { type Options, type PostgresType, type Sql } from 'postgres';
+import { getTimestampPrefix, sanitizeMigrationName } from '@emigrate/plugin-tools';
+import {
+ type MigrationMetadata,
+ type EmigrateStorage,
+ type LoaderPlugin,
+ type Storage,
+ type MigrationMetadataFinished,
+ type GenerateMigrationFunction,
+ type GeneratorPlugin,
+ type SerializedError,
+ type MigrationHistoryEntry,
+} from '@emigrate/types';
+
+const defaultTable = 'migrations';
+
+type ConnectionOptions = Options>;
+
+export type PostgresStorageOptions = {
+ table?: string;
+ /**
+ * @see https://github.com/porsager/postgres#connection
+ */
+ connection: ConnectionOptions | string;
+};
+
+export type PostgresLoaderOptions = {
+ /**
+ * @see https://github.com/porsager/postgres#connection
+ */
+ connection: ConnectionOptions | string;
+};
+
+const getPool = (connection: ConnectionOptions | string) => {
+ if (typeof connection === 'string') {
+ return postgres(connection);
+ }
+
+ return postgres(connection);
+};
+
+const lockMigration = async (sql: Sql, table: string, migration: MigrationMetadata) => {
+ const result = await sql`
+ INSERT INTO ${sql(table)} (name, status, date)
+ VALUES (${migration.name}, ${'locked'}, NOW())
+ ON CONFLICT (name) DO NOTHING
+ `;
+
+ return result.count === 1;
+};
+
+const unlockMigration = async (sql: Sql, table: string, migration: MigrationMetadata) => {
+ const result = await sql`
+ DELETE FROM ${sql(table)}
+ WHERE
+ name = ${migration.name}
+ AND status = ${'locked'}
+ `;
+
+ return result.count === 1;
+};
+
+const finishMigration = async (
+ sql: Sql,
+ table: string,
+ migration: MigrationMetadataFinished,
+ _error?: SerializedError,
+) => {
+ const result = await sql`
+ UPDATE
+ ${sql(table)}
+ SET
+ status = ${migration.status},
+ date = NOW()
+ WHERE
+ name = ${migration.name}
+ AND status = ${'locked'}
+ `;
+
+ return result.count === 1;
+};
+
+const deleteMigration = async (sql: Sql, table: string, migration: MigrationMetadata) => {
+ const result = await sql`
+ DELETE FROM ${sql(table)}
+ WHERE
+ name = ${migration.name}
+ AND status <> ${'locked'}
+ `;
+
+ return result.count === 1;
+};
+
+const initializeTable = async (sql: Sql, table: string) => {
+ const [row] = await sql>`
+ SELECT 1 as exists
+ FROM
+ information_schema.tables
+ WHERE
+ table_schema = 'public'
+ AND table_name = ${table}
+ `;
+
+ if (row?.exists) {
+ return;
+ }
+
+ // This table definition is compatible with the one used by the immigration-postgres package
+ await sql`
+ CREATE TABLE ${sql(table)} (
+ name varchar(255) not null primary key,
+ status varchar(32),
+ date timestamptz not null
+ );
+ `;
+};
+
+export const createPostgresStorage = ({
+ table = defaultTable,
+ connection,
+}: PostgresStorageOptions): EmigrateStorage => {
+ return {
+ async initializeStorage() {
+ const sql = getPool(connection);
+
+ try {
+ await initializeTable(sql, table);
+ } catch (error) {
+ await sql.end();
+ throw error;
+ }
+
+ const storage: Storage = {
+ async lock(migrations) {
+ const lockedMigrations: MigrationMetadata[] = [];
+
+ for await (const migration of migrations) {
+ if (await lockMigration(sql, table, migration)) {
+ lockedMigrations.push(migration);
+ }
+ }
+
+ return lockedMigrations;
+ },
+ async unlock(migrations) {
+ for await (const migration of migrations) {
+ await unlockMigration(sql, table, migration);
+ }
+ },
+ async remove(migration) {
+ await deleteMigration(sql, table, migration);
+ },
+ async *getHistory() {
+ const query = sql>>`
+ SELECT
+ *
+ FROM
+ ${sql(table)}
+ WHERE
+ status <> ${'locked'}
+ ORDER BY
+ date ASC
+ `.cursor();
+
+ for await (const [row] of query) {
+ if (!row) {
+ continue;
+ }
+
+ if (row.status === 'failed') {
+ yield {
+ ...row,
+ error: { name: 'Error', message: 'Unknown error' },
+ };
+ continue;
+ }
+
+ yield row;
+ }
+ },
+ async onSuccess(migration) {
+ await finishMigration(sql, table, migration);
+ },
+ async onError(migration, error) {
+ await finishMigration(sql, table, migration, error);
+ },
+ async end() {
+ await sql.end();
+ },
+ };
+
+ return storage;
+ },
+ };
+};
+
+export const { initializeStorage } = createPostgresStorage({
+ table: process.env['POSTGRES_TABLE'],
+ connection: process.env['POSTGRES_URL'] ?? {
+ host: process.env['POSTGRES_HOST'],
+ port: process.env['POSTGRES_PORT'] ? Number.parseInt(process.env['POSTGRES_PORT'], 10) : undefined,
+ user: process.env['POSTGRES_USER'],
+ password: process.env['POSTGRES_PASSWORD'],
+ database: process.env['POSTGRES_DB'],
+ },
+});
+
+export const createPostgresLoader = ({ connection }: PostgresLoaderOptions): LoaderPlugin => {
+ return {
+ loadableExtensions: ['.sql'],
+ async loadMigration(migration) {
+ return async () => {
+ const sql = getPool(connection);
+
+ try {
+ // @ts-expect-error The "simple" option is not documented, but it exists
+ await sql.file(migration.filePath, { simple: true });
+ } finally {
+ await sql.end();
+ }
+ };
+ },
+ };
+};
+
+export const { loadableExtensions, loadMigration } = createPostgresLoader({
+ connection: process.env['POSTGRES_URL'] ?? {
+ host: process.env['POSTGRES_HOST'],
+ port: process.env['POSTGRES_PORT'] ? Number.parseInt(process.env['POSTGRES_PORT'], 10) : undefined,
+ user: process.env['POSTGRES_USER'],
+ password: process.env['POSTGRES_PASSWORD'],
+ database: process.env['POSTGRES_DB'],
+ },
+});
+
+export const generateMigration: GenerateMigrationFunction = async (name) => {
+ return {
+ filename: `${getTimestampPrefix()}_${sanitizeMigrationName(name)}.sql`,
+ content: `-- Migration: ${name}
+`,
+ };
+};
+
+const defaultExport: EmigrateStorage & LoaderPlugin & GeneratorPlugin = {
+ initializeStorage,
+ loadableExtensions,
+ loadMigration,
+ generateMigration,
+};
+
+export default defaultExport;
diff --git a/packages/postgres/tsconfig.json b/packages/postgres/tsconfig.json
new file mode 100644
index 0000000..1cfcebb
--- /dev/null
+++ b/packages/postgres/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "@emigrate/tsconfig/build.json",
+ "compilerOptions": {
+ "outDir": "dist"
+ },
+ "include": ["src"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1c78edd..9951810 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -148,6 +148,22 @@ importers:
specifier: workspace:*
version: link:../tsconfig
+ packages/postgres:
+ dependencies:
+ '@emigrate/plugin-tools':
+ specifier: workspace:*
+ version: link:../plugin-tools
+ '@emigrate/types':
+ specifier: workspace:*
+ version: link:../types
+ postgres:
+ specifier: 3.4.3
+ version: 3.4.3
+ devDependencies:
+ '@emigrate/tsconfig':
+ specifier: workspace:*
+ version: link:../tsconfig
+
packages/reporter-pino:
dependencies:
'@emigrate/types':
@@ -7030,6 +7046,11 @@ packages:
source-map-js: 1.0.2
dev: false
+ /postgres@3.4.3:
+ resolution: {integrity: sha512-iHJn4+M9vbTdHSdDzNkC0crHq+1CUdFhx+YqCE+SqWxPjm+Zu63jq7yZborOBF64c8pc58O5uMudyL1FQcHacA==}
+ engines: {node: '>=12'}
+ dev: false
+
/prebuild-install@7.1.1:
resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==}
engines: {node: '>=10'}