Commit Graph

49 Commits

Author SHA1 Message Date
Andrew Farries
aa0c6a42e5
Ignore duplicate inferred migrations having the same timestamp (#369)
Remove all but the last inferred migration in a group of inferred
migrations that occur at the same timestamp with the same migration SQL.

Such inferred migrations are typically part of a batched transaction for
which we want to ignore all but the effect of the last statement in the
batch.
2024-07-10 11:52:51 +01:00
Andrew Farries
c4e4ee3c22
Fix performance regression in previous_version function (#366)
https://github.com/xataio/pgroll/pull/365 changed the definition of
`previous_version` and introduced a performance regression.

Update the function with extra `WHERE` clauses so that the recursive CTE
can take better advantage of the table PK index.

On a large (~1M record) `pgroll.migrations` table the query analysis for
the recursive CTE is:

**Before**:

```
+----------------------------------------------------------------------------------------------------------------------------------------------------------+
| QUERY PLAN                                                                                                                                               |
|----------------------------------------------------------------------------------------------------------------------------------------------------------|
| CTE Scan on ancestors  (cost=1819977.60..1819978.62 rows=51 width=214) (actual time=1163.608..6463.980 rows=1 loops=1)                                   |
|   CTE ancestors                                                                                                                                          |
|     ->  Recursive Union  (cost=0.00..1819977.60 rows=51 width=114) (actual time=1163.606..6463.977 rows=1 loops=1)                                       |
|           ->  Seq Scan on migrations  (cost=0.00..442329.71 rows=1 width=114) (actual time=1163.604..6385.489 rows=1 loops=1)                            |
|                 Filter: (name = latest_version('bb_00bo06t68d5ot7i5k5m8um70kg_bb19qc'::name))                                                            |
|                 Rows Removed by Filter: 1193140                                                                                                          |
|           ->  Nested Loop  (cost=0.55..137764.74 rows=5 width=114) (actual time=78.485..78.486 rows=0 loops=1)                                           |
|                 ->  WorkTable Scan on ancestors a  (cost=0.00..0.20 rows=10 width=36) (actual time=0.001..0.002 rows=1 loops=1)                          |
|                 ->  Index Scan using migrations_pkey on migrations m  (cost=0.55..13776.44 rows=1 width=110) (actual time=78.459..78.459 rows=0 loops=1) |
|                       Index Cond: (name = a.parent)                                                                                                      |
|                       Filter: ((migration_type)::text = 'inferred'::text)                                                                                |
|                       Rows Removed by Filter: 1                                                                                                          |
| Planning Time: 0.764 ms                                                                                                                                  |
| JIT:                                                                                                                                                     |
|   Functions: 11                                                                                                                                          |
|   Options: Inlining true, Optimization true, Expressions true, Deforming true                                                                            |
|   Timing: Generation 2.222 ms, Inlining 29.422 ms, Optimization 51.028 ms, Emission 24.552 ms, Total 107.225 ms                                          |
| Execution Time: 6466.347 ms                                                                                                                              |
+----------------------------------------------------------------------------------------------------------------------------------------------------------+
```

**After**:

```
+----------------------------------------------------------------------------------------------------------------------------------------------------+
| QUERY PLAN                                                                                                                                         |
|----------------------------------------------------------------------------------------------------------------------------------------------------|
| CTE Scan on ancestors  (cost=987.99..988.21 rows=11 width=214) (actual time=0.277..0.379 rows=1 loops=1)                                           |
|   CTE ancestors                                                                                                                                    |
|     ->  Recursive Union  (cost=0.55..987.99 rows=11 width=114) (actual time=0.275..0.377 rows=1 loops=1)                                           |
|           ->  Index Scan using history_is_linear on migrations  (cost=0.55..128.36 rows=1 width=114) (actual time=0.274..0.354 rows=1 loops=1)     |
|                 Index Cond: (schema = 'bb_00bo06t68d5ot7i5k5m8um70kg_bb19qc'::name)                                                                |
|                 Filter: (name = latest_version('bb_00bo06t68d5ot7i5k5m8um70kg_bb19qc'::name))                                                      |
|                 Rows Removed by Filter: 5                                                                                                          |
|           ->  Nested Loop  (cost=0.55..85.95 rows=1 width=114) (actual time=0.020..0.020 rows=0 loops=1)                                           |
|                 ->  WorkTable Scan on ancestors a  (cost=0.00..0.20 rows=10 width=100) (actual time=0.001..0.001 rows=1 loops=1)                   |
|                 ->  Index Scan using migrations_pkey on migrations m  (cost=0.55..8.57 rows=1 width=110) (actual time=0.017..0.017 rows=0 loops=1) |
|                       Index Cond: ((schema = a.schema) AND (name = a.parent))                                                                      |
|                       Filter: ((migration_type)::text = 'inferred'::text)                                                                          |
|                       Rows Removed by Filter: 1                                                                                                    |
| Planning Time: 0.744 ms                                                                                                                            |
| Execution Time: 0.459 ms                                                                                                                           |
+----------------------------------------------------------------------------------------------------------------------------------------------------+
```

ie the recursive portion of the table is able to take advantage of the
table's PK (defined on `(schema, name)`).
2024-07-04 12:53:21 +01:00
Andrew Farries
3d6ac6d992
Add WithNoVersionSchemaForRawSQL option (#365)
Add a new `WithNoVersionSchemaForRawSQL` option to control whether or
not version schema should be created for raw SQL migrations.

With the option set, a raw SQL migration:
* Has no version schema created on migration start.
* Leaves the previous version schema in place on migration completion.
2024-07-01 16:34:01 +01:00
Andrew Farries
7cef8b1213
Distinguish inferred migrations by timestamp for statements within the same transaction (#362)
Set `created_at` and `updated_at` explicitly when inserting inferred
migrations into the migrations table.

When two statements are run in a transaction, we need to explicitly
insert `statement_timestamp()` into the `created_at` and `updated_at`
fields rather than relying on the table default of `current_timestamp`.

`current_timestamp` is the same for all statements in a transaction,
which causes problems when ordering statements by `created_at`.


Fixes #361
2024-06-25 14:57:18 +01:00
Andrew Farries
84ef318cb1
Infer CREATE/DROP SCHEMA migrations (#359)
Update the `raw_migation` event trigger function to capture `CREATE
SCHEMA` and `DROP SCHEMA` statements.
2024-06-24 07:50:06 +01:00
Andrew Farries
8234b9e5d2
Revert "Add support to list views (#358)" (#360)
The state query changes from #358 are causing performance issues in an
upstream integration of `pgroll`.

Revert the PR until the cause of the regression is found and fixed.

This reverts commit f994a42192.
2024-06-20 13:37:03 +01:00
Alexis Rico
f994a42192
Add support to list views (#358)
Signed-off-by: Alexis Rico <sferadev@gmail.com>
2024-06-19 16:30:17 +02:00
Andrew Farries
a87fa36dda
Support create/drop index with uppercase names (#356)
Fixes https://github.com/xataio/pgroll/issues/355

Postgres stores index names with uppercase characters in the `pg_index`
catalog using the quoted version of the name. For example:

```
"idx_USERS_name"
```

whereas a lowercase index name would be stored as:

```
idx_users_name
```

This is different to how other object types are stored in their
respective catalogs. For example, table names are stored in
the`pg_class` catalog without quotes, regardless of whether they contain
uppercase characters.

This makes it necessary to strip quotes from index names when retrieving
them from the `pg_index` catalog when building the internal schema
representation.
2024-05-16 12:08:58 +01:00
Andrew Farries
a444723691
Restrict state connection search path (#342)
Restrict the search path of the connection used by the state package to
only the state schema (the schema that contains the `migrations` table).

This ensures that any column types that might reside in other schema are
always qualified with the schema name in the internal schema
representation. Without the restriction of the search path, these type
names would be unqualified if they lived in a schema that was on the
search path used by the state connection.

The representation (qualified/unqualified) of type names in the internal
schema is ultimately due to how the
[format-type](https://www.postgresql.org/docs/9.5/functions-info.html)
function behaves; types in the `search_path` of the caller are
unqualified, otherwise they are qualified.
2024-04-22 08:38:11 +01:00
Andrew Farries
c88c0602c6
Track foreign key ON DELETE setting in the internal schema representation (#311)
Add the `ON DELETE` setting of a foreign key to the information stored
about the key in the internal schema representation.

The schema representation for a foreign key now looks like:

```json
{
  "some_table": {
    ...
    "foreignKeys": {
      "fk_users_id": {
        "name": "fk_users_id",
        "columns": [
          "user_id"
        ],
        "onDelete": "NO ACTION",
        "referencedTable": "users",
        "referencedColumns": [
          "id"
        ]
      }
    }
  }
}
```

Fixes https://github.com/xataio/pgroll/issues/309
2024-03-07 08:59:45 +00:00
Andrew Farries
162bd06f9f
Fix duplicate inferred migrations when dropping columns outside of a migration (#305)
Ensure that only one `inferred` migration is created in the
`pgroll.migrations` table when a column is dropped outside of a
migration.

From the Postgres
[docs](https://www.postgresql.org/docs/current/event-trigger-definition.html):
> The sql_drop event occurs just before the ddl_command_end event
trigger for any operation that drops database objects

This means that when the `raw_migration` function is run in response to
`sql_drop` and `ddl_command_end`, duplicate entries will be created in
`pgroll.migrations`; once as the function is run for `sql_drop` and
again when it's run for `ddl_command_end`.

Change the definition of the `pg_roll_handle_drop` event trigger to only
run on those kinds of drops that won't result in duplicates when the
`pg_roll_handle_ddl` trigger runs for the same change. `DROP TABLE` and
`DROP VIEW` won't result in duplicate migrations because their schema
can't be inferred by the `ddl_command_event` trigger because the object
has already been dropped when the trigger runs.

Update the inferred migration tests with two new testcases covering
dropping tables and columns.

Fixes #304
2024-03-06 09:56:52 +00:00
Carlos Pérez-Aradros Herce
937f65c0e9
Relax backfill requirements so it works with unique columns (#288)
UNIQUE NOT NULL columns should also work in order to perform backfills.
This change relaxes the check on backfill requirements to use those
columns if a primary key is not available.

Validation will still error out if no suitable column is found

Note: this also fixes the `unique` retrieval from schema, where we were
failing to understand composed unique indices, resulting in columns
flagged as unique where they weren't really unique.
2024-02-27 15:16:44 +00:00
Andrew Farries
b4e3044adf
Make state initialization concurrency safe (#285)
Make `pgroll` state initialization concurrency safe by using Postgres
advisory locking to ensure at most one connection can initialize at at
time.

See docs on Postgres advisory locking:
*
https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
*
https://www.postgresql.org/docs/current/explicit-locking.html#ADVISORY-LOCKS

Closes https://github.com/xataio/pgroll/issues/283
2024-02-26 09:04:54 +00:00
Andrew Farries
161fde60ca
Fix hardcoded pgroll schema in state initialization (#284)
Fix some hard-coded occurrences of the `pgroll` schema name where the
schema name should instead be parameterized.

This isn't the first time we've made a mistake like this so also add a
test to guard against it.
2024-02-22 16:14:00 +00:00
Andrew Farries
e906da784f
Strip schema from index names in internal schema representation (#278)
Remove the schema component from index names in the internal schema
representation.

A `pgroll` migration is always run in the context of a a specific schema
so the extra qualification is redundant.

Removing the schema component from the index names makes it easy to
identify duplicated indexes by name when temporary columns and
associated attributes are renamed on migration completion.

Tests for this change are already in place (#276, #277). As of this PR,
migration tests run in a non-public schema will pass:

```bash
PGROLL_TEST_SCHEMA=foo go test ./...
```

Part of #273
2024-02-07 12:08:25 +00:00
Alexis Rico
9586b448f3
Update state command to avoid returning nulls (#274) 2024-02-05 16:35:22 +01:00
Carlos Pérez-Aradros Herce
7e65cda1ed
Fix inferred migrations format (#259)
Inferred migrations do not follow the same format as all other stored
migrations. From the schema history point of view, all migrations in the
list should apply correctly, including these.

This change ensures we store them following the same format, so
replaying a migration history is possible.
2024-02-01 16:01:24 +01:00
Andrew Farries
7fd8c7c6da
Remove RAISE NOTICE statements (#256)
Remove informational notices from the `raw_migration` function.
2024-01-31 16:37:22 +00:00
Andrew Farries
9d25159638
Add fields to GROUP BY clauses in read_schema function (#255)
Specify all columns projected by the `SELECT` in the `GROUP BY` clauses
used by the `read_schema` function.
2024-01-29 16:49:08 +00:00
Andrew Farries
908485f7bb
Include key columns for an index in the internal schema representation (#248)
In the internal schema representation, include the key columns on which
an index is defined in the details for an index. With the new `columns`
field, the representation for an index now looks like:

```json
"indexes": {
    "_pgroll_new_products_pkey": {
        "name": "_pgroll_new_products_pkey",
        "unique": true,
        "columns": [
            "id"
        ]
    }
}
```
2024-01-24 16:28:35 +00:00
Andrew Farries
4bee7acf06
Record index uniqueness in pgroll's internal schema representation (#247)
Add uniqueness information to each index recorded in `pgroll`'s internal
schema representation.

With the extra information, the entry for indexes in the schema
representation now looks like:

```json
"indexes": {                                                           
    "products_name_unique": {                                          
        "name": "products_name_unique",                                
        "unique": true,                                                                                                              
    },                                                                  
    "_pgroll_new_products_pkey": {                                     
        "name": "_pgroll_new_products_pkey",                           
        "unique": true,                                                                                                          
    }                                                                  
}
```

Having index uniqueness recorded in the schema representation will help
with preserving uniqueness constraints on columns duplicated for
backfilling (see https://github.com/xataio/pgroll/issues/227).
2024-01-22 15:04:54 +00:00
Andrew Farries
994a1aa6f8
Add UNIQUE constraints to pgroll's internal schema representation (#242)
Add knowledge of `UNIQUE` constraints defined on a table to `pgroll`'s
internal schema representation.

`UNIQUE` constraints were already present as indexes, but this PR adds
them as their own top-level field in the `Table` schema and includes
information about the columns involved in the constraint.

Part of https://github.com/xataio/pgroll/issues/105
2024-01-18 12:08:53 +00:00
Andrew Farries
70a7c8e5db
Add CHECK constraints to pgroll's internal schema representation (#241)
Add knowledge of `CHECK` constraints defined on a table to `pgroll`'s
internal schema representation

Part of https://github.com/xataio/pgroll/issues/105
2024-01-18 12:06:36 +00:00
Andrew Farries
82b7937d77
Remove unused function (#228) 2024-01-15 10:09:28 +00:00
Andrew Farries
f4c37b82c4
test: Change test isolation model to 'database per test' (#220)
Improve the reliability and performance of the test suite by moving the
test isolation model from 'container per test' to 'database per test'.

The current test suite works well locally but is [very flaky
](https://github.com/xataio/pgroll/actions) when run on Github Actions.
The cause of the flakiness is the 'container per test' isolation model,
under which each testcase in each test starts its own Postgres
container. This model worked well initially but as the number of tests
has increased the actions runner often fails to make available the
required number of containers for such a large number of parallel tests.

This PR changes the isolation model to 'database per test'. Each package
creates one Postgres container and then each testcase in each test
creates a database within that container. This greatly reduces the
number of simultaneous containers required, making the test suite faster
and more reliable.

Each job in the test matrix sees a 60-70% reduction in duration and
(anecdotally) far fewer failures with no failures observed in ~20 runs.
2024-01-08 10:00:01 +00:00
Carlos Pérez-Aradros Herce
66ccf91978
Add unique & FK constraints info to the schema (#218)
This info is useful to better validate incoming migrations, also it
reflects better the resulting schema

example output:

```
{
  "name": "public",
  "tables": {
    "table1": {
      "oid": "66508",
      "name": "table1",
      "columns": {
        "id": {
          "name": "id",
          "type": "integer",
          "unique": true,
          "comment": null,
          "default": null,
          "nullable": false
        }
      },
      "comment": null,
      "indexes": {
        "table1_pkey": {
          "name": "table1_pkey"
        }
      },
      "primaryKey": [
        "id"
      ],
      "foreignKeys": null
    },
    "table2": {
      "oid": "66513",
      "name": "table2",
      "columns": {
        "fk": {
          "name": "fk",
          "type": "integer",
          "unique": false,
          "comment": null,
          "default": null,
          "nullable": false
        }
      },
      "comment": null,
      "indexes": null,
      "primaryKey": null,
      "foreignKeys": {
        "fk_fkey": {
          "name": "fk_fkey",
          "columns": [
            "fk"
          ],
          "referencedTable": "table1",
          "referencedColumns": [
            "id"
          ]
        }
      }
    }
  }
}
```
2023-12-20 14:21:50 +01:00
Andrew Farries
763dabcf2c
Move logic for the status command into the state and migrations packages (#205)
Move the logic for the `pgroll status` command out of the CLI and into
the `migrations` and `state` package and add tests for it.

This makes it possible to consume migration status information from
packages using `pgroll` as a module.
2023-11-22 12:36:54 +00:00
Carlos Pérez-Aradros Herce
954746bac0
Fix primary keys retrieval query (#198)
The previous query had some issues when used under clusters with many
schema objects, this one improves query performance
2023-11-03 17:16:04 +01:00
Andrew Farries
c029d5eff2
Fix previous version detection in the presence of inferred DDL migrations (#197)
Update the definition of the `previous_version` function to use a
recursive CTE to find the first non-'inferred' parent of the current
version.

This requires adding a new column to the `pgroll.migrations` table to
record whether a migration is a pgroll migration or an inferred DDL
change made outside of pgroll.

Together, this means that the previous version schema is removed
correctly when there have been inferred DDL changes between the current
and previous pgroll migration.

Fixes #196
2023-11-03 07:08:01 +00:00
Andrew Farries
2429a6be6e
Ping after creating connection (#195)
Ping the database after establishing a connection for better error
messages.

Without this change, an error establishing a connection would be
prefixed with the message from the next failure.

**before**:
```
Error: unable to set pgroll.internal to true: dial tcp [::1]:7432: connect: connection refused
```

**after**:
```
Error: dial tcp [::1]:7432: connect: connection refused 
```

Fixes https://github.com/xataio/pgroll/issues/133
2023-11-02 10:48:07 +00:00
Andrew Farries
536295be67
Add SECURITY DEFINER to internal functions (#191)
We want to work on the security model for the `pgroll` schema, allowing
admins to remove access while maintaining functionality intact. Because
of this, some functions must still be marked as executable for any user
doing migrations directly with postgres.

This PR adds `SECURITY DEFINER` to these functions

---------

Co-authored-by: Carlos Pérez-Aradros Herce <exekias@gmail.com>
2023-10-25 09:59:42 +02:00
Andrew Farries
d486aabe6a
Fix previous_version function to work with non-public schema (#190)
Fix the `previous_version` function so that it works correctly for
migrations applied in schema other than`public`.

In particular, this meant that the previous version schema would not be
removed on migration completion for migrations applied is schema other
than `public`.

Extend the test that checks the `--schema` flag is respected with the
extra coverage that would have caught this.
2023-10-18 09:00:02 +01:00
Andrew Farries
7f4b90ef54
Store primary keys in pgroll internal schema (#135)
Add a `primaryKey` field to each table in the internal schema store to
record the column(s) that make up a table's primary key.

This will be used when backfilling rows
(https://github.com/xataio/pgroll/issues/127)

An example schema now looks like:

```json
{
  "name": "public",
  "tables": {
    "users": {
      "oid": "16412",
      "name": "users",
      "columns": {
        "id": {
          "name": "id",
          "type": "integer",
          "comment": null,
          "default": "nextval('users_id_seq'::regclass)",
          "nullable": false
        },
        "name": {
          "name": "name",
          "type": "text",
          "comment": null,
          "default": null,
          "nullable": true
        }
      },
      "comment": null,
      "indexes": {
        "users_pkey": {
          "name": "users_pkey"
        }
      },
      "primaryKey": [
        "id"
      ]
    }
  }
}
```
Where the `primaryKey` field is the new field.
2023-09-26 07:08:21 +01:00
Carlos Pérez-Aradros Herce
5c96ee642c
Set project name to pgroll (#124) 2023-09-22 12:50:31 +02:00
Philip Krauss
71d14ad513
License check in github actions (#123)
This PR adds a check to ensure a license declaration is present in the
file head of every `.go` file.
2023-09-22 10:31:49 +00:00
Carlos Pérez-Aradros Herce
99e34b1977
Allow to migrate existing databases (#112)
When applying a first migration against a schema, return the schema as
read form postgres, instead of an empty one.

This change allows migrations to happen against schemas created before
pg-roll install.
2023-09-21 22:41:38 +02:00
Carlos Pérez-Aradros Herce
2a6a0e8c33
Also account for DROP statements in raw SQL DDL autodetection (#102)
This change ensures we also catch DROP statements for their inclusion in
the migrations log.

It seems DROP statements don't make it to the `ddl_command_end` trigger.
We need to explictly listen for them under `sql_drop`.
2023-09-19 11:15:38 +02:00
Carlos Pérez-Aradros Herce
16b1d75ee0
Add raw SQL operation (#43)
This change adds a new `sql` operation, that allows to define an `up`
SQL statement to perform a migration on the schema.

An optional `down` field can be provided, this will be used when trying
to do a rollback after (for instance, in case of migration failure).

A new trigger is installed to capture DDL events coming from direct user
manipulations (not done by pg-roll), so they are stored as a migration,
getting to know the resulting schema in all cases.
2023-08-30 11:50:59 +02:00
Andrew Farries
a18dab932f
Store schema name in pg-roll's internal schema representation (#67)
Change the representation of a schema in `pg-roll`s state store from:

```go
type Schema struct {
	// Tables is a map of virtual table name -> table mapping
	Tables map[string]Table `json:"tables"`
}
```

to:

```go
type Schema struct {
	// Name is the name of the schema
	Name string `json:"name"`
	// Tables is a map of virtual table name -> table mapping
	Tables map[string]Table `json:"tables"`
}
```

ie, store the schema's name.

This allows the signature of `Start` to be simplified, removing the
`schemaName` parameter; the name can be retrieved from the
`schema.Schema` struct that is already provided.
2023-08-29 14:58:24 +01:00
Andrew Farries
813f95fe7d
Change module name (#60)
Change the module name to match its import path.

In order for `pg-roll` to be usable as a module we need the be able to
import `"github.com/xataio/pg-roll/pkg/roll"` etc from other modules.
Changing the name of the module to match its import path ensures that
this is possible.
2023-08-22 09:27:58 +01:00
Andrew Farries
0020c3e751
Store indexes in internal schema representation (#57)
Add information about indexes on a table to `pg-roll`'s internal state
storage.

For each table, store an additional JSON object mapping each index name
on the table to details of the index (initially just its name).

An example of the resulting JSON is:

```json
{
  "tables": {
    "fruits": {
      "oid": "16497",
      "name": "fruits",
      "columns": {
        "id": {
          "name": "id",
          "type": "integer",
          "comment": null,
          "default": "nextval('_pgroll_new_fruits_id_seq'::regclass)",
          "nullable": false
        },
        "name": {
          "name": "name",
          "type": "varchar(255)",
          "comment": null,
          "default": null,
          "nullable": false
        }
      },
      "comment": null,
      "indexes": {
        "_pgroll_idx_fruits_name": {
          "name": "_pgroll_idx_fruits_name"
        },
        "_pgroll_new_fruits_pkey": {
          "name": "_pgroll_new_fruits_pkey"
        },
        "_pgroll_new_fruits_name_key": {
          "name": "_pgroll_new_fruits_name_key"
        }
      }
    }
  }
}
```

Also add fields to the `Schema` model structs to allow the new `indexes`
field to be unmarshalled.
2023-08-17 14:26:44 +01:00
Andrew Farries
b4efd8ad50
Support up SQL on add column operations (#34)
Add a new field `Up` to **add column** migrations:

```json
{
  "name": "03_add_column_to_products",
  "operations": [
    {
      "add_column": {
        "table": "products",
        "up": "UPPER(name)",
        "column": {
          "name": "description",
          "type": "varchar(255)",
          "nullable": true
        }
      }
    }
  ]
}
```

The SQL specified by the `up` field will be run whenever an row is
inserted into the underlying table when the session's `search_path` is
not set to the latest version of the schema.

The `up` SQL snippet can refer to existing columns in the table by name
(as in the the above example, where the `description` field is set to
`UPPER(name)`).
2023-07-20 06:37:03 +01:00
Andrew Farries
952b3598d8
Respect --schema flag (#26)
Make `pg-roll` respect the `--schema` flag by ensuring migrations are
run in the schema specified by the flag.
2023-07-11 14:57:54 +01:00
Andrew Farries
6cc22c5a63
Add status subcommand (#25)
Add a `status` command to show the status of each schema that `pg-roll`
knows about (ie ,those schema that have had >0 migrations run in them).

`go run . status`

**Example output**:
```json
[
  {
    "Schema": "public",
    "Version": "01_create_tables",
    "Status": "In Progress"
  }
]
```
or:

```json
[
  {
    "Schema": "public",
    "Version": "01_create_tables",
    "Status": "Complete"
  }
]
```

In future the `json` output of the command should be behind a `-o json`
switch and the default output should be human readable.
2023-07-11 11:07:18 +01:00
Andrew Farries
99dd68815d
Reimplement analyze subcommand (#24)
The hidden `analyze` subcommand to dump the inferred database schema to
stdout was implemented in #1.

We've since changed how the schema is inferred (in #12).

This PR updates the `analyze` command to use the schema retrieval
implemented in #12.

Example

`go run . analyze public`:
```json
{
  "tables": {
    "bills": {
      "oid": "16417",
      "name": "bills",
      "comment": "",
      "columns": {
        "date": {
          "name": "date",
          "type": "time with time zone",
          "default": null,
          "nullable": false,
          "comment": ""
        },
        "id": {
          "name": "id",
          "type": "integer",
          "default": null,
          "nullable": false,
          "comment": ""
        },
        "quantity": {
          "name": "quantity",
          "type": "integer",
          "default": null,
          "nullable": false,
          "comment": ""
        }
      }
    },
    "customers": {
      "oid": "16408",
      "name": "customers",
      "comment": "",
      "columns": {
        "credit_card": {
          "name": "credit_card",
          "type": "text",
          "default": null,
          "nullable": true,
          "comment": ""
        },
        "id": {
          "name": "id",
          "type": "integer",
          "default": null,
          "nullable": false,
          "comment": ""
        },
        "name": {
          "name": "name",
          "type": "varchar(255)",
          "default": null,
          "nullable": false,
          "comment": ""
        }
      }
    }
  }
}
```
2023-07-11 08:13:23 +01:00
Andrew Farries
7bcbee1256
Remove old schema version on pg-roll complete (#13)
Ensure that the previous version of the schema is removed by `pg-roll
complete`.
2023-07-05 13:58:55 +01:00
Carlos Pérez-Aradros Herce
5e61b040e1
Make migrations schema aware (#12)
This change will retrieve and store the resulting schema after a
migration is completed. This schema will be used as the base to execute
the next migration, making it possible to create views that are aware of
the full schema, and not only the one created by the last migration.

We use a function to retrieve the schema directly from Postgres instead
of building it from the migration files. This allows for more features
in the future, like doing an initial sync on top of the existing schema
or automatically detecting and storing out of band migrations from
triggers.

Example JSON stored schema:

```
{
  "tables": {
    "bills": {
      "oid": "18272",
      "name": "bills",
      "columns": {
        "id": {
          "name": "id",
          "type": "integer",
          "comment": null,
          "default": null,
          "nullable": false
        },
        "date": {
          "name": "date",
          "type": "time with time zone",
          "comment": null,
          "default": null,
          "nullable": false
        },
        "quantity": {
          "name": "quantity",
          "type": "integer",
          "comment": null,
          "default": null,
          "nullable": false
        }
      },
      "comment": null
    },
    "products": {
      "oid": "18286",
      "name": "products",
      "columns": {
        "id": {
          "name": "id",
          "type": "integer",
          "comment": null,
          "default": "nextval(_pgroll_new_products_id_seq::regclass)",
          "nullable": false
        },
        "name": {
          "name": "name",
          "type": "varchar(255)",
          "comment": null,
          "default": null,
          "nullable": false
        },
        "price": {
          "name": "price",
          "type": "numeric(10,2)",
          "comment": null,
          "default": null,
          "nullable": false
        }
      },
      "comment": null
    },
    "customers": {
      "oid": "18263",
      "name": "customers",
      "columns": {
        "id": {
          "name": "id",
          "type": "integer",
          "comment": null,
          "default": null,
          "nullable": false
        },
        "name": {
          "name": "name",
          "type": "varchar(255)",
          "comment": null,
          "default": null,
          "nullable": false
        },
        "credit_card": {
          "name": "credit_card",
          "type": "text",
          "comment": null,
          "default": null,
          "nullable": true
        }
      },
      "comment": null
    }
  }
}
```


After this change, I believe that the `create_table` operation is
feature complete and can be used for many sequential migrations.
2023-07-05 13:20:59 +02:00
Andrew Farries
69ab631485
Add a sentinel error for no active migration (#11)
Add a sentinel error `ErrNoActiveMigration` for the case where there is
no active migration. This improves the error strings presented to users
by not mentioning SQL errors.

**`pg-roll start` when there is a migration in progess:**
```
Error: a migration for schema "public" is already in progress 
```

**`pg-roll rollback` when there is no migration in progress:**
```
Error: unable to get active migration: no active migration 
```

**`pg-complete` when there is no active migration:**
```
Error: unable to get active migration: no active migration
```
2023-07-03 14:18:31 +01:00
Carlos Pérez-Aradros Herce
a8c4fddd14
Add migrations state handling (#7)
This migrations introduces state handling by creating a dedicated
`pgroll` schema (name configurable). We will store migrations there, as
well as their state. So we keep some useful information, ie the
migration definition (so we don't need it for the `complete` state).

Schema includes the proper constraints to guarantee that:
* Only a migration is active at a time
* Migration history is linear (all migrations have a unique parent,
except the first one which is NULL)
* We now the current migration at all times

Some helper functions are included:

* `is_active_migration_period()` will return true if there is an active
migration.
* `latest_version()` will return the name of the latest version of the
schema.
2023-06-28 11:10:03 +02:00