-
Notifications
You must be signed in to change notification settings - Fork 257
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Postgres: support a connection string DSN #435
Conversation
use DATABASE_DSN and DBMATE_DRIVER The format of DATABASE_DSN is defined under connection string here: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING The DATABASE_URL is problematic because it is parsed to a URL in Go. This requires encoding the password for the URL which does not necessarily get decoded properly. It is simpler to not alter the parameters to fit them into a url: that is what DATABASE_DSN does. Currently it will fail if a configuration parameter has a space. However, spaces are normally not used for any of the parameters. And that limitation can be fixed by taking on a DSN parser depdendency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm honestly not a fan of introducing dbutil.DSN
at all.
First, the acronym DSN
in this context is short for "Data Source Name" which is just the name of the data source being defined. What is actually being stored in this structure is an ODBC connection string. A connection string can contain the key-value "DSN" which names the data source defined by the connection string.
Second, I dislike the internals of dbmate needing to know the difference between a database URL and this new DSN structure. It feels like this change is moving against maintaining separation of concerns and the Law of Demeter here.
My preference would be to only add one new function that can parse the ODBC-style connection string and return a JDBC-style database URL from it, and the rest of dbmate can remain unchanged, as it doesn't need to know anything about connection strings or how they're parsed. The rest of the dbmate implementation only needs to know how to handle database URLs.
Is there something that can currently only be specified in the database connection string format that cannot be encoded as a database URL? Could you provide an example?
@@ -196,6 +196,8 @@ $ dbmate -u "postgres://postgres@127.0.0.1:5432/myapp_test?sslmode=disable" up | |||
|
|||
The only advantage of using `dbmate -e TEST_DATABASE_URL` over `dbmate -u $TEST_DATABASE_URL` is that the former takes advantage of dbmate's automatic `.env` file loading. | |||
|
|||
Some drivers (currently just Postgres) support specifying a DSN string instead of a url by using `DATABASE_DSN` instead of `DATABBASE_URL`. When using `DATABASE_DSN` you can specify the driver with `DBMATE_DRIVER`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fix: DATABBASE_URL
-> DATABASE_URL
if dsn := os.Getenv("DATABASE_DSN"); dsn != "" { | ||
driver := os.Getenv("DBMATE_DRIVER") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a fan of hard-coding the environment variable names here.
I would prefer new CLI args be introduced, with this as a suggested implementation:
--dsn value specify the database DSN
--env-dsn value specify an environment variable containing the database DSN (default: "DATABASE_DSN")
--driver value specify the database driver when DSN is used
--env-driver value specify an environment variable containing the database driver (default: "DBMATE_DRIVER")
if db.DatabaseURL == nil || db.DatabaseURL.Scheme == "" { | ||
return nil, ErrInvalidURL | ||
if db.DatabaseDSN != nil { | ||
scheme = strings.ToLower(db.DatabaseDSN.Driver()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I dislike converting the case at usage time, and would rather that be done in one single place, in dbutil.NewDSN()
.
postgresURL.Path = "postgres" | ||
return sql.Open("postgres", postgresURL.String()) | ||
} else if drv.databaseDSN != nil { | ||
_ = drv.databaseDSN.SetKey("dbname=", dsnPostgresDB) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if the user wants to specify an alternative database to use in their session? Is this always overwriting dbname
in the DSN with the string stored in dsnPostgresDB
?
// no schema specified with table name, try search path if available | ||
schema = drv.takeParam("search_path") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems unrelated to implementing DSN support, and should probably be in a separate PR.
func (drv *Driver) takeParam(param string) string { | ||
if drv.databaseURL != nil { | ||
searchPath := strings.Split(drv.databaseURL.Query().Get(param), ",") | ||
u := dbutil.MustParseURL(connectionString(drv.databaseURL)) | ||
query := u.Query() | ||
query.Del(param) | ||
u.RawQuery = query.Encode() | ||
drv.databaseURL = u | ||
return strings.TrimSpace(searchPath[0]) | ||
} | ||
|
||
return drv.databaseDSN.DeleteKey("search_path") | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems unrelated to implementing DSN support, and should probably be in a separate PR.
const ( | ||
envUser = "PGUSER" | ||
envPassword = "PGPASSWORD" | ||
envDatabase = "PGDATABASE" | ||
envHost = "PGHOST" | ||
envPort = "PGPORT" | ||
envSSLMode = "PGSSL" | ||
) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm having trouble finding where in the code these constants are being used. If they're unused, then add them?
@dossy thanks for attending to pull requests. I am still working on this pull request. At a high level it seems that the point of this pull request is still not understood.
I had an auto-generated (strong) db password that did not work due to the URL encoding issues (yes I did try encoding it). Whereas the key=value connection string just works. So parsing it into a URL would defeat the purpose. Another approach could be to not parse the url but just leave it as a string (Postgres doesn't actually want it to be url encoded), but this would be a big change that would require changing every driver. And it is still worse than key=value because '@' still needs to get escaped in a password. Whereas I am quite happy to not put any spaces in my configuration. |
I don't think I understand the problem this PR is solving. You need to encode special characters in the password using URL encoding, that should be very simple in any programming language. Or you could instead generate a random strong password using URL-safe characters. Either option is fine. Any connection string format will need to escape special characters (e.g. if your password contains a space such as If you are having problems with URL encoding please post details and we can help you. |
If this pull request isn't ready for review, I suggest you mark it as such by converting it to a draft pull request.
I would appreciate seeing an example password that cannot be used by dbmate when URL-encoded correctly. As an experiment, I gave this a try:
dbmate has no problem connecting to PostgreSQL with a password containing any of the printable ASCII characters in it, if the password is properly encoded. One Go function that parses a ODBC-style connection string and produces a properly encoded database URL is all that's needed in order for dbmate to support ODBC-style connection strings. |
I was able to get the password encoding working (in the encoding process I accidentally introduced a newline). It's still a hassle though and key=value would simplify things since it's very rare to want a space, but very common to want a url encoded character in a password. I will close this PR though since the maintainers don't like it. There are still a lot of other obviously good PRs that need to get merged- better to spend energy there. |
I do think adding The difficulty here is that, unfortunately, PostgreSQL connection strings do not follow the ODBC specification for connection strings. 😞 This means having a single connection string parsing implementation won't be possible, and it'll need to be per-driver, and we'll need something like I captured these details in a new issue so when someone does go to implement connection string support, these details aren't overlooked. |
use DATABASE_DSN and DBMATE_DRIVER
The format of DATABASE_DSN is defined under connection string here: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING
The DATABASE_URL is problematic because it is parsed to a URL in Go. This requires encoding the password for the URL which does not necessarily get decoded properly.
It is simpler to not alter the parameters to fit them into a url: that is what DATABASE_DSN does. Currently it will fail if a configuration parameter has a space. However, spaces are normally not used for any of the parameters. And that limitation can be fixed by taking on a DSN parser dependency.
Testing
Unit tests for DSN manipulation.
Manual tests were done to see that DATABASE_DSN works with
dbmate status
. I am using it for other commands now as well.