pax_global_header00006660000000000000000000000064150002763520014512gustar00rootroot0000000000000052 comment=04425ad45ff4c5036902c77001b7c9327e72c26e dburl-0.23.7/000077500000000000000000000000001500027635200127135ustar00rootroot00000000000000dburl-0.23.7/.github/000077500000000000000000000000001500027635200142535ustar00rootroot00000000000000dburl-0.23.7/.github/workflows/000077500000000000000000000000001500027635200163105ustar00rootroot00000000000000dburl-0.23.7/.github/workflows/test.yml000066400000000000000000000004721500027635200200150ustar00rootroot00000000000000on: [push, pull_request] name: Test jobs: test: name: Test runs-on: ubuntu-latest steps: - name: Install Go uses: actions/setup-go@v5 with: go-version: stable - name: Checkout code uses: actions/checkout@v4 - name: Test run: CGO_ENABLED=0 go test -v ./... dburl-0.23.7/.gitignore000066400000000000000000000000351500027635200147010ustar00rootroot00000000000000example/example coverage.out dburl-0.23.7/LICENSE000066400000000000000000000020741500027635200137230ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015-2025 Kenneth Shaw Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. dburl-0.23.7/README.md000066400000000000000000000471561500027635200142070ustar00rootroot00000000000000# About dburl Package `dburl` provides a standard, URL style mechanism for parsing and opening SQL database connection strings for [Go][go-project]. Provides standardized way to [parse][goref-parse] and [open][goref-open] URLs for popular databases PostgreSQL, MySQL, SQLite3, Oracle Database, Microsoft SQL Server, in addition to most other SQL databases with a publicly available Go driver. [Overview][] | [Quickstart][] | [Examples][] | [Schemes][] | [Installing][] | [Using][] | [About][] [Overview]: #database-connection-url-overview "Database Connection URL Overview" [Quickstart]: #quickstart "Quickstart" [Examples]: #example-urls "Example URLs" [Schemes]: #database-schemes-aliases-and-drivers "Database Schemes, Aliases, and Drivers" [Installing]: #installing "Installing" [Using]: #using "Using" [About]: #about "About" [![Unit Tests][dburl-ci-status]][dburl-ci] [![Go Reference][goref-dburl-status]][goref-dburl] [![Discord Discussion][discord-status]][discord] [dburl-ci]: https://github.com/xo/dburl/actions/workflows/test.yml [dburl-ci-status]: https://github.com/xo/dburl/actions/workflows/test.yml/badge.svg [goref-dburl]: https://pkg.go.dev/github.com/xo/dburl [goref-dburl-status]: https://pkg.go.dev/badge/github.com/xo/dburl.svg [discord]: https://discord.gg/yJKEzc7prt "Discord Discussion" [discord-status]: https://img.shields.io/discord/829150509658013727.svg?label=Discord&logo=Discord&colorB=7289da&style=flat-square "Discord Discussion" ## Database Connection URL Overview Supported database connection URLs are of the form: ```text protocol+transport://user:pass@host/dbname?opt1=a&opt2=b protocol:/path/to/file ``` Where: | Component | Description | | ------------------- | ------------------------------------------------------------------------------------ | | protocol | driver name or alias (see below) | | transport | "tcp", "udp", "unix" or driver name (odbc/oleodbc) | | user | username | | pass | password | | host | host | | dbname\* | database, instance, or service name/ID to connect to | | ?opt1=... | additional database driver options (see respective SQL driver for available options) | \* for Microsoft SQL Server, `/dbname` can be `/instance/dbname`, where `/instance` is optional. For Oracle Database, `/dbname` is of the form `/service/dbname` where `/service` is the service name or SID, and `/dbname` is optional. Please see below for examples. ## Quickstart Database connection URLs in the above format can be parsed with the [`dburl.Parse` func][goref-parse] as such: ```go import ( "github.com/xo/dburl" ) u, err := dburl.Parse("postgresql://user:pass@localhost/mydatabase/?sslmode=disable") if err != nil { /* ... */ } ``` Additionally, a simple helper, [`dburl.Open`][goref-open], is provided that will parse, open, and return a [standard `sql.DB` database][goref-sql-db] connection: ```go import ( "github.com/xo/dburl" ) db, err := dburl.Open("sqlite:mydatabase.sqlite3?loc=auto") if err != nil { /* ... */ } ``` ## Example URLs The following are example database connection URLs that can be handled by [`dburl.Parse`][goref-parse] and [`dburl.Open`][goref-open]: ```text postgres://user:pass@localhost/dbname pg://user:pass@localhost/dbname?sslmode=disable mysql://user:pass@localhost/dbname mysql:/var/run/mysqld/mysqld.sock sqlserver://user:pass@remote-host.com/dbname mssql://user:pass@remote-host.com/instance/dbname ms://user:pass@remote-host.com:port/instance/dbname?keepAlive=10 oracle://user:pass@somehost.com/sid sap://user:pass@localhost/dbname sqlite:/path/to/file.db file:myfile.sqlite3?loc=auto odbc+postgres://user:pass@localhost:port/dbname?option1= ``` ## Database Schemes, Aliases, and Drivers The following table lists the supported `dburl` protocol schemes (ie, driver), additional aliases, and the related Go driver: | Database | Scheme / Tag | Scheme Aliases | Driver Package / Notes | | -------------------- | --------------- | ----------------------------------------------- | --------------------------------------------------------------------------- | | PostgreSQL | `postgres` | `pg`, `pgsql`, `postgresql` | [github.com/lib/pq][d-postgres] | | MySQL | `mysql` | `my`, `maria`, `aurora`, `mariadb`, `percona` | [github.com/go-sql-driver/mysql][d-mysql] | | Microsoft SQL Server | `sqlserver` | `ms`, `mssql`, `azuresql` | [github.com/microsoft/go-mssqldb][d-sqlserver] | | Oracle Database | `oracle` | `or`, `ora`, `oci`, `oci8`, `odpi`, `odpi-c` | [github.com/sijms/go-ora/v2][d-oracle] | | SQLite3 | `sqlite3` | `sq`, `sqlite`, `file` | [github.com/mattn/go-sqlite3][d-sqlite3] [†][f-cgo] | | ClickHouse | `clickhouse` | `ch` | [github.com/ClickHouse/clickhouse-go/v2][d-clickhouse] | | CSVQ | `csvq` | `cs`, `csv`, `tsv`, `json` | [github.com/mithrandie/csvq-driver][d-csvq] | | | | | | | Alibaba MaxCompute | `maxcompute` | `mc` | [sqlflow.org/gomaxcompute][d-maxcompute] | | Alibaba Tablestore | `ots` | `ot`, `tablestore` | [github.com/aliyun/aliyun-tablestore-go-sql-driver][d-ots] | | Apache Avatica | `avatica` | `av`, `phoenix` | [github.com/apache/calcite-avatica-go/v5][d-avatica] | | Apache H2 | `h2` | | [github.com/jmrobles/h2go][d-h2] | | Apache Hive | `hive` | `hi`, `hive2` | [sqlflow.org/gohive][d-hive] | | Apache Ignite | `ignite` | `ig`, `gridgain` | [github.com/amsokol/ignite-go-client/sql][d-ignite] | | Apache Impala | `impala` | `im` | [github.com/sclgo/impala-go][d-impala] | | AWS Athena | `athena` | `s3`, `aws`, `awsathena` | [github.com/uber/athenadriver/go][d-athena] | | Azure CosmosDB | `cosmos` | `cm`, `gocosmos` | [github.com/btnguyen2k/gocosmos][d-cosmos] | | Cassandra | `cassandra` | `ca`, `scy`, `scylla`, `datastax`, `cql` | [github.com/MichaelS11/go-cql-driver][d-cassandra] | | ChaiSQL | `chai` | `ci`, `genji`, `chaisql` | [github.com/chaisql/chai/driver][d-chai] | | Couchbase | `couchbase` | `n1`, `n1ql` | [github.com/couchbase/go_n1ql][d-couchbase] | | Cznic QL | `ql` | `cznic`, `cznicql` | [modernc.org/ql][d-ql] | | Databend | `databend` | `dd`, `bend` | [github.com/datafuselabs/databend-go][d-databend] | | Databricks | `databricks` | `br`, `brick`, `bricks`, `databrick` | [github.com/databricks/databricks-sql-go][d-databricks] | | DuckDB | `duckdb` | `dk`, `ddb`, `duck`, `file` | [github.com/marcboeker/go-duckdb/v2][d-duckdb] [†][f-cgo] | | DynamoDb | `dynamodb` | `dy`, `dyn`, `dynamo`, `dynamodb` | [github.com/btnguyen2k/godynamo][d-dynamodb] | | Exasol | `exasol` | `ex`, `exa` | [github.com/exasol/exasol-driver-go][d-exasol] | | Firebird | `firebird` | `fb`, `firebirdsql` | [github.com/nakagami/firebirdsql][d-firebird] | | FlightSQL | `flightsql` | `fl`, `flight` | [github.com/apache/arrow/go/v17/arrow/flight/flightsql/driver][d-flightsql] | | Google BigQuery | `bigquery` | `bq` | [gorm.io/driver/bigquery/driver][d-bigquery] | | Google Spanner | `spanner` | `sp` | [github.com/googleapis/go-sql-spanner][d-spanner] | | Microsoft ADODB | `adodb` | `ad`, `ado` | [github.com/mattn/go-adodb][d-adodb] | | ModernC SQLite3 | `moderncsqlite` | `mq`, `modernsqlite` | [modernc.org/sqlite][d-moderncsqlite] | | MySQL MyMySQL | `mymysql` | `zm`, `mymy` | [github.com/ziutek/mymysql/godrv][d-mymysql] | | Netezza | `netezza` | `nz`, `nzgo` | [github.com/IBM/nzgo/v12][d-netezza] | | PostgreSQL PGX | `pgx` | `px` | [github.com/jackc/pgx/v5/stdlib][d-pgx] | | Presto | `presto` | `pr`, `prs`, `prestos`, `prestodb`, `prestodbs` | [github.com/prestodb/presto-go-client/presto][d-presto] | | RamSQL | `ramsql` | `rm`, `ram` | [github.com/proullon/ramsql/driver][d-ramsql] | | SAP ASE | `sapase` | `ax`, `ase`, `tds` | [github.com/thda/tds][d-sapase] | | SAP HANA | `saphana` | `sa`, `sap`, `hana`, `hdb` | [github.com/SAP/go-hdb/driver][d-saphana] | | Snowflake | `snowflake` | `sf` | [github.com/snowflakedb/gosnowflake][d-snowflake] | | Trino | `trino` | `tr`, `trs`, `trinos` | [github.com/trinodb/trino-go-client/trino][d-trino] | | Vertica | `vertica` | `ve` | [github.com/vertica/vertica-sql-go][d-vertica] | | VoltDB | `voltdb` | `vo`, `vdb`, `volt` | [github.com/VoltDB/voltdb-client-go/voltdbclient][d-voltdb] | | YDB | `ydb` | `yd`, `yds`, `ydbs` | [github.com/ydb-platform/ydb-go-sdk/v3][d-ydb] | | | | | | | GO DRiver for ORacle | `godror` | `gr` | [github.com/godror/godror][d-godror] [†][f-cgo] | | ODBC | `odbc` | `od` | [github.com/alexbrainman/odbc][d-odbc] [†][f-cgo] | | | | | | | Amazon Redshift | `postgres` | `rs`, `redshift` | [github.com/lib/pq][d-postgres] [‡][f-wire] | | CockroachDB | `postgres` | `cr`, `cdb`, `crdb`, `cockroach`, `cockroachdb` | [github.com/lib/pq][d-postgres] [‡][f-wire] | | OLE ODBC | `adodb` | `oo`, `ole`, `oleodbc` | [github.com/mattn/go-adodb][d-adodb] [‡][f-wire] | | SingleStore MemSQL | `mysql` | `me`, `memsql` | [github.com/go-sql-driver/mysql][d-mysql] [‡][f-wire] | | TiDB | `mysql` | `ti`, `tidb` | [github.com/go-sql-driver/mysql][d-mysql] [‡][f-wire] | | Vitess Database | `mysql` | `vt`, `vitess` | [github.com/go-sql-driver/mysql][d-mysql] [‡][f-wire] | | | | | | | | | | | [d-adodb]: https://github.com/mattn/go-adodb [d-athena]: https://github.com/uber/athenadriver [d-avatica]: https://github.com/apache/calcite-avatica-go [d-bigquery]: https://github.com/go-gorm/bigquery [d-cassandra]: https://github.com/MichaelS11/go-cql-driver [d-chai]: https://github.com/chaisql/chai [d-clickhouse]: https://github.com/ClickHouse/clickhouse-go [d-cosmos]: https://github.com/btnguyen2k/gocosmos [d-couchbase]: https://github.com/couchbase/go_n1ql [d-csvq]: https://github.com/mithrandie/csvq-driver [d-databend]: https://github.com/datafuselabs/databend-go [d-databricks]: https://github.com/databricks/databricks-sql-go [d-duckdb]: https://github.com/marcboeker/go-duckdb [d-dynamodb]: https://github.com/btnguyen2k/godynamo [d-exasol]: https://github.com/exasol/exasol-driver-go [d-firebird]: https://github.com/nakagami/firebirdsql [d-flightsql]: https://github.com/apache/arrow/tree/main/go/arrow/flight/flightsql/driver [d-godror]: https://github.com/godror/godror [d-h2]: https://github.com/jmrobles/h2go [d-hive]: https://github.com/sql-machine-learning/gohive [d-ignite]: https://github.com/amsokol/ignite-go-client [d-impala]: https://github.com/sclgo/impala-go [d-maxcompute]: https://github.com/sql-machine-learning/gomaxcompute [d-moderncsqlite]: https://gitlab.com/cznic/sqlite [d-mymysql]: https://github.com/ziutek/mymysql [d-mysql]: https://github.com/go-sql-driver/mysql [d-netezza]: https://github.com/IBM/nzgo [d-odbc]: https://github.com/alexbrainman/odbc [d-oracle]: https://github.com/sijms/go-ora [d-ots]: https://github.com/aliyun/aliyun-tablestore-go-sql-driver [d-pgx]: https://github.com/jackc/pgx [d-postgres]: https://github.com/lib/pq [d-presto]: https://github.com/prestodb/presto-go-client [d-ql]: https://gitlab.com/cznic/ql [d-ramsql]: https://github.com/proullon/ramsql [d-sapase]: https://github.com/thda/tds [d-saphana]: https://github.com/SAP/go-hdb [d-snowflake]: https://github.com/snowflakedb/gosnowflake [d-spanner]: https://github.com/googleapis/go-sql-spanner [d-sqlite3]: https://github.com/mattn/go-sqlite3 [d-sqlserver]: https://github.com/microsoft/go-mssqldb [d-trino]: https://github.com/trinodb/trino-go-client [d-vertica]: https://github.com/vertica/vertica-sql-go [d-voltdb]: https://github.com/VoltDB/voltdb-client-go [d-ydb]: https://github.com/ydb-platform/ydb-go-sdk [f-cgo]: #f-cgo "Requires CGO" [f-wire]: #f-wire "Wire compatible"

