Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 17 additions & 16 deletions database/postgres/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,23 @@

`postgres://user:password@host:port/dbname?query` (`postgresql://` works, too)

| URL Query | WithInstance Config | Description |
|------------|---------------------|-------------|
| `x-migrations-table` | `MigrationsTable` | Name of the migrations table |
| `x-statement-timeout` | `StatementTimeout` | Abort any statement that takes more than the specified number of milliseconds |
| `dbname` | `DatabaseName` | The name of the database to connect to |
| `search_path` | | This variable specifies the order in which schemas are searched when an object is referenced by a simple name with no schema specified. |
| `user` | | The user to sign in as |
| `password` | | The user's password |
| `host` | | The host to connect to. Values that start with / are for unix domain sockets. (default is localhost) |
| `port` | | The port to bind to. (default is 5432) |
| `fallback_application_name` | | An application_name to fall back to if one isn't provided. |
| `connect_timeout` | | Maximum wait for connection, in seconds. Zero or not specified means wait indefinitely. |
| `sslcert` | | Cert file location. The file must contain PEM encoded data. |
| `sslkey` | | Key file location. The file must contain PEM encoded data. |
| `sslrootcert` | | The location of the root certificate file. The file must contain PEM encoded data. |
| `sslmode` | | Whether or not to use SSL (disable\|require\|verify-ca\|verify-full) |
| URL Query | WithInstance Config | Description | Default value |
|------------|---------------------|-------------|---------------|
| `x-migrations-table-has-schema` | `MigrationsTableHasSchema` | Enable schema name support in `x-migrations-table` parameter | `false` |
| `x-migrations-table` | `MigrationsTable` | Name of the migrations table, if `x-migrations-table-has-schema` is enabled then the first dot is treated as the schema and table name separator, for instance `gomigrate.schema_migrations` | `schema_migrations` |
| `x-statement-timeout` | `StatementTimeout` | Abort any statement that takes more than the specified number of milliseconds | `0` |
| `dbname` | `DatabaseName` | The name of the database to connect to | `SELECT CURRENT_DATABASE()` result |
| `search_path` | | This variable specifies the order in which schemas are searched when an object is referenced by a simple name with no schema specified. | `SHOW search_path` result |
| `user` | | The user to sign in as | |
| `password` | | The user's password | |
| `host` | | The host to connect to. Values that start with / are for unix domain sockets | `localhost` |
| `port` | | The port to bind to| `5432` |
| `fallback_application_name` | | An application_name to fall back to if one isn't provided. | |
| `connect_timeout` | | Maximum wait for connection, in seconds. Zero or not specified means wait indefinitely. | |
| `sslcert` | | Cert file location. The file must contain PEM encoded data. | |
| `sslkey` | | Key file location. The file must contain PEM encoded data. | |
| `sslrootcert` | | The location of the root certificate file. The file must contain PEM encoded data. | |
| `sslmode` | | Whether or not to use SSL (disable\|require\|verify-ca\|verify-full) | |


## Upgrading from v1
Expand Down
43 changes: 32 additions & 11 deletions database/postgres/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@ var (
)

type Config struct {
MigrationsTable string
DatabaseName string
SchemaName string
StatementTimeout time.Duration
MigrationsTableHasSchema bool
MigrationsTable string
DatabaseName string
SchemaName string
StatementTimeout time.Duration
}