† Requires CGO
‡ Wire compatible (see respective driver)

Any protocol scheme `alias://` can be used in place of `protocol://`, and will work identically with [`dburl.Parse`][goref-parse] and [`dburl.Open`][goref-open]. ## Installing Install in the usual Go fashion: ```sh $ go get github.com/xo/dburl@latest ``` ## Using `dburl` does not import any of Go's SQL drivers, as it only provides a way to [parse][goref-parse] and [open][goref-open] database URL stylized connection strings. As such, it is necessary to explicitly `import` the relevant SQL driver: ```go import ( // import Microsoft SQL Server driver _ "github.com/microsoft/go-mssqldb" ) ``` See the [database schemes table][Schemes] above for a list of the expected Go driver `import`'s. Additional examples and API details can be found in [the `dburl` package documentation][goref-dburl]. ### URL Parsing Rules [`dburl.Parse`][goref-parse] and [`dburl.Open`][goref-open] rely primarily on Go's standard [`net/url.URL`][goref-net-url] type, and as such, parsing or opening database connection URLs with `dburl` are subject to the same rules, conventions, and semantics as [Go's `net/url.Parse` func][goref-net-url-parse]. ## Example A [full example](_example/example.go) for reference: ```go // _example/example.go package main import ( "fmt" "log" _ "github.com/microsoft/go-mssqldb" "github.com/xo/dburl" ) func main() { db, err := dburl.Open("sqlserver://user:pass@localhost/dbname") if err != nil { log.Fatal(err) } var name string if err := db.QueryRow(`SELECT name FROM mytable WHERE id=10`).Scan(&name); err != nil { log.Fatal(err) } fmt.Println("name:", name) } ``` ## Scheme Resolution By default on non-Windows systems, `dburl` will resolve paths on disk, and URLs with `file:` schemes to an appropriate database driver: 1. Directories will resolve as `postgres:` URLs 2. Unix sockets will resolve as `mysql:` URLs 3. Regular files will have their headers checked to determine if they are either `sqlite3:` or `duckdb:` files 4. Non-existent files will test their file extension against well-known `sqlite3:` and `duckdb:` file extensions and open with the appropriate scheme If this behavior is undesired, it can be disabled by providing different implementations for [`dburl.Stat`][goref-variables] and [`dburl.OpenFile`][goref-variables], or alternately by setting [`dburl.ResolveSchemeType`][goref-variables] to false: ```go import "github.com/xo/dburl" func init() { dburl.ResolveSchemeType = false } ``` ## About `dburl` was built primarily to support these projects: - [usql][usql] - a universal command-line interface for SQL databases - [xo][xo] - a command-line tool to generate code for SQL databases [go-project]: https://go.dev/project [goref-open]: https://pkg.go.dev/github.com/xo/dburl#Open [goref-variables]: https://pkg.go.dev/github.com/xo/dburl#pkg-variables [goref-parse]: https://pkg.go.dev/github.com/xo/dburl#Parse [goref-sql-db]: https://pkg.go.dev/database/sql#DB [goref-net-url]: https://pkg.go.dev/net/url#URL [goref-net-url-parse]: https://pkg.go.dev/net/url#URL.Parse [usql]: https://github.com/xo/usql [xo]: https://github.com/xo/xo dburl-0.23.7/_example/000077500000000000000000000000001500027635200145055ustar00rootroot00000000000000dburl-0.23.7/_example/example.go000066400000000000000000000006141500027635200164700ustar00rootroot00000000000000// _example/example.go package main import ( "fmt" "log" _ "github.com/microsoft/go-mssqldb" "github.com/xo/dburl" ) func main() { db, err := dburl.Open("sqlserver://user:pass@localhost/dbname") if err != nil { log.Fatal(err) } var name string if err := db.QueryRow(`SELECT name FROM mytable WHERE id=10`).Scan(&name); err != nil { log.Fatal(err) } fmt.Println("name:", name) } dburl-0.23.7/dburl.go000066400000000000000000000361251500027635200143610ustar00rootroot00000000000000// Package dburl provides a standard, [net/url.URL] style mechanism for parsing // and opening SQL database connection strings for Go. Provides standardized // way to parse and open [URL]'s for popular databases PostgreSQL, MySQL, SQLite3, // Oracle Database, Microsoft SQL Server, in addition to most other SQL // databases with a publicly available Go driver. // // See the [package documentation README section] for more details. // // [package documentation README section]: https://pkg.go.dev/github.com/xo/dburl#section-readme package dburl import ( "database/sql" "fmt" "io/fs" "net/url" "os" "path" "path/filepath" "runtime" "strings" ) // ResolveSchemeType is a configuration setting to open paths on disk using // [SchemeType], [Stat], and [OpenFile]. Set this to false in an `init()` func // in order to disable this behavior. var ResolveSchemeType = true // Open takes a URL string, also known as a DSN, in the form of // "protocol+transport://user:pass@host/dbname?option1=a&option2=b" and opens a // standard [sql.DB] connection. // // See [Parse] for information on formatting URL strings to work properly with Open. func Open(urlstr string) (*sql.DB, error) { u, err := Parse(urlstr) if err != nil { return nil, err } driver := u.Driver if u.GoDriver != "" { driver = u.GoDriver } return sql.Open(driver, u.DSN) } // OpenMap takes a map of URL components and opens a standard [sql.DB] connection. // // See [BuildURL] for information on the recognized map components. func OpenMap(components map[string]any) (*sql.DB, error) { urlstr, err := BuildURL(components) if err != nil { return nil, err } return Open(urlstr) } // URL wraps the standard [net/url.URL] type, adding OriginalScheme, Transport, // Driver, Unaliased, and DSN strings. type URL struct { // URL is the base [net/url.URL]. url.URL // OriginalScheme is the original parsed scheme (ie, "sq", "mysql+unix", "sap", etc). OriginalScheme string // Transport is the specified transport protocol (ie, "tcp", "udp", // "unix", ...), if provided. Transport string // Driver is the non-aliased SQL driver name that should be used in a call // to [sql.Open]. Driver string // GoDriver is the Go SQL driver name to use when opening a connection to // the database. Used by Microsoft SQL Server's azuresql:// URLs, as the // wire-compatible alias style uses a different syntax style. GoDriver string // UnaliasedDriver is the unaliased driver name. UnaliasedDriver string // DSN is the built connection "data source name" that can be used in a // call to [sql.Open]. DSN string // hostPortDB will be set by Gen*() funcs after determining the host, port, // database. // // When empty, indicates that these values are not special, and can be // retrieved as the host, port, and path[1:] as usual. hostPortDB []string } // Parse parses a URL string, similar to the standard [net/url.Parse]. // // Handles parsing OriginalScheme, Transport, Driver, Unaliased, and DSN // fields. // // Note: if the URL has a Opaque component (ie, URLs not specified as // "scheme://" but "scheme:"), and the database scheme does not support opaque // components, Parse will attempt to re-process the URL as "scheme://". func Parse(urlstr string) (*URL, error) { // parse url v, err := url.Parse(urlstr) switch { case err != nil: return nil, err case v.Scheme == "": if ResolveSchemeType { if typ, err := SchemeType(urlstr); err == nil { return Parse(typ + ":" + urlstr) } } return nil, ErrInvalidDatabaseScheme } // create url u := &URL{ URL: *v, OriginalScheme: urlstr[:len(v.Scheme)], Transport: "tcp", } // check for +transport in scheme var checkTransport bool if i := strings.IndexRune(u.Scheme, '+'); i != -1 { u.Transport = urlstr[i+1 : len(v.Scheme)] u.Scheme = u.Scheme[:i] checkTransport = true } // get dsn generator scheme, ok := schemeMap[u.Scheme] switch { case !ok: return nil, ErrUnknownDatabaseScheme case scheme.Driver == "file": // determine scheme for file s := u.opaqueOrPath() switch { case u.Transport != "tcp", strings.Index(u.OriginalScheme, "+") != -1: return nil, ErrInvalidTransportProtocol case s == "": return nil, ErrMissingPath case ResolveSchemeType: if typ, err := SchemeType(s); err == nil { return Parse(typ + "://" + u.buildOpaque()) } } return nil, ErrUnknownFileExtension case !scheme.Opaque && u.Opaque != "": // if scheme does not understand opaque URLs, retry parsing after // building fully qualified URL return Parse(u.OriginalScheme + "://" + u.buildOpaque()) case scheme.Opaque && u.Opaque == "": // force Opaque u.Opaque, u.Host, u.Path, u.RawPath = u.Host+u.Path, "", "", "" case u.Host == ".", u.Host == "" && strings.TrimPrefix(u.Path, "/") != "": // force unix proto u.Transport = "unix" } // check transport if checkTransport || u.Transport != "tcp" { switch { case scheme.Transport == TransportNone: return nil, ErrInvalidTransportProtocol case scheme.Transport&TransportAny != 0 && u.Transport != "", scheme.Transport&TransportTCP != 0 && u.Transport == "tcp", scheme.Transport&TransportUDP != 0 && u.Transport == "udp", scheme.Transport&TransportUnix != 0 && u.Transport == "unix": default: return nil, ErrInvalidTransportProtocol } } // set driver u.Driver, u.UnaliasedDriver = scheme.Driver, scheme.Driver if scheme.Override != "" { u.Driver = scheme.Override } // generate dsn if u.DSN, u.GoDriver, err = scheme.Generator(u); err != nil { return nil, err } return u, nil } // FromMap creates a [URL] using the mapped components. // // Recognized components are: // // protocol, proto, scheme // transport // username, user // password, pass // hostname, host // port // path, file, opaque // database, dbname, db // instance // parameters, params, options, opts, query, q // // See [BuildURL] for more information. func FromMap(components map[string]any) (*URL, error) { urlstr, err := BuildURL(components) if err != nil { return nil, err } return Parse(urlstr) } // String satisfies the [fmt.Stringer] interface. func (u *URL) String() string { p := &url.URL{ Scheme: u.OriginalScheme, Opaque: u.Opaque, User: u.User, Host: u.Host, Path: u.Path, RawPath: u.RawPath, RawQuery: u.RawQuery, Fragment: u.Fragment, } return p.String() } // Short provides a short description of the user, host, and database. func (u *URL) Short() string { if u.Scheme == "" { return "" } s := schemeMap[u.Scheme].Aliases[0] if u.Scheme == "odbc" || u.Scheme == "oleodbc" { n := u.Transport if v, ok := schemeMap[n]; ok { n = v.Aliases[0] } s += "+" + n } else if u.Transport != "tcp" { s += "+" + u.Transport } s += ":" if u.User != nil { if n := u.User.Username(); n != "" { s += n + "@" } } if u.Host != "" { s += u.Host } if u.Path != "" && u.Path != "/" { s += u.Path } if u.Opaque != "" { s += u.Opaque } return s } // Normalize returns the driver, host, port, database, and user name of a URL, // joined with sep, populating blank fields with empty. func (u *URL) Normalize(sep, empty string, cut int) string { s := []string{u.UnaliasedDriver, "", "", "", ""} if u.Transport != "tcp" && u.Transport != "unix" { s[0] += "+" + u.Transport } // set host port dbname fields if u.hostPortDB == nil { if u.Opaque != "" { u.hostPortDB = []string{u.Opaque, "", ""} } else { u.hostPortDB = []string{u.Hostname(), u.Port(), strings.TrimPrefix(u.Path, "/")} } } copy(s[1:], u.hostPortDB) // set user if u.User != nil { s[4] = u.User.Username() } // replace blank entries ... for i := 0; i < len(s); i++ { if s[i] == "" { s[i] = empty } } if cut > 0 { // cut to only populated fields i := len(s) - 1 for ; i > cut; i-- { if s[i] != "" { break } } s = s[:i] } return strings.Join(s, sep) } // buildOpaque builds a opaque path. func (u *URL) buildOpaque() string { var up string if u.User != nil { up = u.User.String() + "@" } var q string if u.RawQuery != "" { q = "?" + u.RawQuery } var f string if u.Fragment != "" { f = "#" + u.Fragment } return up + u.opaqueOrPath() + q + f } // opaqueOrPath returns the opaque or path value. func (u *URL) opaqueOrPath() string { if u.Opaque != "" { return u.Opaque } return u.Path } // SchemeType returns the scheme type for a path. func SchemeType(name string) (string, error) { // try to resolve the path on unix systems if runtime.GOOS != "windows" { if typ, ok := resolveType(name); ok { return typ, nil } } if f, err := OpenFile(name); err == nil { defer f.Close() // file exists, match header buf := make([]byte, 64) if n, _ := f.Read(buf); n == 0 { return "sqlite3", nil } for _, typ := range fileTypes { if typ.f(buf) { return typ.driver, nil } } return "", ErrUnknownFileHeader } // doesn't exist, match file extension ext := filepath.Ext(name) for _, typ := range fileTypes { if typ.ext.MatchString(ext) { return typ.driver, nil } } return "", ErrUnknownFileExtension } // Error is an error. type Error string // Error satisfies the error interface. func (err Error) Error() string { return string(err) } // Error values. const ( // ErrInvalidDatabaseScheme is the invalid database scheme error. ErrInvalidDatabaseScheme Error = "invalid database scheme" // ErrUnknownDatabaseScheme is the unknown database type error. ErrUnknownDatabaseScheme Error = "unknown database scheme" // ErrUnknownFileHeader is the unknown file header error. ErrUnknownFileHeader Error = "unknown file header" // ErrUnknownFileExtension is the unknown file extension error. ErrUnknownFileExtension Error = "unknown file extension" // ErrInvalidTransportProtocol is the invalid transport protocol error. ErrInvalidTransportProtocol Error = "invalid transport protocol" // ErrRelativePathNotSupported is the relative paths not supported error. ErrRelativePathNotSupported Error = "relative path not supported" // ErrMissingHost is the missing host error. ErrMissingHost Error = "missing host" // ErrMissingPath is the missing path error. ErrMissingPath Error = "missing path" // ErrMissingUser is the missing user error. ErrMissingUser Error = "missing user" // ErrInvalidQuery is the invalid query error. ErrInvalidQuery Error = "invalid query" ) // Stat is the default stat func. // // Used internally to stat files, and used when generating the DSNs for // postgres://, mysql://, file:// schemes, and opaque [URL]'s. var Stat = func(name string) (fs.FileInfo, error) { return fs.Stat(os.DirFS(filepath.Dir(name)), filepath.Base(name)) } // OpenFile is the default open file func. // // Used internally to read file headers. var OpenFile = func(name string) (fs.File, error) { f, err := os.OpenFile(name, os.O_RDONLY, 0) if err != nil { return nil, err } return f, nil } // BuildURL creates a dsn using the mapped components. // // Recognized components are: // // protocol, proto, scheme // transport // username, user // password, pass // hostname, host // port // path, file, opaque // database, dbname, db // instance // parameters, params, options, opts, query, q // // See [BuildURL] for more information. func BuildURL(components map[string]any) (string, error) { if components == nil { return "", ErrInvalidDatabaseScheme } var urlstr string if proto, ok := getComponent(components, "protocol", "proto", "scheme"); ok { if transport, ok := getComponent(components, "transport"); ok { proto += "+" + transport } urlstr = proto + ":" } if host, ok := getComponent(components, "hostname", "host"); ok { hostinfo := url.QueryEscape(host) if port, ok := getComponent(components, "port"); ok { hostinfo += ":" + port } var userinfo string if user, ok := getComponent(components, "username", "user"); ok { userinfo += url.QueryEscape(user) if pass, ok := getComponent(components, "password", "pass"); ok { userinfo += ":" + url.QueryEscape(pass) } hostinfo = userinfo + "@" + hostinfo } urlstr += "//" + hostinfo } if pathstr, ok := getComponent(components, "path", "file", "opaque"); ok { if urlstr == "" { urlstr += "file:" } urlstr += pathstr } else { var v []string if instance, ok := getComponent(components, "instance"); ok { v = append(v, url.PathEscape(instance)) } if dbname, ok := getComponent(components, "database", "dbname", "db"); ok { v = append(v, url.PathEscape(dbname)) } if len(v) != 0 { if s := path.Join(v...); s != "" { urlstr += "/" + s } } } if v, ok := getFirst(components, "parameters", "params", "options", "opts", "query", "q"); ok { switch z := v.(type) { case string: if z != "" { urlstr += "?" + z } case map[string]any: q := url.Values{} for k, v := range z { q.Set(k, fmt.Sprintf("%v", v)) } if s := q.Encode(); s != "" { urlstr += "?" + s } default: return "", ErrInvalidQuery } } return urlstr, nil } // resolveType tries to resolve a path to a Unix domain socket or directory. func resolveType(s string) (string, bool) { if i := strings.LastIndex(s, "?"); i != -1 { if _, err := Stat(s[:i]); err == nil { s = s[:i] } } dir := s for dir != "" && dir != "/" && dir != "." { // chop off :4444 port i, j := strings.LastIndex(dir, ":"), strings.LastIndex(dir, "/") if i != -1 && i > j { dir = dir[:i] } switch fi, err := Stat(dir); { case err == nil && fi.IsDir(): return "postgres", true case err == nil && fi.Mode()&fs.ModeSocket != 0: return "mysql", true case err == nil: return "", false } if j != -1 { dir = dir[:j] } else { dir = "" } } return "", false } // resolveSocket tries to resolve a path to a Unix domain socket based on the // form "/path/to/socket/dbname" returning either the original path and the // empty string, or the components "/path/to/socket" and "dbname", when // /path/to/socket/dbname is reported by Stat as a socket. func resolveSocket(s string) (string, string) { dir, dbname := s, "" for dir != "" && dir != "/" && dir != "." { if mode(dir)&fs.ModeSocket != 0 { return dir, dbname } dir, dbname = path.Dir(dir), path.Base(dir) } return s, "" } // resolveDir resolves a directory with a :port list. func resolveDir(s string) (string, string, string) { dir := s for dir != "" && dir != "/" && dir != "." { port := "" i, j := strings.LastIndex(dir, ":"), strings.LastIndex(dir, "/") if i != -1 && i > j { port, dir = dir[i+1:], dir[:i] } if mode(dir)&fs.ModeDir != 0 { dbname := strings.TrimPrefix(strings.TrimPrefix(strings.TrimPrefix(s, dir), ":"+port), "/") return dir, port, dbname } if j != -1 { dir = dir[:j] } else { dir = "" } } return s, "", "" } // mode returns the mode of the path. func mode(s string) os.FileMode { if fi, err := Stat(s); err == nil { return fi.Mode() } return 0 } // getComponent returns the first defined component in the map. func getComponent(m map[string]any, v ...string) (string, bool) { if z, ok := getFirst(m, v...); ok { str := fmt.Sprintf("%v", z) return str, str != "" } return "", false } // getFirst returns the first value in the map. func getFirst(m map[string]any, v ...string) (any, bool) { for _, s := range v { if z, ok := m[s]; ok { return z, ok } } return nil, false } dburl-0.23.7/dburl_test.go000066400000000000000000000620171500027635200154170ustar00rootroot00000000000000package dburl import ( "errors" "io/fs" "os" "strconv" "testing" "time" ) func TestBadParse(t *testing.T) { tests := []struct { s string exp error }{ {``, ErrInvalidDatabaseScheme}, {` `, ErrInvalidDatabaseScheme}, {`pgsqlx://`, ErrUnknownDatabaseScheme}, {`m`, ErrInvalidDatabaseScheme}, {`pg+udp://user:pass@localhost/dbname`, ErrInvalidTransportProtocol}, {`sqlite+unix://`, ErrInvalidTransportProtocol}, {`sqlite+tcp://`, ErrInvalidTransportProtocol}, {`file+tcp://`, ErrInvalidTransportProtocol}, {`file://`, ErrMissingPath}, {`ql://`, ErrMissingPath}, {`mssql+tcp://user:pass@host/dbname`, ErrInvalidTransportProtocol}, {`mssql+foobar://`, ErrInvalidTransportProtocol}, {`mssql+unix:/var/run/mssql.sock`, ErrInvalidTransportProtocol}, {`mssql+udp:localhost:155`, ErrInvalidTransportProtocol}, {`adodb+foo+bar://provider/database`, ErrInvalidTransportProtocol}, {`memsql:/var/run/mysqld/mysqld.sock`, ErrInvalidTransportProtocol}, {`tidb:/var/run/mysqld/mysqld.sock`, ErrInvalidTransportProtocol}, {`vitess:/var/run/mysqld/mysqld.sock`, ErrInvalidTransportProtocol}, {`memsql+unix:///var/run/mysqld/mysqld.sock`, ErrInvalidTransportProtocol}, {`tidb+unix:///var/run/mysqld/mysqld.sock`, ErrInvalidTransportProtocol}, {`vitess+unix:///var/run/mysqld/mysqld.sock`, ErrInvalidTransportProtocol}, {`cockroach:/var/run/postgresql`, ErrInvalidTransportProtocol}, {`cockroach+unix:/var/run/postgresql`, ErrInvalidTransportProtocol}, {`cockroach:./path`, ErrInvalidTransportProtocol}, {`cockroach+unix:./path`, ErrInvalidTransportProtocol}, {`redshift:/var/run/postgresql`, ErrInvalidTransportProtocol}, {`redshift+unix:/var/run/postgresql`, ErrInvalidTransportProtocol}, {`redshift:./path`, ErrInvalidTransportProtocol}, {`redshift+unix:./path`, ErrInvalidTransportProtocol}, {`pg:./path/to/socket`, ErrRelativePathNotSupported}, // relative paths are not possible for postgres sockets {`pg+unix:./path/to/socket`, ErrRelativePathNotSupported}, {`snowflake://`, ErrMissingHost}, {`sf://`, ErrMissingHost}, {`snowflake://account`, ErrMissingUser}, {`sf://account`, ErrMissingUser}, {`mq+unix://`, ErrInvalidTransportProtocol}, {`mq+tcp://`, ErrInvalidTransportProtocol}, {`ots+tcp://`, ErrInvalidTransportProtocol}, {`tablestore+tcp://`, ErrInvalidTransportProtocol}, {`bend://`, ErrMissingHost}, {`databend://`, ErrMissingHost}, {`unknown_file.ext3`, ErrInvalidDatabaseScheme}, } for i, tt := range tests { test := tt t.Run(strconv.Itoa(i), func(t *testing.T) { testBadParse(t, test.s, test.exp) }) } } func testBadParse(t *testing.T, s string, exp error) { t.Helper() _, err := Parse(s) switch { case err == nil: t.Errorf("%q expected error nil error, got: %v", s, err) case !errors.Is(err, exp): t.Errorf("%q expected error %v, got: %v", s, exp, err) } } func TestParse(t *testing.T) { OdbcIgnoreQueryPrefixes = []string{"usql_"} tests := []struct { s string d string exp string path string }{ { `pg:`, `postgres`, ``, ``, }, { `pg://`, `postgres`, ``, ``, }, { `pg:user:pass@localhost/booktest`, `postgres`, `dbname=booktest host=localhost password=pass user=user`, ``, }, { `pg:/var/run/postgresql`, `postgres`, `host=/var/run/postgresql`, `/var/run/postgresql`, }, { `pg:/var/run/postgresql:6666/mydb`, `postgres`, `dbname=mydb host=/var/run/postgresql port=6666`, `/var/run/postgresql`, }, { `/var/run/postgresql:6666/mydb`, `postgres`, `dbname=mydb host=/var/run/postgresql port=6666`, `/var/run/postgresql`, }, { `pg:/var/run/postgresql/mydb`, `postgres`, `dbname=mydb host=/var/run/postgresql`, `/var/run/postgresql`, }, { `/var/run/postgresql/mydb`, `postgres`, `dbname=mydb host=/var/run/postgresql`, `/var/run/postgresql`, }, { `pg:/var/run/postgresql:7777`, `postgres`, `host=/var/run/postgresql port=7777`, `/var/run/postgresql`, }, { `pg+unix:/var/run/postgresql:4444/booktest`, `postgres`, `dbname=booktest host=/var/run/postgresql port=4444`, `/var/run/postgresql`, }, { `/var/run/postgresql:7777`, `postgres`, `host=/var/run/postgresql port=7777`, `/var/run/postgresql`, }, { `pg:user:pass@/var/run/postgresql/mydb`, `postgres`, `dbname=mydb host=/var/run/postgresql password=pass user=user`, `/var/run/postgresql`, }, { `pg:user:pass@/really/bad/path`, `postgres`, `host=/really/bad/path password=pass user=user`, ``, }, { `my:`, `mysql`, `tcp(localhost:3306)/`, ``, }, { `my://`, `mysql`, `tcp(localhost:3306)/`, ``, }, { `my:booktest:booktest@localhost/booktest`, `mysql`, `booktest:booktest@tcp(localhost:3306)/booktest`, ``, }, { `my:/var/run/mysqld/mysqld.sock/mydb?timeout=90`, `mysql`, `unix(/var/run/mysqld/mysqld.sock)/mydb?timeout=90`, `/var/run/mysqld/mysqld.sock`, }, { `/var/run/mysqld/mysqld.sock/mydb?timeout=90`, `mysql`, `unix(/var/run/mysqld/mysqld.sock)/mydb?timeout=90`, `/var/run/mysqld/mysqld.sock`, }, { `my:///var/run/mysqld/mysqld.sock/mydb?timeout=90`, `mysql`, `unix(/var/run/mysqld/mysqld.sock)/mydb?timeout=90`, `/var/run/mysqld/mysqld.sock`, }, { `my+unix:user:pass@mysqld.sock?timeout=90`, `mysql`, `user:pass@unix(mysqld.sock)/?timeout=90`, ``, }, { `my:./path/to/socket`, `mysql`, `unix(path/to/socket)/`, ``, }, { `my+unix:./path/to/socket`, `mysql`, `unix(path/to/socket)/`, ``, }, { `mymy:`, `mymysql`, `tcp:localhost:3306*//`, ``, }, { `mymy://`, `mymysql`, `tcp:localhost:3306*//`, ``, }, { `mymy:user:pass@localhost/booktest`, `mymysql`, `tcp:localhost:3306*booktest/user/pass`, ``, }, { `mymy:/var/run/mysqld/mysqld.sock/mydb?timeout=90&test=true`, `mymysql`, `unix:/var/run/mysqld/mysqld.sock,test,timeout=90*mydb`, `/var/run/mysqld/mysqld.sock`, }, { `mymy:///var/run/mysqld/mysqld.sock/mydb?timeout=90`, `mymysql`, `unix:/var/run/mysqld/mysqld.sock,timeout=90*mydb`, `/var/run/mysqld/mysqld.sock`, }, { `mymy+unix:user:pass@mysqld.sock?timeout=90`, `mymysql`, `unix:mysqld.sock,timeout=90*/user/pass`, ``, }, { `mymy:./path/to/socket`, `mymysql`, `unix:path/to/socket*//`, ``, }, { `mymy+unix:./path/to/socket`, `mymysql`, `unix:path/to/socket*//`, ``, }, { `mssql://`, `sqlserver`, `sqlserver://localhost`, ``, }, { `mssql://user:pass@localhost/dbname`, `sqlserver`, `sqlserver://user:pass@localhost/?database=dbname`, ``, }, { `mssql://user@localhost/service/dbname`, `sqlserver`, `sqlserver://user@localhost/service?database=dbname`, ``, }, { `mssql://user:!234%23$@localhost:1580/dbname`, `sqlserver`, `sqlserver://user:%21234%23$@localhost:1580/?database=dbname`, ``, }, { `mssql://user:!234%23$@localhost:1580/service/dbname?fedauth=true`, `azuresql`, `sqlserver://user:%21234%23$@localhost:1580/service?database=dbname&fedauth=true`, ``, }, { `azuresql://user:pass@localhost:100/dbname`, `azuresql`, `sqlserver://user:pass@localhost:100/?database=dbname`, ``, }, { `sqlserver://xxx.database.windows.net?database=xxx&fedauth=ActiveDirectoryMSI`, `azuresql`, `sqlserver://xxx.database.windows.net?database=xxx&fedauth=ActiveDirectoryMSI`, ``, }, { `azuresql://xxx.database.windows.net/dbname?fedauth=ActiveDirectoryMSI`, `azuresql`, `sqlserver://xxx.database.windows.net/?database=dbname&fedauth=ActiveDirectoryMSI`, ``, }, { `adodb://Microsoft.ACE.OLEDB.12.0?Extended+Properties=%22Text%3BHDR%3DNO%3BFMT%3DDelimited%22`, `adodb`, `Data Source=.;Extended Properties="Text;HDR=NO;FMT=Delimited";Provider=Microsoft.ACE.OLEDB.12.0`, ``, }, { `adodb://user:pass@Provider.Name:1542/Oracle8i/dbname`, `adodb`, `Data Source=Oracle8i;Database=dbname;Password=pass;Port=1542;Provider=Provider.Name;User ID=user`, ``, }, { `adodb://user:pass@Provider.Name:1542/Oracle8i/dbname?not_ignored=1&usql_ignore=1`, `adodb`, `Data Source=Oracle8i;Database=dbname;Password=pass;Port=1542;Provider=Provider.Name;User ID=user;not_ignored=1`, ``, }, { `oo+Postgres+Unicode://user:pass@host:5432/dbname`, `adodb`, `Provider=MSDASQL.1;Extended Properties="Database=dbname;Driver={Postgres Unicode};PWD=pass;Port=5432;Server=host;UID=user"`, ``, }, { `oo+Postgres+Unicode://user:pass@host:5432/dbname?not_ignored=1&usql_ignore=1`, `adodb`, `Provider=MSDASQL.1;Extended Properties="Database=dbname;Driver={Postgres Unicode};PWD=pass;Port=5432;Server=host;UID=user;not_ignored=1"`, ``, }, { `odbc+Postgres+Unicode://user:pass@host:5432/dbname?not_ignored=1`, `odbc`, `Database=dbname;Driver={Postgres Unicode};PWD=pass;Port=5432;Server=host;UID=user;not_ignored=1`, ``, }, { `odbc+Postgres+Unicode://user:pass@host:5432/dbname?usql_ignore=1¬_ignored=1`, `odbc`, `Database=dbname;Driver={Postgres Unicode};PWD=pass;Port=5432;Server=host;UID=user;not_ignored=1`, ``, }, { `sqlite:///path/to/file.sqlite3`, `sqlite3`, `/path/to/file.sqlite3`, ``, }, { `sq://path/to/file.sqlite3`, `sqlite3`, `path/to/file.sqlite3`, ``, }, { `sq:path/to/file.sqlite3`, `sqlite3`, `path/to/file.sqlite3`, ``, }, { `sq:./path/to/file.sqlite3`, `sqlite3`, `./path/to/file.sqlite3`, ``, }, { `sq://./path/to/file.sqlite3?loc=auto`, `sqlite3`, `./path/to/file.sqlite3?loc=auto`, ``, }, { `sq::memory:?loc=auto`, `sqlite3`, `:memory:?loc=auto`, ``, }, { `sq://:memory:?loc=auto`, `sqlite3`, `:memory:?loc=auto`, ``, }, { `or://user:pass@localhost:3000/sidname`, `oracle`, `oracle://user:pass@localhost:3000/sidname`, ``, }, { `or://localhost`, `oracle`, `oracle://localhost:1521`, ``, }, { `oracle://user:pass@localhost`, `oracle`, `oracle://user:pass@localhost:1521`, ``, }, { `oracle://user:pass@localhost/service_name/instance_name`, `oracle`, `oracle://user:pass@localhost:1521/service_name/instance_name`, ``, }, { `oracle://user:pass@localhost:2000/xe.oracle.docker`, `oracle`, `oracle://user:pass@localhost:2000/xe.oracle.docker`, ``, }, { `or://username:password@host/ORCL`, `oracle`, `oracle://username:password@host:1521/ORCL`, ``, }, { `odpi://username:password@sales-server:1521/sales.us.acme.com`, `oracle`, `oracle://username:password@sales-server:1521/sales.us.acme.com`, ``, }, { `oracle://username:password@sales-server.us.acme.com/sales.us.oracle.com`, `oracle`, `oracle://username:password@sales-server.us.acme.com:1521/sales.us.oracle.com`, ``, }, { `presto://host:8001/`, `presto`, `http://user@host:8001?catalog=default`, ``, }, { `presto://host/catalogname/schemaname`, `presto`, `http://user@host:8080?catalog=catalogname&schema=schemaname`, ``, }, { `prs://admin@host/catalogname`, `presto`, `https://admin@host:8443?catalog=catalogname`, ``, }, { `prestodbs://admin:pass@host:9998/catalogname`, `presto`, `https://admin:pass@host:9998?catalog=catalogname`, ``, }, { `ca://host`, `cql`, `host:9042`, ``, }, { `cassandra://host:9999`, `cql`, `host:9999`, ``, }, { `scy://user@host:9999`, `cql`, `host:9999?username=user`, ``, }, { `scylla://user@host:9999?timeout=1000`, `cql`, `host:9999?timeout=1000&username=user`, ``, }, { `datastax://user:pass@localhost:9999/?timeout=1000`, `cql`, `localhost:9999?password=pass&timeout=1000&username=user`, ``, }, { `ca://user:pass@localhost:9999/dbname?timeout=1000`, `cql`, `localhost:9999?keyspace=dbname&password=pass&timeout=1000&username=user`, ``, }, { `ig://host`, `ignite`, `tcp://host:10800`, ``, }, { `ignite://host:9999`, `ignite`, `tcp://host:9999`, ``, }, { `gridgain://user@host:9999`, `ignite`, `tcp://host:9999?username=user`, ``, }, { `ig://user@host:9999?timeout=1000`, `ignite`, `tcp://host:9999?timeout=1000&username=user`, ``, }, { `ig://user:pass@localhost:9999/?timeout=1000`, `ignite`, `tcp://localhost:9999?password=pass&timeout=1000&username=user`, ``, }, { `ig://user:pass@localhost:9999/dbname?timeout=1000`, `ignite`, `tcp://localhost:9999/dbname?password=pass&timeout=1000&username=user`, ``, }, { `sf://user@host:9999/dbname/schema?timeout=1000`, `snowflake`, `user@host:9999/dbname/schema?timeout=1000`, ``, }, { `sf://user:pass@localhost:9999/dbname/schema?timeout=1000`, `snowflake`, `user:pass@localhost:9999/dbname/schema?timeout=1000`, ``, }, { `rs://user:pass@amazon.com/dbname`, `postgres`, `postgres://user:pass@amazon.com:5439/dbname`, ``, }, { `ve://`, `vertica`, `vertica://localhost:5433/`, ``, }, { `ve://user:pass@vertica-host/dbvertica?tlsmode=server-strict`, `vertica`, `vertica://user:pass@vertica-host:5433/dbvertica?tlsmode=server-strict`, ``, }, { `vertica://vertica:P4ssw0rd@localhost/vertica`, `vertica`, `vertica://vertica:P4ssw0rd@localhost:5433/vertica`, ``, }, { `ve://vertica:P4ssw0rd@localhost:5433/vertica`, `vertica`, `vertica://vertica:P4ssw0rd@localhost:5433/vertica`, ``, }, { `moderncsqlite:///path/to/file.sqlite3`, `moderncsqlite`, `/path/to/file.sqlite3`, ``, }, { `modernsqlite:///path/to/file.sqlite3`, `moderncsqlite`, `/path/to/file.sqlite3`, ``, }, { `mq://path/to/file.sqlite3`, `moderncsqlite`, `path/to/file.sqlite3`, ``, }, { `mq:path/to/file.sqlite3`, `moderncsqlite`, `path/to/file.sqlite3`, ``, }, { `mq:./path/to/file.sqlite3`, `moderncsqlite`, `./path/to/file.sqlite3`, ``, }, { `mq://./path/to/file.sqlite3?loc=auto`, `moderncsqlite`, `./path/to/file.sqlite3?loc=auto`, ``, }, { `mq::memory:?loc=auto`, `moderncsqlite`, `:memory:?loc=auto`, ``, }, { `mq://:memory:?loc=auto`, `moderncsqlite`, `:memory:?loc=auto`, ``, }, { `gr://user:pass@localhost:3000/sidname`, `godror`, `user/pass@//localhost:3000/sidname`, ``, }, { `gr://localhost`, `godror`, `localhost`, ``, }, { `godror://user:pass@localhost`, `godror`, `user/pass@//localhost`, ``, }, { `godror://user:pass@localhost/service_name/instance_name`, `godror`, `user/pass@//localhost/service_name/instance_name`, ``, }, { `godror://user:pass@localhost:2000/xe.oracle.docker`, `godror`, `user/pass@//localhost:2000/xe.oracle.docker`, ``, }, { `gr://username:password@host/ORCL`, `godror`, `username/password@//host/ORCL`, ``, }, { `gr://username:password@sales-server:1521/sales.us.acme.com`, `godror`, `username/password@//sales-server:1521/sales.us.acme.com`, ``, }, { `godror://username:password@sales-server.us.acme.com/sales.us.oracle.com`, `godror`, `username/password@//sales-server.us.acme.com/sales.us.oracle.com`, ``, }, { `trino://host:8001/`, `trino`, `http://user@host:8001?catalog=default`, ``, }, { `trino://host/catalogname/schemaname`, `trino`, `http://user@host:8080?catalog=catalogname&schema=schemaname`, ``, }, { `trs://admin@host/catalogname`, `trino`, `https://admin@host:8443?catalog=catalogname`, ``, }, { `pgx://`, `pgx`, `postgres://localhost:5432/`, ``, }, { `ca://`, `cql`, `localhost:9042`, ``, }, { `exa://`, `exasol`, `exa:localhost:8563`, ``, }, { `exa://user:pass@host:1883/dbname?autocommit=1`, `exasol`, `exa:host:1883;autocommit=1;password=pass;schema=dbname;user=user`, ``, }, { `ots://user:pass@localhost/instance_name`, `ots`, `https://user:pass@localhost/instance_name`, ``, }, { `ots+https://user:pass@localhost/instance_name`, `ots`, `https://user:pass@localhost/instance_name`, ``, }, { `ots+http://user:pass@localhost/instance_name`, `ots`, `http://user:pass@localhost/instance_name`, ``, }, { `tablestore://user:pass@localhost/instance_name`, `ots`, `https://user:pass@localhost/instance_name`, ``, }, { `tablestore+https://user:pass@localhost/instance_name`, `ots`, `https://user:pass@localhost/instance_name`, ``, }, { `tablestore+http://user:pass@localhost/instance_name`, `ots`, `http://user:pass@localhost/instance_name`, ``, }, { `bend://user:pass@localhost/instance_name?sslmode=disabled&warehouse=wh`, `databend`, `bend://user:pass@localhost/instance_name?sslmode=disabled&warehouse=wh`, ``, }, { `databend://user:pass@localhost/instance_name?tenant=tn&warehouse=wh`, `databend`, `databend://user:pass@localhost/instance_name?tenant=tn&warehouse=wh`, ``, }, { `flightsql://user:pass@localhost?timeout=3s&token=foobar&tls=enabled`, `flightsql`, `flightsql://user:pass@localhost?timeout=3s&token=foobar&tls=enabled`, ``, }, { `duckdb:/path/to/foo.db?access_mode=read_only&threads=4`, `duckdb`, `/path/to/foo.db?access_mode=read_only&threads=4`, ``, }, { `dk:///path/to/foo.db?access_mode=read_only&threads=4`, `duckdb`, `/path/to/foo.db?access_mode=read_only&threads=4`, ``, }, { `duckdb://`, `duckdb`, ``, ``, }, { `duckdb://:memory:`, `duckdb`, `:memory:`, ``, }, { `duckdb:?threads=4`, `duckdb`, `?threads=4`, ``, }, { `file:./testdata/test.sqlite3?a=b`, `sqlite3`, `./testdata/test.sqlite3?a=b`, ``, }, { `file:./testdata/test.duckdb?a=b`, `duckdb`, `./testdata/test.duckdb?a=b`, ``, }, { `file:__nonexistent__.db`, `sqlite3`, `__nonexistent__.db`, ``, }, { `file:__nonexistent__.sqlite3`, `sqlite3`, `__nonexistent__.sqlite3`, ``, }, { `file:__nonexistent__.duckdb`, `duckdb`, `__nonexistent__.duckdb`, ``, }, { `__nonexistent__.db`, `sqlite3`, `__nonexistent__.db`, ``, }, { `__nonexistent__.sqlite3`, `sqlite3`, `__nonexistent__.sqlite3`, ``, }, { `__nonexistent__.duckdb`, `duckdb`, `__nonexistent__.duckdb`, ``, }, { `file:fake.sqlite3?a=b`, `sqlite3`, `fake.sqlite3?a=b`, ``, }, { `fake.sq`, `sqlite3`, `fake.sq`, ``, }, { `file:fake.duckdb?a=b`, `duckdb`, `fake.duckdb?a=b`, ``, }, { `fake.dk`, `duckdb`, `fake.dk`, ``, }, { `file:/var/run/mysqld/mysqld.sock/mydb?timeout=90`, `mysql`, `unix(/var/run/mysqld/mysqld.sock)/mydb?timeout=90`, `/var/run/mysqld/mysqld.sock`, }, { `file:/var/run/postgresql`, `postgres`, `host=/var/run/postgresql`, `/var/run/postgresql`, }, { `file:/var/run/postgresql:6666/mydb`, `postgres`, `dbname=mydb host=/var/run/postgresql port=6666`, `/var/run/postgresql`, }, { `file:/var/run/postgresql/mydb`, `postgres`, `dbname=mydb host=/var/run/postgresql`, `/var/run/postgresql`, }, { `file:/var/run/postgresql:7777`, `postgres`, `host=/var/run/postgresql port=7777`, `/var/run/postgresql`, }, { `file://user:pass@/var/run/postgresql/mydb`, `postgres`, `dbname=mydb host=/var/run/postgresql password=pass user=user`, `/var/run/postgresql`, }, { `hive://myhost/mydb`, `hive`, `myhost:10000/mydb`, ``, }, { `hi://myhost:9999/mydb?auth=PLAIN`, `hive`, `myhost:9999/mydb?auth=PLAIN`, ``, }, { `hive2://user:pass@myhost:9999/mydb?auth=PLAIN`, `hive`, `user:pass@myhost:9999/mydb?auth=PLAIN`, ``, }, { `dy://user:pass@myhost:9999?TimeoutMs=1000`, `godynamo`, `Region=myhost;AkId=user;Secret_Key=pass;TimeoutMs=1000`, ``, }, { `br://user:pass@dbname`, `databricks`, `token:user@pass.databricks.com:443/sql/1.0/endpoints/dbname`, ``, }, { `brick://user:pass@dbname?timeout=1000&maxRows=1000`, `databricks`, `token:user@pass.databricks.com:443/sql/1.0/endpoints/dbname?maxRows=1000&timeout=1000`, ``, }, { `ydb://`, `ydb`, `grpc://localhost:2136/`, ``, }, { `yds://`, `ydb`, `grpcs://localhost:2135/`, ``, }, { `ydbs://user:pass@localhost:8888/?opt1=a&opt2=b`, `ydb`, `grpcs://user:pass@localhost:8888/?opt1=a&opt2=b`, ``, }, { `clickhouse://user:pass@localhost/?opt1=a&opt2=b`, `clickhouse`, `clickhouse://user:pass@localhost:9000/?opt1=a&opt2=b`, ``, }, { `clickhouse+http://user:pass@localhost/?opt1=a&opt2=b`, `clickhouse`, `http://user:pass@localhost/?opt1=a&opt2=b`, ``, }, { `clickhouse+https://user:pass@host/?opt1=a&opt2=b`, `clickhouse`, `https://user:pass@host/?opt1=a&opt2=b`, ``, }, } m := make(map[string]bool) for i, tt := range tests { test := tt t.Run(strconv.Itoa(i), func(t *testing.T) { if _, ok := m[test.s]; ok { t.Fatalf("%s is already tested", test.s) } m[test.s] = true testParse(t, test.s, test.d, test.exp, test.path) }) } } func testParse(t *testing.T, s, d, exp, path string) { t.Helper() u, err := Parse(s) switch { case err != nil: t.Errorf("%q expected no error, got: %v", s, err) case u.GoDriver != "" && u.GoDriver != d: t.Errorf("%q expected go driver %q, got: %q", s, d, u.GoDriver) case u.GoDriver == "" && u.Driver != d: t.Errorf("%q expected driver %q, got: %q", s, d, u.Driver) case u.DSN != exp: _, err := os.Stat(path) if path != "" && err != nil && os.IsNotExist(err) { t.Logf("%q expected dsn %q, got: %q -- ignoring because `%s` does not exist", s, exp, u.DSN, path) } else { t.Errorf("%q expected:\n%q\ngot:\n%q", s, exp, u.DSN) } } } func TestBuildURL(t *testing.T) { tests := []struct { m map[string]any exp string err error }{ {nil, "", ErrInvalidDatabaseScheme}, { map[string]any{ "proto": "mysql", "transport": "tcp", "host": "localhost", "port": 999, "q": map[string]any{ "foo": "bar", "opt1": "b", }, }, "mysql+tcp://localhost:999?foo=bar&opt1=b", nil, }, { map[string]any{ "proto": "sqlserver", "host": "localhost", "port": "5555", "instance": "instance", "database": "dbname", "q": map[string]any{ "foo": "bar", "opt1": "b", }, }, "sqlserver://localhost:5555/instance/dbname?foo=bar&opt1=b", nil, }, { map[string]any{ "proto": "pg", "host": "host name", "user": "user name", "password": "P!!!@@@@ 👀", "database": "my awesome db", "q": map[string]any{ "foo": "bar is cool", "opt1": "b zzzz@@@:/", }, }, "pg://user+name:P%21%21%21%40%40%40%40+%F0%9F%91%80@host+name/my%20awesome%20db?foo=bar+is+cool&opt1=b+zzzz%40%40%40%3A%2F", nil, }, { map[string]any{ "file": "fake.sqlite3", "q": map[string]any{ "foo": "bar", "opt1": "b", }, }, "file:fake.sqlite3?foo=bar&opt1=b", nil, }, } for i, test := range tests { t.Run(strconv.Itoa(i), func(t *testing.T) { switch s, err := BuildURL(test.m); { case err != nil && !errors.Is(err, test.err): t.Fatalf("expected error %v, got: %v", test.err, err) case err != nil && test.err == nil: t.Fatalf("expected no error, got: %v", err) case s != test.exp: t.Errorf("expected %q, got: %q", test.exp, s) default: t.Logf("dsn: %q", s) } switch u, err := FromMap(test.m); { case err != nil: t.Logf("parse error: %v", err) default: t.Logf("url: %q", u.String()) } }) } } func init() { statFile, openFile := Stat, OpenFile Stat = func(name string) (fs.FileInfo, error) { if s, ok := newStat(name); ok { return s, nil } return statFile(name) } OpenFile = func(name string) (fs.File, error) { if s, ok := newStat(name); ok { return s, nil } return openFile(name) } } type stat struct { name string mode fs.FileMode content string } func newStat(name string) (stat, bool) { const ( sqlite3Header = "SQLite format 3\000.........." duckdbHeader = "12345678DUCK87654321.............." ) files := map[string]string{ "fake.sqlite3": sqlite3Header, "fake.sq": sqlite3Header, "fake.duckdb": duckdbHeader, "fake.dk": duckdbHeader, } switch name { case "/var/run/postgresql": return stat{name, fs.ModeDir, ""}, true case "/var/run/mysqld/mysqld.sock": return stat{name, fs.ModeSocket, ""}, true case "fake.sqlite3", "fake.sq", "fake.duckdb", "fake.dk": return stat{name, 0, files[name]}, true } return stat{}, false } func (s stat) Name() string { return s.name } func (s stat) Size() int64 { return int64(len(s.content)) } func (s stat) Mode() fs.FileMode { return s.mode } func (s stat) ModTime() time.Time { return time.Now() } func (s stat) IsDir() bool { return s.mode&fs.ModeDir != 0 } func (s stat) Sys() any { return nil } func (s stat) Close() error { return nil } func (s stat) Stat() (fs.FileInfo, error) { return s, nil } func (s stat) Read(b []byte) (int, error) { v := []byte(s.content) copy(b, v) return len(v), nil } dburl-0.23.7/dsn.go000066400000000000000000000452411500027635200140340ustar00rootroot00000000000000package dburl import ( "fmt" "net/url" "path" "sort" "strings" ) // OdbcIgnoreQueryPrefixes are the query prefixes to ignore when generating the // odbc DSN. Used by GenOdbc var OdbcIgnoreQueryPrefixes []string // GenScheme returns a generator that will generate a scheme based on the // passed scheme DSN. func GenScheme(scheme string) func(*URL) (string, string, error) { return func(u *URL) (string, string, error) { z := &url.URL{ Scheme: scheme, Opaque: u.Opaque, User: u.User, Host: u.Host, Path: u.Path, RawPath: u.RawPath, RawQuery: u.RawQuery, Fragment: u.Fragment, } if z.Host == "" { z.Host = "localhost" } return z.String(), "", nil } } // GenFromURL returns a func that generates a DSN based on parameters of the // passed URL. func GenFromURL(urlstr string) func(*URL) (string, string, error) { z, err := url.Parse(urlstr) if err != nil { panic(err) } return func(u *URL) (string, string, error) { opaque := z.Opaque if u.Opaque != "" { opaque = u.Opaque } user := z.User if u.User != nil { user = u.User } host, port := z.Hostname(), z.Port() if h := u.Hostname(); h != "" { host = h } if p := u.Port(); p != "" { port = p } if port != "" { host += ":" + port } pstr := z.Path if u.Path != "" { pstr = u.Path } rawPath := z.RawPath if u.RawPath != "" { rawPath = u.RawPath } q := z.Query() for k, v := range u.Query() { q.Set(k, strings.Join(v, " ")) } fragment := z.Fragment if u.Fragment != "" { fragment = u.Fragment } y := &url.URL{ Scheme: z.Scheme, Opaque: opaque, User: user, Host: host, Path: pstr, RawPath: rawPath, RawQuery: q.Encode(), Fragment: fragment, } return strings.TrimPrefix(y.String(), "truncate://"), "", nil } } // GenOpaque generates a opaque file path DSN from the passed URL. func GenOpaque(u *URL) (string, string, error) { if u.Opaque == "" { return "", "", ErrMissingPath } return u.Opaque + genQueryOptions(u.Query()), "", nil } // GenAdodb generates a adodb DSN from the passed URL. func GenAdodb(u *URL) (string, string, error) { // grab data source host, port := u.Hostname(), u.Port() dsname, dbname := strings.TrimPrefix(u.Path, "/"), "" if dsname == "" { dsname = "." } // check if data source is not a path on disk if mode(dsname) == 0 { if i := strings.IndexAny(dsname, `\/`); i != -1 { dbname = dsname[i+1:] dsname = dsname[:i] } } // build q q := u.Query() q.Set("Provider", host) q.Set("Port", port) q.Set("Data Source", dsname) q.Set("Database", dbname) if u.User != nil { q.Set("User ID", u.User.Username()) pass, _ := u.User.Password() q.Set("Password", pass) } if u.hostPortDB == nil { n := dsname if dbname != "" { n += "/" + dbname } u.hostPortDB = []string{host, port, n} } return genOptionsOdbc(q, true, nil, OdbcIgnoreQueryPrefixes), "", nil } // GenCassandra generates a cassandra DSN from the passed URL. func GenCassandra(u *URL) (string, string, error) { host, port, dbname := "localhost", "9042", strings.TrimPrefix(u.Path, "/") if h := u.Hostname(); h != "" { host = h } if p := u.Port(); p != "" { port = p } q := u.Query() // add user/pass if u.User != nil { q.Set("username", u.User.Username()) if pass, _ := u.User.Password(); pass != "" { q.Set("password", pass) } } // add dbname if dbname != "" { q.Set("keyspace", dbname) } return host + ":" + port + genQueryOptions(q), "", nil } // GenClickhouse generates a clickhouse DSN from the passed URL. func GenClickhouse(u *URL) (string, string, error) { switch strings.ToLower(u.Transport) { case "", "tcp": return clickhouseTCP(u) case "http": return clickhouseHTTP(u) case "https": return clickhouseHTTPS(u) } return "", "", ErrInvalidTransportProtocol } // clickhouse generators. var ( clickhouseTCP = GenFromURL("clickhouse://localhost:9000/") clickhouseHTTP = GenFromURL("http://localhost/") clickhouseHTTPS = GenFromURL("https://localhost/") ) // GenCosmos generates a cosmos DSN from the passed URL. func GenCosmos(u *URL) (string, string, error) { host, port, dbname := u.Hostname(), u.Port(), strings.TrimPrefix(u.Path, "/") if port != "" { port = ":" + port } q := u.Query() q.Set("AccountEndpoint", "https://"+host+port) // add user/pass if u.User == nil { return "", "", ErrMissingUser } q.Set("AccountKey", u.User.Username()) if dbname != "" { q.Set("Db", dbname) } return genOptionsOdbc(q, true, nil, nil), "gocosmos", nil } // GenDatabend generates a databend DSN from the passed URL. func GenDatabend(u *URL) (string, string, error) { if u.Hostname() == "" { return "", "", ErrMissingHost } return u.String(), "", nil } // GenDynamo generates a dynamo DSN from the passed URL. func GenDynamo(u *URL) (string, string, error) { var v []string if host := u.Hostname(); host != "" { v = append(v, "Region="+host) } if u.User != nil { v = append(v, "AkId="+u.User.Username()) if pass, ok := u.User.Password(); ok { v = append(v, "Secret_Key="+pass) } } return strings.Join(v, ";") + genOptions(u.Query(), ";", "=", ";", ",", true, []string{"Region", "Secret_Key", "AkId"}, nil), "", nil } // GenDatabricks generates a databricks DSN from the passed URL. func GenDatabricks(u *URL) (string, string, error) { if u.User == nil { return "", "", ErrMissingUser } user := u.User.Username() pass, ok := u.User.Password() if !ok || pass == "" { return "", "", ErrMissingUser } host, port := u.Hostname(), u.Port() if host == "" { return "", "", ErrMissingHost } if port == "" { port = "443" } s := fmt.Sprintf("token:%s@%s.databricks.com:%s/sql/1.0/endpoints/%s", user, pass, port, host) return s + genOptions(u.Query(), "?", "=", "&", ",", true, nil, nil), "", nil } // GenExasol generates a exasol DSN from the passed URL. func GenExasol(u *URL) (string, string, error) { host, port, dbname := u.Hostname(), u.Port(), strings.TrimPrefix(u.Path, "/") if host == "" { host = "localhost" } if port == "" { port = "8563" } q := u.Query() if dbname != "" { q.Set("schema", dbname) } if u.User != nil { q.Set("user", u.User.Username()) pass, _ := u.User.Password() q.Set("password", pass) } return fmt.Sprintf("exa:%s:%s%s", host, port, genOptions(q, ";", "=", ";", ",", true, nil, nil)), "", nil } // GenFirebird generates a firebird DSN from the passed URL. func GenFirebird(u *URL) (string, string, error) { z := &url.URL{ User: u.User, Host: u.Host, Path: u.Path, RawPath: u.RawPath, RawQuery: u.RawQuery, Fragment: u.Fragment, } return strings.TrimPrefix(z.String(), "//"), "", nil } // GenGodror generates a godror DSN from the passed URL. func GenGodror(u *URL) (string, string, error) { // Easy Connect Naming method enables clients to connect to a database server // without any configuration. Clients use a connect string for a simple TCP/IP // address, which includes a host name and optional port and service name: // CONNECT username[/password]@[//]host[:port][/service_name][:server][/instance_name] host, port, service := u.Hostname(), u.Port(), strings.TrimPrefix(u.Path, "/") // grab instance name from service name var instance string if i := strings.LastIndex(service, "/"); i != -1 { instance, service = service[i+1:], service[:i] } // build dsn dsn := host if port != "" { dsn += ":" + port } if u.User != nil { if n := u.User.Username(); n != "" { if p, ok := u.User.Password(); ok { n += "/" + p } dsn = n + "@//" + dsn } } if service != "" { dsn += "/" + service } if instance != "" { dsn += "/" + instance } return dsn, "", nil } // GenIgnite generates an ignite DSN from the passed URL. func GenIgnite(u *URL) (string, string, error) { host, port, dbname := "localhost", "10800", strings.TrimPrefix(u.Path, "/") if h := u.Hostname(); h != "" { host = h } if p := u.Port(); p != "" { port = p } q := u.Query() // add user/pass if u.User != nil { q.Set("username", u.User.Username()) if pass, _ := u.User.Password(); pass != "" { q.Set("password", pass) } } // add dbname if dbname != "" { dbname = "/" + dbname } return "tcp://" + host + ":" + port + dbname + genQueryOptions(q), "", nil } // GenMymysql generates a mymysql DSN from the passed URL. func GenMymysql(u *URL) (string, string, error) { host, port, dbname := u.Hostname(), u.Port(), strings.TrimPrefix(u.Path, "/") // resolve path if u.Transport == "unix" { if host == "" { dbname = "/" + dbname } host, dbname = resolveSocket(path.Join(host, dbname)) port = "" } // save host, port, dbname if u.hostPortDB == nil { u.hostPortDB = []string{host, port, dbname} } // if host or proto is not empty if u.Transport != "unix" { if host == "" { host = "localhost" } if port == "" { port = "3306" } } if port != "" { port = ":" + port } // build dsn dsn := u.Transport + ":" + host + port dsn += genOptions( convertOptions(u.Query(), "true", ""), ",", "=", ",", " ", false, nil, nil, ) dsn += "*" + dbname if u.User != nil { pass, _ := u.User.Password() dsn += "/" + u.User.Username() + "/" + pass } else if strings.HasSuffix(dsn, "*") { dsn += "//" } return dsn, "", nil } // GenMysql generates a mysql DSN from the passed URL. func GenMysql(u *URL) (string, string, error) { host, port, dbname := u.Hostname(), u.Port(), strings.TrimPrefix(u.Path, "/") // build dsn var dsn string if u.User != nil { if n := u.User.Username(); n != "" { if p, ok := u.User.Password(); ok { n += ":" + p } dsn += n + "@" } } // resolve path if u.Transport == "unix" { if host == "" { dbname = "/" + dbname } host, dbname = resolveSocket(path.Join(host, dbname)) port = "" } // save host, port, dbname if u.hostPortDB == nil { u.hostPortDB = []string{host, port, dbname} } // if host or proto is not empty if u.Transport != "unix" { if host == "" { host = "localhost" } if port == "" { port = "3306" } } if port != "" { port = ":" + port } // add proto and database dsn += u.Transport + "(" + host + port + ")" + "/" + dbname return dsn + genQueryOptions(u.Query()), "", nil } // GenOdbc generates a odbc DSN from the passed URL. func GenOdbc(u *URL) (string, string, error) { // save host, port, dbname host, port, dbname := u.Hostname(), u.Port(), strings.TrimPrefix(u.Path, "/") if u.hostPortDB == nil { u.hostPortDB = []string{host, port, dbname} } // build q q := u.Query() q.Set("Driver", "{"+strings.Replace(u.Transport, "+", " ", -1)+"}") q.Set("Server", host) if port == "" { proto := strings.ToLower(u.Transport) switch { case strings.Contains(proto, "mysql"): q.Set("Port", "3306") case strings.Contains(proto, "postgres"): q.Set("Port", "5432") case strings.Contains(proto, "db2") || strings.Contains(proto, "ibm"): q.Set("ServiceName", "50000") default: q.Set("Port", "1433") } } else { q.Set("Port", port) } q.Set("Database", dbname) // add user/pass if u.User != nil { q.Set("UID", u.User.Username()) p, _ := u.User.Password() q.Set("PWD", p) } return genOptionsOdbc(q, true, nil, OdbcIgnoreQueryPrefixes), "", nil } // GenOleodbc generates a oleodbc DSN from the passed URL. func GenOleodbc(u *URL) (string, string, error) { props, _, err := GenOdbc(u) if err != nil { return "", "", nil } return `Provider=MSDASQL.1;Extended Properties="` + props + `"`, "", nil } // GenPostgres generates a postgres DSN from the passed URL. func GenPostgres(u *URL) (string, string, error) { host, port, dbname := u.Hostname(), u.Port(), strings.TrimPrefix(u.Path, "/") if host == "." { return "", "", ErrRelativePathNotSupported } // resolve path if u.Transport == "unix" { if host == "" { dbname = "/" + dbname } host, port, dbname = resolveDir(path.Join(host, dbname)) } // build q q := u.Query() q.Set("host", host) q.Set("port", port) q.Set("dbname", dbname) // add user/pass if u.User != nil { q.Set("user", u.User.Username()) pass, _ := u.User.Password() q.Set("password", pass) } // save host, port, dbname if u.hostPortDB == nil { u.hostPortDB = []string{host, port, dbname} } return genOptions(q, "", "=", " ", ",", true, nil, nil), "", nil } // GenPresto generates a presto DSN from the passed URL. func GenPresto(u *URL) (string, string, error) { z := &url.URL{ Scheme: "http", Opaque: u.Opaque, User: u.User, Host: u.Host, RawQuery: u.RawQuery, Fragment: u.Fragment, } // change to https if strings.HasSuffix(u.OriginalScheme, "s") { z.Scheme = "https" } // force user if z.User == nil { z.User = url.User("user") } // force host if z.Host == "" { z.Host = "localhost" } // force port if z.Port() == "" { if z.Scheme == "http" { z.Host += ":8080" } else if z.Scheme == "https" { z.Host += ":8443" } } // add parameters q := z.Query() dbname, schema := strings.TrimPrefix(u.Path, "/"), "" if dbname == "" { dbname = "default" } else if i := strings.Index(dbname, "/"); i != -1 { schema, dbname = dbname[i+1:], dbname[:i] } q.Set("catalog", dbname) if schema != "" { q.Set("schema", schema) } z.RawQuery = q.Encode() return z.String(), "", nil } // GenSnowflake generates a snowflake DSN from the passed URL. func GenSnowflake(u *URL) (string, string, error) { host, port, dbname := u.Hostname(), u.Port(), strings.TrimPrefix(u.Path, "/") if host == "" { return "", "", ErrMissingHost } if port != "" { port = ":" + port } // add user/pass if u.User == nil { return "", "", ErrMissingUser } user := u.User.Username() if pass, _ := u.User.Password(); pass != "" { user += ":" + pass } return user + "@" + host + port + "/" + dbname + genQueryOptions(u.Query()), "", nil } // GenSpanner generates a spanner DSN from the passed URL. func GenSpanner(u *URL) (string, string, error) { project, instance, dbname := u.Hostname(), "", strings.TrimPrefix(u.Path, "/") if project == "" { return "", "", ErrMissingHost } i := strings.Index(dbname, "/") if i == -1 { return "", "", ErrMissingPath } instance, dbname = dbname[:i], dbname[i+1:] if instance == "" || dbname == "" { return "", "", ErrMissingPath } return fmt.Sprintf(`projects/%s/instances/%s/databases/%s`, project, instance, dbname), "", nil } // GenSqlserver generates a sqlserver DSN from the passed URL. func GenSqlserver(u *URL) (string, string, error) { z := &url.URL{ Scheme: "sqlserver", Opaque: u.Opaque, User: u.User, Host: u.Host, Path: u.Path, RawQuery: u.RawQuery, Fragment: u.Fragment, } if z.Host == "" { z.Host = "localhost" } driver := "sqlserver" if strings.Contains(strings.ToLower(u.Scheme), "azuresql") || u.Query().Get("fedauth") != "" { driver = "azuresql" } v := strings.Split(strings.TrimPrefix(z.Path, "/"), "/") if n, q := len(v), z.Query(); !q.Has("database") && n != 0 && len(v[0]) != 0 { q.Set("database", v[n-1]) z.Path, z.RawQuery = "/"+strings.Join(v[:n-1], "/"), q.Encode() } return z.String(), driver, nil } // GenTableStore generates a tablestore DSN from the passed URL. func GenTableStore(u *URL) (string, string, error) { var transport string splits := strings.Split(u.OriginalScheme, "+") if len(splits) == 0 { return "", "", ErrInvalidDatabaseScheme } else if len(splits) == 1 || splits[1] == "https" { transport = "https" } else if splits[1] == "http" { transport = "http" } else { return "", "", ErrInvalidTransportProtocol } z := &url.URL{ Scheme: transport, Opaque: u.Opaque, User: u.User, Host: u.Host, Path: u.Path, RawPath: u.RawPath, RawQuery: u.RawQuery, Fragment: u.Fragment, } return z.String(), "", nil } // GenVoltdb generates a voltdb DSN from the passed URL. func GenVoltdb(u *URL) (string, string, error) { host, port := "localhost", "21212" if h := u.Hostname(); h != "" { host = h } if p := u.Port(); p != "" { port = p } return host + ":" + port, "", nil } // GenYDB generates a ydb dsn from the passed URL. func GenYDB(u *URL) (string, string, error) { scheme, host, port := "grpc", "localhost", "2136" if strings.HasSuffix(strings.ToLower(u.OriginalScheme), "s") { scheme, port = "grpcs", "2135" } if h := u.Hostname(); h != "" { host = h } if p := u.Port(); p != "" { port = p } var userpass string if u.User != nil { userpass = u.User.String() + "@" } s := scheme + "://" + userpass + host + ":" + port + "/" + strings.TrimPrefix(u.Path, "/") return s + genOptions(u.Query(), "?", "=", "&", ",", true, nil, nil), "", nil } // GenDuckDB generates a duckdb dsn from the passed URL. func GenDuckDB(u *URL) (string, string, error) { // Same as GenOpaque but accepts empty path which refers to in-memory DB return u.Opaque + genQueryOptions(u.Query()), "", nil } // convertOptions converts an option value based on name, value pairs. func convertOptions(q url.Values, pairs ...string) url.Values { n := make(url.Values) for k, v := range q { x := make([]string, len(v)) for i, z := range v { for j := 0; j < len(pairs); j += 2 { if pairs[j] == z { z = pairs[j+1] } } x[i] = z } n[k] = x } return n } // genQueryOptions generates standard query options. func genQueryOptions(q url.Values) string { if s := q.Encode(); s != "" { return "?" + s } return "" } // genOptionsOdbc is a util wrapper around genOptions that uses the fixed // settings for ODBC style connection strings. func genOptionsOdbc(q url.Values, skipWhenEmpty bool, ignore, ignorePrefixes []string) string { return genOptions(q, "", "=", ";", ",", skipWhenEmpty, ignore, ignorePrefixes) } // genOptions takes URL values and generates options, joining together with // joiner, and separated by sep, with any multi URL values joined by valSep, // ignoring any values with keys in ignore. // // For example, to build a "ODBC" style connection string, can be used like the // following: // // genOptions(u.Query(), "", "=", ";", ",", false) func genOptions(q url.Values, joiner, assign, sep, valSep string, skipWhenEmpty bool, ignore, ignorePrefixes []string) string { if len(q) == 0 { return "" } // make ignore map ig := make(map[string]bool, len(ignore)) for _, v := range ignore { ig[strings.ToLower(v)] = true } // sort keys s := make([]string, len(q)) var i int for k := range q { s[i] = k i++ } sort.Strings(s) var opts []string for _, k := range s { if s := strings.ToLower(k); !ig[s] && !hasPrefix(s, ignorePrefixes) { val := strings.Join(q[k], valSep) if !skipWhenEmpty || val != "" { if val != "" { val = assign + val } opts = append(opts, k+val) } } } if len(opts) != 0 { return joiner + strings.Join(opts, sep) } return "" } // hasPrefix returns true when s begins with any listed prefix. func hasPrefix(s string, prefixes []string) bool { for _, prefix := range prefixes { if strings.HasPrefix(s, prefix) { return true } } return false } dburl-0.23.7/example_test.go000066400000000000000000000013071500027635200157350ustar00rootroot00000000000000package dburl_test import ( "database/sql" "log" "github.com/xo/dburl" ) func Example() { db, err := dburl.Open("my://user:pass@host:1234/dbname") if err != nil { log.Fatal(err) } res, err := db.Query("SELECT ...") if err != nil { log.Fatal(err) } for res.Next() { /* ... */ } if err := res.Err(); err != nil { log.Fatal(err) } } func Example_parse() { u, err := dburl.Parse("pg://user:pass@host:1234/dbname") if err != nil { log.Fatal(err) } db, err := sql.Open(u.Driver, u.DSN) if err != nil { log.Fatal(err) } res, err := db.Query("SELECT ...") if err != nil { log.Fatal(err) } for res.Next() { /* ... */ } if err := res.Err(); err != nil { log.Fatal(err) } } dburl-0.23.7/go.mod000066400000000000000000000000441500027635200140170ustar00rootroot00000000000000module github.com/xo/dburl go 1.22 dburl-0.23.7/passfile/000077500000000000000000000000001500027635200145215ustar00rootroot00000000000000dburl-0.23.7/passfile/example_test.go000066400000000000000000000013221500027635200175400ustar00rootroot00000000000000package passfile_test import ( "log" "os/user" "github.com/xo/dburl" "github.com/xo/dburl/passfile" ) func Example_entries() { u, err := user.Current() if err != nil { log.Fatal(err) } // read ~/.usqlpass or $ENV{USQLPASS} entries, err := passfile.Entries(u.HomeDir, "usqlpass") if err != nil { log.Fatal(err) } for i, entry := range entries { log.Printf("%d: %v", i, entry) } } func Example_match() { v, err := user.Current() if err != nil { log.Fatal(err) } u, err := dburl.Parse("pg://") if err != nil { log.Fatal(err) } // read ~/.usqlpass or $ENV{USQLPASS} user, err := passfile.Match(u, v.HomeDir, "usqlpass") if err == nil { u.User = user } log.Println("url:", u.String()) } dburl-0.23.7/passfile/passfile.go000066400000000000000000000163571500027635200166720ustar00rootroot00000000000000// Package passfile provides a mechanism for reading database credentials from // passfiles. package passfile import ( "bufio" "database/sql" "fmt" "io" "net/url" "os" "path/filepath" "regexp" "runtime" "slices" "strings" "github.com/xo/dburl" ) // Entry is a passfile entry. // // Corresponds to a non-empty line in a passfile. type Entry struct { Protocol, Host, Port, DBName, Username, Password string } // NewEntry creates a new passfile entry. func NewEntry(v []string) Entry { // make sure there's always at least 6 elements v = append(v, "", "", "", "", "", "") return Entry{ Protocol: v[0], Host: v[1], Port: v[2], DBName: v[3], Username: v[4], Password: v[5], } } // Parse parses passfile entries from the reader. func Parse(r io.Reader) ([]Entry, error) { var entries []Entry i, s := 0, bufio.NewScanner(r) for s.Scan() { i++ // grab next line line := strings.TrimSpace(commentRE.ReplaceAllString(s.Text(), "")) if line == "" { continue } // split and check length v := strings.Split(line, ":") if len(v) != 6 { return nil, &ErrInvalidEntry{i} } // make sure no blank entries exist for j := range v { if v[j] == "" { return nil, &ErrEmptyField{i, j} } } entries = append(entries, NewEntry(v)) } return entries, nil } // commentRE matches comment entries in a passfile. var commentRE = regexp.MustCompile(`#.*`) // ParseFile parses passfile entries contained in file. func ParseFile(file string) ([]Entry, error) { fi, err := os.Stat(file) switch { case err != nil && os.IsNotExist(err): return nil, nil case err != nil: return nil, &FileError{file, err} case fi.IsDir(): // ensure not a directory return nil, &FileError{file, ErrMustNotBeDirectory} case runtime.GOOS != "windows" && fi.Mode()&0x3f != 0: // ensure not group/world readable/writable/executable return nil, &FileError{file, ErrHasGroupOrWorldAccess} } // open f, err := os.OpenFile(file, os.O_RDONLY, 0) if err != nil { return nil, &FileError{file, err} } // parse entries, err := Parse(f) if err != nil { defer f.Close() return nil, &FileError{file, err} } if err := f.Close(); err != nil { return nil, &FileError{file, err} } return entries, nil } // Equals returns true when v matches the entry. func (entry Entry) Equals(v Entry, protocols ...string) bool { return (entry.Protocol == "*" || slices.Contains(protocols, entry.Protocol)) && (entry.Host == "*" || entry.Host == v.Host) && (entry.Port == "*" || entry.Port == v.Port) } // MatchEntries returns a Userinfo when the normalized v is found in entries. func MatchEntries(u *dburl.URL, entries []Entry, protocols ...string) (*url.Userinfo, error) { // check if v already has password defined ... var username string if u.User != nil { username = u.User.Username() if _, ok := u.User.Password(); ok { return nil, nil } } // find matching entry n := strings.SplitN(u.Normalize(":", "", 3), ":", 6) if len(n) < 3 { return nil, ErrUnableToNormalizeURL } m := NewEntry(n) for _, entry := range entries { if entry.Equals(m, protocols...) { u := entry.Username if entry.Username == "*" { u = username } return url.UserPassword(u, entry.Password), nil } } return nil, nil } // MatchFile returns a Userinfo from a passfile entry matching database URL v // read from the specified file. func MatchFile(u *dburl.URL, file string, protocols ...string) (*url.Userinfo, error) { entries, err := ParseFile(file) if err != nil { return nil, &FileError{file, err} } if entries == nil { return nil, nil } user, err := MatchEntries(u, entries, protocols...) if err != nil { return nil, &FileError{file, err} } return user, nil } // Match returns a Userinfo from a passfile entry matching database URL read // from the file in $HOME/. or $ENV{NAME}. // // Equivalent to MatchFile(u, Path(homeDir, name), dburl.Protocols(u.Driver)...). func Match(u *dburl.URL, homeDir, name string) (*url.Userinfo, error) { return MatchFile(u, Path(homeDir, name), dburl.Protocols(u.Driver)...) } // MatchProtocols returns a Userinfo from a passfile entry matching database // URL read from the file in $HOME/. or $ENV{NAME} using the specified // protocols. // // Equivalent to MatchFile(u, Path(homeDir, name), protocols...). func MatchProtocols(u *dburl.URL, homeDir, name string, protocols ...string) (*url.Userinfo, error) { return MatchFile(u, Path(homeDir, name), protocols...) } // Entries returns the entries for the specified passfile name. // // Equivalent to ParseFile(Path(homeDir, name)). func Entries(homeDir, name string) ([]Entry, error) { return ParseFile(Path(homeDir, name)) } // Path returns the expanded path to the password file for name. // // Uses $HOME/., overridden by environment variable $ENV{NAME} (for // example, ~/.usqlpass and $ENV{USQLPASS}). func Path(homeDir, name string) string { file := "~/." + strings.ToLower(name) if s := os.Getenv(strings.ToUpper(name)); s != "" { file = s } return Expand(homeDir, file) } // Expand expands the beginning tilde (~) in a file name to the provided home // directory. func Expand(homeDir string, file string) string { switch { case file == "~": return homeDir case strings.HasPrefix(file, "~/"): return filepath.Join(homeDir, strings.TrimPrefix(file, "~/")) } return file } // OpenURL opens a database connection for the provided URL, reading the named // passfile in the home directory. func OpenURL(u *dburl.URL, homeDir, name string) (*sql.DB, error) { if u.User != nil { return sql.Open(u.Driver, u.DSN) } user, err := Match(u, homeDir, name) if err != nil { return sql.Open(u.Driver, u.DSN) } u.User = user v, _ := dburl.Parse(u.String()) *u = *v return sql.Open(v.Driver, v.DSN) } // Open opens a database connection for a URL, reading the named passfile in // the home directory. func Open(urlstr, homeDir, name string) (*sql.DB, error) { u, err := dburl.Parse(urlstr) if err != nil { return nil, err } return OpenURL(u, homeDir, name) } // Error is a error. type Error string // Error satisfies the error interface. func (err Error) Error() string { return string(err) } const ( // ErrUnableToNormalizeURL is the unable to normalize URL error. ErrUnableToNormalizeURL Error = "unable to normalize URL" // ErrMustNotBeDirectory is the must not be directory error. ErrMustNotBeDirectory Error = "must not be directory" // ErrHasGroupOrWorldAccess is the has group or world access error. ErrHasGroupOrWorldAccess Error = "has group or world access" ) // FileError is a file error. type FileError struct { File string Err error } // Error satisfies the error interface. func (err *FileError) Error() string { return fmt.Sprintf("passfile %q: %v", err.File, err.Err) } // Unwrap satisfies the unwrap interface. func (err *FileError) Unwrap() error { return err.Err } // ErrInvalidEntry is the invalid entry error. type ErrInvalidEntry struct { Line int } // Error satisfies the error interface. func (err *ErrInvalidEntry) Error() string { return fmt.Sprintf("invalid entry at line %d", err.Line) } // ErrEmptyField is the empty field error. type ErrEmptyField struct { Line int Field int } // Error satisfies the error interface. func (err *ErrEmptyField) Error() string { return fmt.Sprintf("line %d has empty field %d", err.Line, err.Field) } dburl-0.23.7/passfile/passfile_test.go000066400000000000000000000025361500027635200177230ustar00rootroot00000000000000package passfile import ( "reflect" "strings" "testing" ) func TestParse(t *testing.T) { entries, err := Parse(strings.NewReader(passfile)) if err != nil { t.Fatalf("expected no error, got: %v", err) } if len(entries) != 10 { t.Fatalf("entries should have exactly 10 entries, got: %d", len(entries)) } exp := []Entry{ {"postgres", "*", "*", "*", "postgres", "P4ssw0rd"}, {"cql", "*", "*", "*", "cassandra", "cassandra"}, {"godror", "*", "*", "*", "system", "P4ssw0rd"}, {"ignite", "*", "*", "*", "ignite", "ignite"}, {"mymysql", "*", "*", "*", "root", "P4ssw0rd"}, {"mysql", "*", "*", "*", "root", "P4ssw0rd"}, {"oracle", "*", "*", "*", "system", "P4ssw0rd"}, {"pgx", "*", "*", "*", "postgres", "P4ssw0rd"}, {"sqlserver", "*", "*", "*", "sa", "Adm1nP@ssw0rd"}, {"vertica", "*", "*", "*", "dbadmin", "P4ssw0rd"}, } if !reflect.DeepEqual(entries, exp) { t.Errorf("entries does not equal expected:\nexp:%#v\n---\ngot:%#v", exp, entries) } } const passfile = `# sample ~/.usqlpass file # # format is: # protocol:host:port:dbname:user:pass postgres:*:*:*:postgres:P4ssw0rd cql:*:*:*:cassandra:cassandra godror:*:*:*:system:P4ssw0rd ignite:*:*:*:ignite:ignite mymysql:*:*:*:root:P4ssw0rd mysql:*:*:*:root:P4ssw0rd oracle:*:*:*:system:P4ssw0rd pgx:*:*:*:postgres:P4ssw0rd sqlserver:*:*:*:sa:Adm1nP@ssw0rd vertica:*:*:*:dbadmin:P4ssw0rd ` dburl-0.23.7/scheme.go000066400000000000000000000273351500027635200145200ustar00rootroot00000000000000package dburl import ( "bytes" "fmt" "regexp" "slices" "sort" ) // Transport is the allowed transport protocol types in a database [URL] scheme. type Transport uint // Transport types. const ( TransportNone Transport = 0 TransportTCP Transport = 1 TransportUDP Transport = 2 TransportUnix Transport = 4 TransportAny Transport = 8 ) // Scheme wraps information used for registering a database URL scheme for use // with [Parse]/[Open]. type Scheme struct { // Driver is the name of the SQL driver that is set as the Scheme in // Parse'd URLs and is the driver name expected by the standard sql.Open // calls. // // Note: a 2 letter alias will always be registered for the Driver as the // first 2 characters of the Driver, unless one of the Aliases includes an // alias that is 2 characters. Driver string // Generator is the func responsible for generating a DSN based on parsed // URL information. // // Note: this func should not modify the passed URL. Generator func(*URL) (string, string, error) // Transport are allowed protocol transport types for the scheme. Transport Transport // Opaque toggles Parse to not re-process URLs with an "opaque" component. Opaque bool // Aliases are any additional aliases for the scheme. Aliases []string // Override is the Go SQL driver to use instead of Driver. // // Used for "wire compatible" driver schemes. Override string } // BaseSchemes returns the supported base schemes. func BaseSchemes() []Scheme { return []Scheme{ { "file", GenOpaque, 0, true, []string{"file"}, "", }, // core databases { "mysql", GenMysql, TransportTCP | TransportUDP | TransportUnix, false, []string{"mariadb", "maria", "percona", "aurora"}, "", }, { "oracle", GenFromURL("oracle://localhost:1521"), 0, false, []string{"ora", "oci", "oci8", "odpi", "odpi-c"}, "", }, { "postgres", GenPostgres, TransportUnix, false, []string{"pg", "postgresql", "pgsql"}, "", }, { "sqlite3", GenOpaque, 0, true, []string{"sqlite"}, "", }, { "sqlserver", GenSqlserver, 0, false, []string{"ms", "mssql", "azuresql"}, "", }, // wire compatibles { "cockroachdb", GenFromURL("postgres://localhost:26257/?sslmode=disable"), 0, false, []string{"cr", "cockroach", "crdb", "cdb"}, "postgres", }, { "memsql", GenMysql, 0, false, nil, "mysql", }, { "redshift", GenFromURL("postgres://localhost:5439/"), 0, false, []string{"rs"}, "postgres", }, { "tidb", GenMysql, 0, false, nil, "mysql", }, { "vitess", GenMysql, 0, false, []string{"vt"}, "mysql", }, // alternate implementations { "godror", GenGodror, 0, false, []string{"gr"}, "", }, { "moderncsqlite", GenOpaque, 0, true, []string{"mq", "modernsqlite"}, "", }, { "mymysql", GenMymysql, TransportTCP | TransportUDP | TransportUnix, false, []string{"zm", "mymy"}, "", }, { "pgx", GenFromURL("postgres://localhost:5432/"), TransportUnix, false, []string{"px"}, "", }, // other databases { "adodb", GenAdodb, 0, false, []string{"ado"}, "", }, { "awsathena", GenScheme("s3"), 0, false, []string{"s3", "aws", "athena"}, "", }, { "avatica", GenFromURL("http://localhost:8765/"), 0, false, []string{"phoenix"}, "", }, { "bigquery", GenScheme("bigquery"), 0, false, []string{"bq"}, "", }, { "clickhouse", GenClickhouse, TransportAny, false, []string{"ch"}, "", }, { "cosmos", GenCosmos, 0, false, []string{"cm", "gocosmos"}, "", }, { "cql", GenCassandra, 0, false, []string{"ca", "cassandra", "datastax", "scy", "scylla"}, "", }, { "csvq", GenOpaque, 0, true, []string{"csv", "tsv", "json"}, "", }, { "databend", GenDatabend, 0, false, []string{"dd", "bend"}, "", }, { "databricks", GenDatabricks, 0, false, []string{"br", "brick", "bricks", "databrick"}, "", }, { "duckdb", GenDuckDB, 0, true, []string{"dk", "ddb", "duck"}, "", }, { "godynamo", GenDynamo, 0, false, []string{"dy", "dyn", "dynamo", "dynamodb"}, "", }, { "exasol", GenExasol, 0, false, []string{"ex", "exa"}, "", }, { "firebirdsql", GenFirebird, 0, false, []string{"fb", "firebird"}, "", }, { "flightsql", GenScheme("flightsql"), 0, false, []string{"fl", "flight"}, "", }, { "chai", GenOpaque, 0, true, []string{"ci", "chaisql", "genji"}, "", }, { "h2", GenFromURL("h2://localhost:9092/"), 0, false, nil, "", }, { "hdb", GenScheme("hdb"), 0, false, []string{"sa", "saphana", "sap", "hana"}, "", }, { "hive", GenFromURL("truncate://localhost:10000/"), 0, false, []string{"hive2"}, "", }, { "ignite", GenIgnite, 0, false, []string{"ig", "gridgain"}, "", }, { "impala", GenScheme("impala"), 0, false, nil, "", }, { "maxcompute", GenFromURL("truncate://localhost/"), 0, false, []string{"mc"}, "", }, { "n1ql", GenFromURL("http://localhost:8093/"), 0, false, []string{"couchbase"}, "", }, { "nzgo", GenPostgres, TransportUnix, false, []string{"nz", "netezza"}, "", }, { "odbc", GenOdbc, TransportAny, false, nil, "", }, { "oleodbc", GenOleodbc, TransportAny, false, []string{"oo", "ole"}, "adodb", }, { "ots", GenTableStore, TransportAny, false, []string{"tablestore"}, "", }, { "presto", GenPresto, 0, false, []string{"prestodb", "prestos", "prs", "prestodbs"}, "", }, { "ql", GenOpaque, 0, true, []string{"ql", "cznic", "cznicql"}, "", }, { "ramsql", GenFromURL("truncate://ramsql"), 0, false, []string{"rm", "ram"}, "", }, { "snowflake", GenSnowflake, 0, false, []string{"sf"}, "", }, { "spanner", GenSpanner, 0, false, []string{"sp"}, "", }, { "tds", GenFromURL("http://localhost:5000/"), 0, false, []string{"ax", "ase", "sapase"}, "", }, { "trino", GenPresto, 0, false, []string{"trino", "trinos", "trs"}, "", }, { "vertica", GenFromURL("vertica://localhost:5433/"), 0, false, nil, "", }, { "voltdb", GenVoltdb, 0, false, []string{"volt", "vdb"}, "", }, { "ydb", GenYDB, 0, false, []string{"yd", "yds", "ydbs"}, "", }, } } func init() { // register schemes schemes := BaseSchemes() schemeMap = make(map[string]*Scheme, len(schemes)) for _, scheme := range schemes { Register(scheme) } RegisterFileType("duckdb", isDuckdbHeader, `(?i)\.duckdb$`) RegisterFileType("sqlite3", isSqlite3Header, `(?i)\.(db|sqlite|sqlite3)$`) } // schemeMap is the map of registered schemes. var schemeMap map[string]*Scheme // registerAlias registers a alias for an already registered Scheme. func registerAlias(name, alias string, doSort bool) { scheme, ok := schemeMap[name] if !ok { panic(fmt.Sprintf("scheme %s not registered", name)) } if doSort && slices.Contains(scheme.Aliases, alias) { panic(fmt.Sprintf("scheme %s already has alias %s", name, alias)) } if _, ok := schemeMap[alias]; ok { panic(fmt.Sprintf("scheme %s already registered", alias)) } scheme.Aliases = append(scheme.Aliases, alias) if doSort { sort.Slice(scheme.Aliases, func(i, j int) bool { if len(scheme.Aliases[i]) <= len(scheme.Aliases[j]) { return true } if len(scheme.Aliases[j]) < len(scheme.Aliases[i]) { return false } return scheme.Aliases[i] < scheme.Aliases[j] }) } schemeMap[alias] = scheme } // Register registers a [Scheme]. func Register(scheme Scheme) { if scheme.Generator == nil { panic("must specify Generator when registering Scheme") } if scheme.Opaque && scheme.Transport&TransportUnix != 0 { panic("scheme must support only Opaque or Unix protocols, not both") } // check if registered if _, ok := schemeMap[scheme.Driver]; ok { panic(fmt.Sprintf("scheme %s already registered", scheme.Driver)) } sz := &Scheme{ Driver: scheme.Driver, Generator: scheme.Generator, Transport: scheme.Transport, Opaque: scheme.Opaque, Override: scheme.Override, } schemeMap[scheme.Driver] = sz // add aliases var hasShort bool for _, alias := range scheme.Aliases { if len(alias) == 2 { hasShort = true } if scheme.Driver != alias { registerAlias(scheme.Driver, alias, false) } } if !hasShort && len(scheme.Driver) > 2 { registerAlias(scheme.Driver, scheme.Driver[:2], false) } // ensure always at least one alias, and that if Driver is 2 characters, // that it gets added as well if len(sz.Aliases) == 0 || len(scheme.Driver) == 2 { sz.Aliases = append(sz.Aliases, scheme.Driver) } // sort sort.Slice(sz.Aliases, func(i, j int) bool { if len(sz.Aliases[i]) <= len(sz.Aliases[j]) { return true } if len(sz.Aliases[j]) < len(sz.Aliases[i]) { return false } return sz.Aliases[i] < sz.Aliases[j] }) } // Unregister unregisters a scheme and all associated aliases, returning the // removed [Scheme]. func Unregister(name string) *Scheme { if scheme, ok := schemeMap[name]; ok { for _, alias := range scheme.Aliases { delete(schemeMap, alias) } delete(schemeMap, name) return scheme } return nil } // RegisterAlias registers an additional alias for a registered scheme. func RegisterAlias(name, alias string) { registerAlias(name, alias, true) } // fileTypes are registered header recognition funcs. var fileTypes []fileType // RegisterFileType registers a file header recognition func, and extension regexp. func RegisterFileType(driver string, f func([]byte) bool, ext string) { extRE, err := regexp.Compile(ext) if err != nil { panic(fmt.Sprintf("invalid extension regexp %q: %v", ext, err)) } fileTypes = append(fileTypes, fileType{ driver: driver, f: f, ext: extRE, }) } // fileType wraps file type information. type fileType struct { driver string f func([]byte) bool ext *regexp.Regexp } // FileTypes returns the registered file types. func FileTypes() []string { var v []string for _, typ := range fileTypes { v = append(v, typ.driver) } return v } // Protocols returns list of all valid protocol aliases for a registered // [Scheme] name. func Protocols(name string) []string { if scheme, ok := schemeMap[name]; ok { return append([]string{scheme.Driver}, scheme.Aliases...) } return nil } // SchemeDriverAndAliases returns the registered driver and aliases for a // database scheme. func SchemeDriverAndAliases(name string) (string, []string) { if scheme, ok := schemeMap[name]; ok { driver := scheme.Driver if scheme.Override != "" { driver = scheme.Override } var aliases []string for _, alias := range scheme.Aliases { if alias == driver { continue } aliases = append(aliases, alias) } sort.Slice(aliases, func(i, j int) bool { if len(aliases[i]) <= len(aliases[j]) { return true } if len(aliases[j]) < len(aliases[i]) { return false } return aliases[i] < aliases[j] }) return driver, aliases } return "", nil } // ShortAlias returns the short alias for the scheme name. func ShortAlias(name string) string { if scheme, ok := schemeMap[name]; ok { return scheme.Aliases[0] } return "" } // isSqlite3Header returns true when the passed header is empty or starts with // the SQLite3 header. // // See: https://www.sqlite.org/fileformat.html func isSqlite3Header(buf []byte) bool { return bytes.HasPrefix(buf, sqlite3Header) } // sqlite3Header is the sqlite3 header. var sqlite3Header = []byte("SQLite format 3\000") // isDuckdbHeader returns true when the passed header is a DuckDB header. // // See: https://duckdb.org/internals/storage func isDuckdbHeader(buf []byte) bool { return duckdbRE.Match(buf) } // duckdbRE is the duckdb storage header regexp. var duckdbRE = regexp.MustCompile(`^.{8}DUCK.{8}`) dburl-0.23.7/testdata/000077500000000000000000000000001500027635200145245ustar00rootroot00000000000000dburl-0.23.7/testdata/test.duckdb000066400000000000000000000300001500027635200166520ustar00rootroot00000000000000g'MLhqÃDUCK@v0.9.1401c806ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿdburl-0.23.7/testdata/test.duckdb.wal000066400000000000000000000000001500027635200174310ustar00rootroot00000000000000dburl-0.23.7/testdata/test.sqlite3000066400000000000000000000200001500027635200170010ustar00rootroot00000000000000SQLite format 3@ .jÐ ÞÞ 1tableaaCREATE TABLE a (a)