type Postgres struct {
Expand Down Expand Up @@ -124,6 +125,7 @@ func (p *Postgres) Open(url string) (database.Driver, error) {

migrationsTable := purl.Query().Get("x-migrations-table")
statementTimeoutString := purl.Query().Get("x-statement-timeout")
migrationsTableHasSchemaString := purl.Query().Get("x-migrations-table-has-schema")
statementTimeout := 0
if statementTimeoutString != "" {
statementTimeout, err = strconv.Atoi(statementTimeoutString)
Expand All @@ -132,10 +134,19 @@ func (p *Postgres) Open(url string) (database.Driver, error) {
}
}

migrationsTableHasSchema := false
if migrationsTableHasSchemaString != "" {
migrationsTableHasSchema, err = strconv.ParseBool(migrationsTableHasSchemaString)
if err != nil {
return nil, err
}
}

px, err := WithInstance(db, &Config{
DatabaseName: purl.Path,
MigrationsTable: migrationsTable,
StatementTimeout: time.Duration(statementTimeout) * time.Millisecond,
DatabaseName: purl.Path,
MigrationsTableHasSchema: migrationsTableHasSchema,
MigrationsTable: migrationsTable,
StatementTimeout: time.Duration(statementTimeout) * time.Millisecond,
})

if err != nil {
Expand Down Expand Up @@ -266,13 +277,23 @@ func runesLastIndex(input []rune, target rune) int {
return -1
}

func (p *Postgres) quoteIdentifier(name string) string {
if p.config.MigrationsTableHasSchema {
firstDotPosition := strings.Index(name, ".")
if firstDotPosition != -1 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we error if a . is not found?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we error if a . is not found?

@dhui Why? I suggest to use default schema if . not found in table name.

Do you think this is a bad idea? Do you prefer that schema name must be mandatory?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Falling back to search_path makes sense. The behavior isn't obvious based on the config name, so could you update the docs to mention that . is not required?

return pq.QuoteIdentifier(name[0:firstDotPosition]) + "." + pq.QuoteIdentifier(name[firstDotPosition+1:])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We error if more than one . is found or document the behavior. e.g. the first dot is treated as the schema and table name separator.

Another approach I just thought of is to have an option to treat the identifier as already quoted. e.g. you'd pass in "myschema"."mytable" and we'd just check that the string starts and ends with ". Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or document the behavior. e.g. the first dot is treated as the schema and table name separator.

@dhui I chose to document it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another approach I just thought of is to have an option to treat the identifier as already quoted. e.g. you'd pass in "myschema"."mytable" and we'd just check that the string starts and ends with ". Thoughts?

Do you think that this is a better approach ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having an option to treat the identifier as already quoted and requiring that the identifier starts and ends with a " is a simpler approach since we're not parsing the identifer so there are fewer opportunities for unexpected behaviors
What do you think?

If we go with the pre-quoted identifier approach, what do you think of the config name x-migrations-table-quoted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having an option to treat the identifier as already quoted and requiring that the identifier starts and ends with a " is a simpler approach since we're not parsing the identifer so there are fewer opportunities for unexpected behaviors
What do you think?

@dhui This is a new Pull Request with this implementation: #533

}
}
return pq.QuoteIdentifier(name)
}

func (p *Postgres) SetVersion(version int, dirty bool) error {
tx, err := p.conn.BeginTx(context.Background(), &sql.TxOptions{})
if err != nil {
return &database.Error{OrigErr: err, Err: "transaction start failed"}
}

query := `TRUNCATE ` + pq.QuoteIdentifier(p.config.MigrationsTable)
query := `TRUNCATE ` + p.quoteIdentifier(p.config.MigrationsTable)
if _, err := tx.Exec(query); err != nil {
if errRollback := tx.Rollback(); errRollback != nil {
err = multierror.Append(err, errRollback)
Expand All @@ -284,7 +305,7 @@ func (p *Postgres) SetVersion(version int, dirty bool) error {
// empty schema version for failed down migration on the first migration
// See: https://github.com/golang-migrate/migrate/issues/330
if version >= 0 || (version == database.NilVersion && dirty) {
query = `INSERT INTO ` + pq.QuoteIdentifier(p.config.MigrationsTable) +
query = `INSERT INTO ` + p.quoteIdentifier(p.config.MigrationsTable) +
` (version, dirty) VALUES ($1, $2)`
if _, err := tx.Exec(query, version, dirty); err != nil {
if errRollback := tx.Rollback(); errRollback != nil {
Expand All @@ -302,7 +323,7 @@ func (p *Postgres) SetVersion(version int, dirty bool) error {
}

func (p *Postgres) Version() (version int, dirty bool, err error) {
query := `SELECT version, dirty FROM ` + pq.QuoteIdentifier(p.config.MigrationsTable) + ` LIMIT 1`
query := `SELECT version, dirty FROM ` + p.quoteIdentifier(p.config.MigrationsTable) + ` LIMIT 1`
err = p.conn.QueryRowContext(context.Background(), query).Scan(&version, &dirty)
switch {
case err == sql.ErrNoRows:
Expand Down Expand Up @@ -380,7 +401,7 @@ func (p *Postgres) ensureVersionTable() (err error) {
}
}()

query := `CREATE TABLE IF NOT EXISTS ` + pq.QuoteIdentifier(p.config.MigrationsTable) + ` (version bigint not null primary key, dirty boolean not null)`
query := `CREATE TABLE IF NOT EXISTS ` + p.quoteIdentifier(p.config.MigrationsTable) + ` (version bigint not null primary key, dirty boolean not null)`
if _, err = p.conn.ExecContext(context.Background(), query); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
Expand Down
41 changes: 41 additions & 0 deletions database/postgres/postgres_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -508,5 +508,46 @@ func Test_computeLineFromPos(t *testing.T) {
run(true, true)
})
}
}

func Test_quoteIdentifier(t *testing.T) {
testcases := []struct {
migrationsTableHasSchema bool
name string
want string
}{
{
true,
"schema_name.table_name",
"\"schema_name\".\"table_name\"",
},
{
true,
"schema_name.table.name",
"\"schema_name\".\"table.name\"",
},
{
false,
"table_name",
"\"table_name\"",
},
{
false,
"table.name",
"\"table.name\"",
},
}
p := &Postgres{
config: &Config{
MigrationsTableHasSchema: true,
},
}

for _, tc := range testcases {
p.config.MigrationsTableHasSchema = tc.migrationsTableHasSchema
got := p.quoteIdentifier(tc.name)
if tc.want != got {
t.Fatalf("expected %s but got %s", tc.want, got)
}
}
}