├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── build_oracle_docker_image.sh ├── dialect.go ├── dialect_mssql.go ├── dialect_mysql.go ├── dialect_oracle.go ├── dialect_postgres.go ├── dialect_sqlite.go ├── doc.go ├── docker-compose.yml ├── docker-db-init-mssql.sh ├── docker-db-init-mssql.sql ├── docker-db-init-oracle.sql ├── docker-entrypoint-mssql.sh ├── escape_test.go ├── go.mod ├── go.sum ├── schema.go ├── schema_mssql_test.go ├── schema_mysql_test.go ├── schema_oracle_test.go ├── schema_postgres_test.go ├── schema_sqlite_test.go ├── schema_suite_test.go └── schema_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | TODO 2 | coverage.out 3 | *.sublime-project 4 | *.sublime-workspace 5 | *.code-workspace 6 | 7 | /test 8 | /test/docker-oracle -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - "1.16.x" 4 | 5 | sudo: required 6 | 7 | services: 8 | - docker 9 | 10 | env: 11 | DOCKER_COMPOSE_VERSION=1.29.0 GO111MODULE=on 12 | # See https://arslan.io/2018/08/26/using-go-modules-with-vendor-support-on-travis-ci/ 13 | 14 | before_install: 15 | # Update docker-compose. 16 | - sudo rm /usr/local/bin/docker-compose 17 | - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose 18 | - chmod +x docker-compose 19 | - sudo mv docker-compose /usr/local/bin 20 | # Go get our deps. 21 | - go get -v ./... 22 | 23 | before_script: 24 | # Start everything except oracle servvice. 25 | - docker-compose up -d mssql mysql postgres 26 | 27 | script: 28 | - go test -race -coverprofile=coverage.txt -covermode=atomic -tags=travis 29 | 30 | after_script: 31 | - docker-compose down -v 32 | 33 | after_success: 34 | - bash <(curl -s https://codecov.io/bash) 35 | 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # BSD 3-Clause License 2 | Copyright © 2018, Jim Smart. 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | 1\. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | 9 | 2\. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | 11 | 3\. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # schema 2 | 3 | [![BSD3](https://img.shields.io/badge/license-BSD3-blue.svg?style=flat)](LICENSE.md) 4 | [![Build Status](https://img.shields.io/travis/jimsmart/schema/master.svg?style=flat)](https://travis-ci.org/jimsmart/schema) 5 | [![codecov](https://codecov.io/gh/jimsmart/schema/branch/master/graph/badge.svg)](https://codecov.io/gh/jimsmart/schema) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/jimsmart/schema)](https://goreportcard.com/report/github.com/jimsmart/schema) 7 | [![Used By](https://img.shields.io/sourcegraph/rrc/github.com/jimsmart/schema.svg)](https://sourcegraph.com/github.com/jimsmart/schema) 8 | [![Godoc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/jimsmart/schema) 9 | 10 | schema is a [Go](https://golang.org) package providing access to database schema metadata, for database/sql drivers. 11 | 12 | TODO more docs 13 | 14 | Currently supporting the following database engines / SQL dialects: 15 | 16 | - Microsoft SQL Server 17 | - MySQL 18 | - Oracle 19 | - Postgres 20 | - Snowflake 21 | - SQLite 22 | - Vitess 23 | 24 | For a list of supported drivers, and their capabilities with regards to sql.ColumnType support, see [drivercaps](https://github.com/jimsmart/drivercaps) 25 | 26 | ## Installation 27 | 28 | ```bash 29 | go get github.com/jimsmart/schema 30 | ``` 31 | 32 | ```go 33 | import "github.com/jimsmart/schema" 34 | ``` 35 | 36 | ### Dependencies 37 | 38 | - A [supported](https://github.com/jimsmart/drivercaps) database driver. 39 | - Standard library. 40 | - [Ginkgo](https://onsi.github.io/ginkgo/) and [Gomega](https://onsi.github.io/gomega/) are used in the tests. 41 | - Tests also require [Docker Compose](https://docs.docker.com/compose/install/) to be installed. 42 | 43 | ## Example 44 | 45 | See GoDocs for usage examples. 46 | 47 | ## Documentation 48 | 49 | GoDocs [https://godoc.org/github.com/jimsmart/schema](https://godoc.org/github.com/jimsmart/schema) 50 | 51 | ## Testing 52 | 53 | Database services for testing against are hosted in Docker. 54 | 55 | To bring up the database services: execute `docker-compose up` inside the project folder, and wait until all of the Docker services have completed their startup (i.e. there is no further output in the terminal), then open a second terminal. (In future one may choose to use `docker-compose up -d` instead) 56 | 57 | To run the tests execute `go test` inside the project folder. 58 | 59 | For a full coverage report, try: 60 | 61 | ```bash 62 | go test -coverprofile=coverage.out && go tool cover -html=coverage.out 63 | ``` 64 | 65 | To shutdown the Docker services, execute `docker-compose down -v` inside the project folder. 66 | 67 | ### Oracle Setup Checklist 68 | 69 | #### Build Docker Image 70 | 71 | Build a Docker image for Oracle, by executing script: 72 | 73 | ```bash 74 | ./build_oracle_docker_image.sh 75 | ``` 76 | 77 | Once the script has successfully completed, you are free to delete the folder used when creating the image: 78 | 79 | ```bash 80 | rm -rf ./test/docker-oracle 81 | ``` 82 | 83 | #### Increase Docker's RAM limits 84 | 85 | By default, Docker allocates 2gb RAM to each container. To prevent out-of-memory errors when running Oracle, increase Docker's RAM limits: 86 | 87 | Docker -> Preferences -> Resources -> Advanced -> Memory, change to 4gb, click Apply & Restart. 88 | 89 | #### Install Oracle Instant Client 90 | 91 | Oracle database/sql drivers require dynamic libraries that are part of the [Oracle Instant Client](https://www.oracle.com/uk/database/technologies/instant-client.html) installation. 92 | 93 | ##### Mac 94 | 95 | ```bash 96 | brew tap InstantClientTap/instantclient 97 | brew install instantclient-basic 98 | ``` 99 | 100 | ## License 101 | 102 | Package schema is copyright 2018-2021 by Jim Smart and released under the [BSD 3-Clause License](LICENSE.md). 103 | 104 | ## History 105 | 106 | - v0.2.1: Added dialect alias for Vitess driver github.com/vitessio/vitess. 107 | - v0.2.0: Replaced Table and View methods with ColumnTypes method. 108 | - v0.1.0: Added schema name to methods and results. 109 | - v0.0.8: Disabled Oracle tests on Travis. 110 | - v0.0.7: Added PrimaryKey method. TableNames and ViewNames are now sorted. Improved Oracle testing. Refactored dialect handling. 111 | - v0.0.6: Fix Oracle quoting strategy. Added support for driver github.com/godror/godror. 112 | - v0.0.5: Added dialect alias for Snowflake driver github.com/snowflakedb/gosnowflake. 113 | - v0.0.4: Improved error handling for unknown DB driver types. Test environment now uses Docker. 114 | - v0.0.3: Minor code cleanups. 115 | - v0.0.2: Added identifier escaping for methods that query sql.ColumnType. 116 | - v0.0.1: Started using Go modules. 117 | - 2019-11-04: Fix for renamed driver struct in github.com/mattn/go-oci8 (Oracle) 118 | - 2019-11-04: Fix for renamed driver struct in github.com/denisenkom/go-mssqldb (MSSQL) 119 | -------------------------------------------------------------------------------- /build_oracle_docker_image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Downloads Oracle XE 18c, clones the official image scripts, 4 | # and builds image 'oracle/database:18.4.0-xe' 5 | 6 | # Based upon https://www.petefreitag.com/item/886.cfm 7 | 8 | mkdir -p ./test/docker-oracle 9 | cd ./test/docker-oracle 10 | 11 | git clone https://github.com/oracle/docker-images.git 12 | 13 | cd ./docker-images/OracleDatabase/SingleInstance/dockerfiles 14 | 15 | curl -L https://download.oracle.com/otn-pub/otn_software/db-express/oracle-database-xe-18c-1.0-1.x86_64.rpm --output ./18.4.0/oracle-database-xe-18c-1.0-1.x86_64.rpm 16 | 17 | ./buildContainerImage.sh -x -v 18.4.0 18 | 19 | echo "\nDocker image built ok" 20 | echo "\nIt is safe to remove the work folder with: rm -rf ./test/docker-oracle" 21 | -------------------------------------------------------------------------------- /dialect.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "database/sql" 5 | "strings" 6 | ) 7 | 8 | type dialect interface { 9 | escapeIdent(ident string) string 10 | 11 | ColumnTypes(db *sql.DB, schema, name string) ([]*sql.ColumnType, error) 12 | PrimaryKey(db *sql.DB, schema, name string) ([]string, error) 13 | TableNames(db *sql.DB) ([][2]string, error) 14 | ViewNames(db *sql.DB) ([][2]string, error) 15 | } 16 | 17 | // driverDialect is a registry, mapping database/sql driver names to database dialects. 18 | // This is somewhat fragile. 19 | var driverDialect = map[string]dialect{ 20 | "*sqlite3.SQLiteDriver": sqliteDialect{}, // github.com/mattn/go-sqlite3 21 | "*sqlite.impl": sqliteDialect{}, // github.com/gwenn/gosqlite 22 | "sqlite3.Driver": sqliteDialect{}, // github.com/mxk/go-sqlite 23 | "*pq.Driver": postgresDialect{}, // github.com/lib/pq 24 | "*stdlib.Driver": postgresDialect{}, // github.com/jackc/pgx 25 | "*pgsqldriver.postgresDriver": postgresDialect{}, // github.com/jbarham/gopgsqldriver 26 | "*gosnowflake.SnowflakeDriver": postgresDialect{}, // github.com/snowflakedb/gosnowflake 27 | "*mysql.MySQLDriver": mysqlDialect{}, // github.com/go-sql-driver/mysql 28 | "*godrv.Driver": mysqlDialect{}, // github.com/ziutek/mymysql 29 | "vitessdriver.drv": mysqlDialect{}, // github.com/vitessio/vitess 30 | "*mssql.Driver": mssqlDialect{}, // github.com/denisenkom/go-mssqldb 31 | "*mssql.MssqlDriver": mssqlDialect{}, // github.com/denisenkom/go-mssqldb 32 | "*freetds.MssqlDriver": mssqlDialect{}, // github.com/minus5/gofreetds 33 | "*goracle.drv": oracleDialect{}, // gopkg.in/goracle.v2 34 | "*godror.drv": oracleDialect{}, // github.com/godror/godror 35 | "*ora.Drv": oracleDialect{}, // gopkg.in/rana/ora.v4 36 | "*oci8.OCI8DriverStruct": oracleDialect{}, // github.com/mattn/go-oci8 37 | "*oci8.OCI8Driver": oracleDialect{}, // github.com/mattn/go-oci8 38 | } 39 | 40 | // TODO Should we expose a method of registering a driver string/dialect in our registry? 41 | // -- It would allow folk to work around the fragility. e.g. 42 | // 43 | // func Register(driver sql.Driver, d *Dialect) {} 44 | // 45 | 46 | // // pack a string, normalising its whitespace. 47 | // func pack(s string) string { 48 | // return strings.Join(strings.Fields(s), " ") 49 | // } 50 | 51 | // escapeWithDoubleQuotes implements double-quote escaping of a string, 52 | // in accordance with SQL:1999 standard. 53 | func escapeWithDoubleQuotes(s string) string { 54 | return escape(s, '"', '"') 55 | } 56 | 57 | // escapeWithBackticks implements backtick escaping of a string. 58 | func escapeWithBackticks(s string) string { 59 | return escape(s, '`', '`') 60 | } 61 | 62 | // escapeWithBrackets implements bracket escaping of a string. 63 | func escapeWithBrackets(s string) string { 64 | return escape(s, '[', ']') 65 | } 66 | 67 | // escapeWithBraces implements brace escaping of a string. 68 | func escapeWithBraces(s string) string { 69 | return escape(s, '{', '}') 70 | } 71 | 72 | // escape escapes a string identifier. 73 | func escape(s string, escBegin, escEnd byte) string { 74 | // It would be nice to know when not to escape, 75 | // but a regex (e.g. "^[a-zA-Z_][a-zA-Z0-9_#@$]*$") 76 | // doesn't solve this, because it would not catch keywords. 77 | // Which is why we simply always escape identifiers. 78 | 79 | // TODO(js) Correct handling of backslash escaping of identifiers needs 80 | // further investigation: different dialects look to handle it differently 81 | // - removed for now. 82 | // Please file an issue if you encounter a problem regarding backslash escaping. 83 | 84 | var b strings.Builder 85 | b.WriteByte(escBegin) 86 | for i := 0; i < len(s); i++ { 87 | c := s[i] 88 | b.WriteByte(c) 89 | if c == escEnd { // || c == '\\' { 90 | b.WriteByte(c) 91 | } 92 | } 93 | b.WriteByte(escEnd) 94 | return b.String() 95 | } 96 | -------------------------------------------------------------------------------- /dialect_mssql.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "database/sql" 5 | ) 6 | 7 | const mssqlAllColumns = `SELECT * FROM %s WHERE 1=0` 8 | 9 | // TODO(js) Should we be filtering out system tables, like we currently do? 10 | 11 | // See https://stackoverflow.com/questions/8774928/how-to-exclude-system-table-when-querying-sys-tables 12 | 13 | const mssqlTableNamesWithSchema = ` 14 | SELECT 15 | schema_name(t.schema_id), 16 | t.name 17 | FROM 18 | sys.tables t 19 | INNER JOIN 20 | sys.schemas s 21 | ON s.schema_id = t.schema_id 22 | LEFT JOIN 23 | sys.extended_properties ep 24 | ON ep.major_id = t.[object_id] 25 | WHERE 26 | t.is_ms_shipped = 0 AND 27 | (ep.class_desc IS NULL OR (ep.class_desc <> 'OBJECT_OR_COLUMN' AND 28 | ep.[name] <> 'microsoft_database_tools_support')) 29 | ORDER BY 30 | schema_name(t.schema_id), 31 | t.name 32 | ` 33 | 34 | const mssqlViewNamesWithSchema = ` 35 | SELECT 36 | schema_name(t.schema_id), 37 | t.name 38 | FROM 39 | sys.views t 40 | INNER JOIN 41 | sys.schemas s 42 | ON s.schema_id = t.schema_id 43 | LEFT JOIN 44 | sys.extended_properties ep 45 | ON ep.major_id = t.[object_id] 46 | WHERE 47 | t.is_ms_shipped = 0 AND 48 | (ep.class_desc IS NULL OR (ep.class_desc <> 'OBJECT_OR_COLUMN' AND 49 | ep.[name] <> 'microsoft_database_tools_support')) 50 | ORDER BY 51 | schema_name(t.schema_id), 52 | t.name 53 | ` 54 | 55 | const mssqlPrimaryKey = ` 56 | SELECT 57 | tc.name 58 | FROM 59 | sys.schemas s 60 | INNER JOIN 61 | sys.tables t 62 | ON s.schema_id = t.schema_id 63 | INNER JOIN 64 | sys.indexes i 65 | ON t.object_id = i.object_id 66 | INNER JOIN 67 | sys.index_columns ic 68 | ON i.object_id = ic.object_id AND 69 | i.index_id = ic.index_id 70 | INNER JOIN 71 | sys.columns tc 72 | ON ic.object_id = tc.object_id AND 73 | ic.column_id = tc.column_id 74 | WHERE 75 | i.is_primary_key = 1 AND 76 | s.schema_id = SCHEMA_ID() AND 77 | t.name = ? 78 | ORDER BY 79 | ic.key_ordinal 80 | ` 81 | 82 | const mssqlPrimaryKeyWithSchema = ` 83 | SELECT 84 | tc.name 85 | FROM 86 | sys.schemas s 87 | INNER JOIN 88 | sys.tables t 89 | ON s.schema_id = t.schema_id 90 | INNER JOIN 91 | sys.indexes i 92 | ON t.object_id = i.object_id 93 | INNER JOIN 94 | sys.index_columns ic 95 | ON i.object_id = ic.object_id AND 96 | i.index_id = ic.index_id 97 | INNER JOIN 98 | sys.columns tc 99 | ON ic.object_id = tc.object_id AND 100 | ic.column_id = tc.column_id 101 | WHERE 102 | i.is_primary_key = 1 AND 103 | s.schema_id = SCHEMA_ID(?) AND 104 | t.name = ? 105 | ORDER BY 106 | ic.key_ordinal 107 | ` 108 | 109 | type mssqlDialect struct{} 110 | 111 | func (mssqlDialect) escapeIdent(ident string) string { 112 | // [tablename] 113 | return escapeWithBrackets(ident) 114 | } 115 | 116 | func (d mssqlDialect) ColumnTypes(db *sql.DB, schema, name string) ([]*sql.ColumnType, error) { 117 | return fetchColumnTypes(db, mssqlAllColumns, schema, name, d.escapeIdent) 118 | } 119 | 120 | func (mssqlDialect) PrimaryKey(db *sql.DB, schema, name string) ([]string, error) { 121 | if schema == "" { 122 | return fetchNames(db, mssqlPrimaryKey, "", name) 123 | } 124 | return fetchNames(db, mssqlPrimaryKeyWithSchema, schema, name) 125 | } 126 | 127 | func (mssqlDialect) TableNames(db *sql.DB) ([][2]string, error) { 128 | return fetchObjectNames(db, mssqlTableNamesWithSchema) 129 | } 130 | 131 | func (mssqlDialect) ViewNames(db *sql.DB) ([][2]string, error) { 132 | return fetchObjectNames(db, mssqlViewNamesWithSchema) 133 | } 134 | -------------------------------------------------------------------------------- /dialect_mysql.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "database/sql" 5 | ) 6 | 7 | const mysqlAllColumns = `SELECT * FROM %s LIMIT 0` 8 | 9 | const mysqlTableNamesWithSchema = ` 10 | SELECT 11 | table_schema, 12 | table_name 13 | FROM 14 | information_schema.tables 15 | WHERE 16 | table_type = 'BASE TABLE' 17 | ORDER BY 18 | table_schema, 19 | table_name 20 | ` 21 | 22 | const mysqlViewNamesWithSchema = ` 23 | SELECT 24 | table_schema, 25 | table_name 26 | FROM 27 | information_schema.tables 28 | WHERE 29 | table_type = 'VIEW' 30 | ORDER BY 31 | table_schema, 32 | table_name 33 | ` 34 | 35 | const mysqlPrimaryKey = ` 36 | SELECT 37 | sta.column_name 38 | FROM 39 | information_schema.tables tab 40 | INNER JOIN 41 | information_schema.statistics sta 42 | ON sta.table_schema = tab.table_schema AND 43 | sta.table_name = tab.table_name AND 44 | sta.index_name = 'primary' 45 | WHERE 46 | tab.table_type = 'BASE TABLE' AND 47 | tab.table_schema = database() AND 48 | tab.table_name = ? 49 | ORDER BY 50 | sta.seq_in_index 51 | ` 52 | 53 | const mysqlPrimaryKeyWithSchema = ` 54 | SELECT 55 | sta.column_name 56 | FROM 57 | information_schema.tables tab 58 | INNER JOIN 59 | information_schema.statistics sta 60 | ON sta.table_schema = tab.table_schema AND 61 | sta.table_name = tab.table_name AND 62 | sta.index_name = 'primary' 63 | WHERE 64 | tab.table_type = 'BASE TABLE' AND 65 | tab.table_schema = ? AND 66 | tab.table_name = ? 67 | ORDER BY 68 | sta.seq_in_index 69 | ` 70 | 71 | type mysqlDialect struct{} 72 | 73 | func (mysqlDialect) escapeIdent(ident string) string { 74 | // `tablename` 75 | return escapeWithBackticks(ident) 76 | } 77 | 78 | func (d mysqlDialect) ColumnTypes(db *sql.DB, schema, name string) ([]*sql.ColumnType, error) { 79 | return fetchColumnTypes(db, mysqlAllColumns, schema, name, d.escapeIdent) 80 | } 81 | 82 | func (mysqlDialect) PrimaryKey(db *sql.DB, schema, name string) ([]string, error) { 83 | if schema == "" { 84 | return fetchNames(db, mysqlPrimaryKey, "", name) 85 | } 86 | return fetchNames(db, mysqlPrimaryKeyWithSchema, schema, name) 87 | } 88 | 89 | func (mysqlDialect) TableNames(db *sql.DB) ([][2]string, error) { 90 | return fetchObjectNames(db, mysqlTableNamesWithSchema) 91 | } 92 | 93 | func (mysqlDialect) ViewNames(db *sql.DB) ([][2]string, error) { 94 | return fetchObjectNames(db, mysqlViewNamesWithSchema) 95 | } 96 | -------------------------------------------------------------------------------- /dialect_oracle.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "database/sql" 5 | ) 6 | 7 | // TODO(js) Are we querying the correct tables? See https://dba.stackexchange.com/questions/153436/i-want-to-see-all-tables-of-db-but-no-system-tables 8 | 9 | const oracleAllColumns = `SELECT * FROM %s WHERE 1=0` 10 | 11 | const oracleTableNamesWithSchema = ` 12 | SELECT 13 | owner, 14 | table_name 15 | FROM 16 | all_tables 17 | WHERE 18 | owner IN (SELECT sys_context('userenv', 'current_schema') from dual) 19 | ORDER BY 20 | owner, 21 | table_name 22 | ` 23 | 24 | const oracleViewNamesWithSchema = ` 25 | SELECT 26 | owner, 27 | view_name 28 | FROM 29 | all_views 30 | WHERE 31 | owner IN (SELECT sys_context('userenv', 'current_schema') from dual) 32 | ORDER BY 33 | owner, 34 | view_name 35 | ` 36 | 37 | const oraclePrimaryKey = ` 38 | SELECT 39 | cc.column_name 40 | FROM 41 | all_constraints c, 42 | all_cons_columns cc 43 | WHERE 44 | c.constraint_type = 'P' AND 45 | c.constraint_name = cc.constraint_name AND 46 | c.owner = cc.owner AND 47 | cc.owner IN (SELECT sys_context('userenv', 'current_schema') from dual) AND 48 | cc.table_name = :1 49 | ORDER BY 50 | cc.position 51 | ` 52 | 53 | const oraclePrimaryKeyWithSchema = ` 54 | SELECT 55 | cc.column_name 56 | FROM 57 | all_constraints c, 58 | all_cons_columns cc 59 | WHERE 60 | c.constraint_type = 'P' AND 61 | c.constraint_name = cc.constraint_name AND 62 | c.owner = cc.owner AND 63 | cc.owner = :1 AND 64 | cc.table_name = :2 65 | ORDER BY 66 | cc.position 67 | ` 68 | 69 | type oracleDialect struct{} 70 | 71 | func (oracleDialect) escapeIdent(ident string) string { 72 | // "tablename" 73 | return escapeWithDoubleQuotes(ident) 74 | } 75 | 76 | func (d oracleDialect) ColumnTypes(db *sql.DB, schema, name string) ([]*sql.ColumnType, error) { 77 | return fetchColumnTypes(db, oracleAllColumns, schema, name, d.escapeIdent) 78 | } 79 | 80 | func (oracleDialect) PrimaryKey(db *sql.DB, schema, name string) ([]string, error) { 81 | if schema == "" { 82 | return fetchNames(db, oraclePrimaryKey, "", name) 83 | } 84 | return fetchNames(db, oraclePrimaryKeyWithSchema, schema, name) 85 | } 86 | 87 | func (oracleDialect) TableNames(db *sql.DB) ([][2]string, error) { 88 | return fetchObjectNames(db, oracleTableNamesWithSchema) 89 | } 90 | 91 | func (oracleDialect) ViewNames(db *sql.DB) ([][2]string, error) { 92 | return fetchObjectNames(db, oracleViewNamesWithSchema) 93 | } 94 | -------------------------------------------------------------------------------- /dialect_postgres.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "database/sql" 5 | ) 6 | 7 | // TODO(js) Should we be filtering out system tables, like we currently do? 8 | 9 | const postgresAllColumns = `SELECT * FROM %s LIMIT 0` 10 | 11 | const postgresTableNamesWithSchema = ` 12 | SELECT 13 | table_schema, 14 | table_name 15 | FROM 16 | information_schema.tables 17 | WHERE 18 | table_type = 'BASE TABLE' AND 19 | table_schema NOT IN ('pg_catalog', 'information_schema') 20 | ORDER BY 21 | table_schema, 22 | table_name 23 | ` 24 | 25 | const postgresViewNamesWithSchema = ` 26 | SELECT 27 | table_schema, 28 | table_name 29 | FROM 30 | information_schema.tables 31 | WHERE 32 | table_type = 'VIEW' AND 33 | table_schema NOT IN ('pg_catalog', 'information_schema') 34 | ORDER BY 35 | table_schema, 36 | table_name 37 | ` 38 | 39 | const postgresPrimaryKey = ` 40 | SELECT 41 | kcu.column_name 42 | FROM 43 | information_schema.table_constraints tco 44 | JOIN 45 | information_schema.key_column_usage kcu 46 | ON kcu.constraint_name = tco.constraint_name AND 47 | kcu.constraint_schema = tco.constraint_schema AND 48 | kcu.constraint_name = tco.constraint_name 49 | WHERE 50 | tco.constraint_type = 'PRIMARY KEY' AND 51 | kcu.table_schema = current_schema() AND 52 | kcu.table_name = $1 53 | ORDER BY 54 | kcu.ordinal_position 55 | ` 56 | 57 | const postgresPrimaryKeyWithSchema = ` 58 | SELECT 59 | kcu.column_name 60 | FROM 61 | information_schema.table_constraints tco 62 | JOIN 63 | information_schema.key_column_usage kcu 64 | ON kcu.constraint_name = tco.constraint_name AND 65 | kcu.constraint_schema = tco.constraint_schema AND 66 | kcu.constraint_name = tco.constraint_name 67 | WHERE 68 | tco.constraint_type = 'PRIMARY KEY' AND 69 | kcu.table_schema = $1 AND 70 | kcu.table_name = $2 71 | ORDER BY 72 | kcu.ordinal_position 73 | ` 74 | 75 | type postgresDialect struct{} 76 | 77 | func (postgresDialect) escapeIdent(ident string) string { 78 | // "tablename" 79 | return escapeWithDoubleQuotes(ident) 80 | } 81 | 82 | func (d postgresDialect) ColumnTypes(db *sql.DB, schema, name string) ([]*sql.ColumnType, error) { 83 | return fetchColumnTypes(db, postgresAllColumns, schema, name, d.escapeIdent) 84 | } 85 | 86 | func (postgresDialect) PrimaryKey(db *sql.DB, schema, name string) ([]string, error) { 87 | if schema == "" { 88 | return fetchNames(db, postgresPrimaryKey, "", name) 89 | } 90 | return fetchNames(db, postgresPrimaryKeyWithSchema, schema, name) 91 | } 92 | 93 | func (postgresDialect) TableNames(db *sql.DB) ([][2]string, error) { 94 | return fetchObjectNames(db, postgresTableNamesWithSchema) 95 | } 96 | 97 | func (postgresDialect) ViewNames(db *sql.DB) ([][2]string, error) { 98 | return fetchObjectNames(db, postgresViewNamesWithSchema) 99 | } 100 | -------------------------------------------------------------------------------- /dialect_sqlite.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "database/sql" 5 | ) 6 | 7 | // TODO(js) Can we see tables in an attached database? How are their names handled? See https://sqlite.org/lang_naming.html 8 | 9 | const sqliteAllColumns = `SELECT * FROM %s LIMIT 0` 10 | 11 | const sqliteTableNamesWithSchema = ` 12 | SELECT 13 | "" AS schema, 14 | name 15 | FROM 16 | sqlite_master 17 | WHERE 18 | type = 'table' 19 | ORDER BY 20 | name 21 | ` 22 | 23 | const sqliteViewNamesWithSchema = ` 24 | SELECT 25 | "" AS schema, 26 | name 27 | FROM 28 | sqlite_master 29 | WHERE 30 | type = 'view' 31 | ORDER BY 32 | name 33 | ` 34 | 35 | const sqlitePrimaryKey = ` 36 | SELECT 37 | name 38 | FROM 39 | pragma_table_info(?) 40 | WHERE 41 | pk > 0 42 | ORDER BY 43 | pk 44 | ` 45 | 46 | type sqliteDialect struct{} 47 | 48 | func (sqliteDialect) escapeIdent(ident string) string { 49 | // "tablename" 50 | return escapeWithDoubleQuotes(ident) 51 | } 52 | 53 | func (d sqliteDialect) ColumnTypes(db *sql.DB, schema, name string) ([]*sql.ColumnType, error) { 54 | return fetchColumnTypes(db, sqliteAllColumns, schema, name, d.escapeIdent) 55 | } 56 | 57 | func (sqliteDialect) PrimaryKey(db *sql.DB, schema, name string) ([]string, error) { 58 | // if schema == "" { 59 | // return fetchNames(db, sqlitePrimaryKey, "", name) 60 | // } 61 | return fetchNames(db, sqlitePrimaryKey, "", name) 62 | } 63 | 64 | func (sqliteDialect) TableNames(db *sql.DB) ([][2]string, error) { 65 | return fetchObjectNames(db, sqliteTableNamesWithSchema) 66 | } 67 | 68 | func (sqliteDialect) ViewNames(db *sql.DB) ([][2]string, error) { 69 | return fetchObjectNames(db, sqliteViewNamesWithSchema) 70 | } 71 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package schema provides access to database schema metadata, for database/sql drivers. 2 | // 3 | // For further information about current driver support status, see https://github.com/jimsmart/schema 4 | // 5 | // Table Metadata 6 | // 7 | // The schema package works alongside database/sql and its underlying driver to provide schema metadata. 8 | // 9 | // // Fetch names of all tables 10 | // tnames, err := schema.TableNames(db) 11 | // ... 12 | // // tnames is [][2]string 13 | // for i := range tnames { 14 | // fmt.Println("Table:", tnames[i][1]) 15 | // } 16 | // 17 | // // Output: 18 | // // Table: employee_tbl 19 | // // Table: department_tbl 20 | // // Table: sales_tbl 21 | // 22 | // Both user permissions and current database/schema effect table visibility. 23 | // 24 | // Use schema.ColumnTypes() to query column type metadata for a single table: 25 | // 26 | // // Fetch column metadata for given table 27 | // tcols, err := schema.ColumnTypes(db, "", "employee_tbl") 28 | // ... 29 | // // tcols is []*sql.ColumnType 30 | // for i := range tcols { 31 | // fmt.Println("Column:", tcols[i].Name(), tcols[i].DatabaseTypeName()) 32 | // } 33 | // 34 | // // Output: 35 | // // Column: employee_id INTEGER 36 | // // Column: first_name TEXT 37 | // // Column: last_name TEXT 38 | // // Column: created_at TIMESTAMP 39 | // 40 | // To query table names and column type metadata for all tables, use schema.Tables(). 41 | // 42 | // See also https://golang.org/pkg/database/sql/#ColumnType 43 | // 44 | // Note: underlying support for column type metadata is driver implementation specific and somewhat variable. 45 | // 46 | // View Metadata 47 | // 48 | // The same metadata can also be queried for views also: 49 | // 50 | // // Fetch names of all views 51 | // vnames, err := schema.ViewNames(db) 52 | // ... 53 | // // Fetch column metadata for given view 54 | // vcols, err := schema.ColumnTypes(db, "", "monthly_sales_view") 55 | // ... 56 | // // Fetch column metadata for all views 57 | // views, err := schema.Views(db) 58 | // ... 59 | // 60 | // Primary Key Metadata 61 | // 62 | // To obtain a list of columns making up the primary key for a given table: 63 | // 64 | // // Fetch primary key for given table 65 | // pks, err := schema.PrimaryKey(db, "", "employee_tbl") 66 | // ... 67 | // // pks is []string 68 | // for i := range pks { 69 | // fmt.Println("Primary Key:", pks[i]) 70 | // } 71 | // 72 | // // Output: 73 | // // Primary Key: employee_id 74 | // 75 | package schema 76 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | 4 | # Port numbers (normal port number + 40000): 5 | # - mssql 41433 6 | # - mysql 43306 7 | # - postgres 45432 8 | # - oracle 41521 9 | 10 | mssql: 11 | # See https://hub.docker.com/_/microsoft-mssql-server 12 | image: mcr.microsoft.com/mssql/server:2017-latest-ubuntu 13 | container_name: schema-test-mssql 14 | ports: 15 | - 127.0.0.1:41433:1433 16 | volumes: 17 | # Override the normal entry point, to call our own db-init.sh 18 | - ./docker-entrypoint-mssql.sh:/entrypoint.sh:ro 19 | # Script to do init after startup is complete. 20 | - ./docker-db-init-mssql.sh:/db-init.sh:ro 21 | # SQL script executed at startup. 22 | - ./docker-db-init-mssql.sql:/init.sql:ro 23 | command: /bin/bash /entrypoint.sh 24 | environment: 25 | ACCEPT_EULA: Y 26 | MSSQL_PID: Developer 27 | SA_PASSWORD: 7kRZ4mUsSD4XedMq 28 | 29 | mysql: 30 | # See https://hub.docker.com/_/mysql 31 | image: mysql:latest 32 | container_name: schema-test-mysql 33 | command: --default-authentication-plugin=mysql_native_password 34 | ports: 35 | - 127.0.0.1:43306:3306 36 | environment: 37 | MYSQL_RANDOM_ROOT_PASSWORD: 'yes' 38 | MYSQL_DATABASE: test_db 39 | MYSQL_USER: test_user 40 | MYSQL_PASSWORD: password-123 41 | 42 | oracle: 43 | # See https://www.petefreitag.com/item/886.cfm 44 | image: oracle/database:18.4.0-xe 45 | container_name: schema-test-oracle 46 | ports: 47 | - 127.0.0.1:41521:1521 48 | volumes: 49 | # SQL script executed after initial setup (not on every startup). 50 | - ./docker-db-init-oracle.sql:/opt/oracle/scripts/setup/init.sql:ro 51 | # - ./docker-db-init-oracle.sql:/opt/oracle/scripts/startup/init.sql:ro 52 | 53 | postgres: 54 | # See https://hub.docker.com/_/postgres 55 | image: postgres:latest 56 | container_name: schema-test-postgres 57 | ports: 58 | - 127.0.0.1:45432:5432 59 | environment: 60 | POSTGRES_HOST_AUTH_METHOD: trust 61 | -------------------------------------------------------------------------------- /docker-db-init-mssql.sh: -------------------------------------------------------------------------------- 1 | # Based upon comments here https://github.com/Microsoft/mssql-docker/issues/11 2 | 3 | # Wait for MSSQL to start. 4 | while [ true ]; do 5 | sleep 1s 6 | /opt/mssql-tools/bin/sqlcmd -l 30 -S localhost -h-1 -V1 -U sa -P "$SA_PASSWORD" -Q "select name from sys.databases where state_desc != 'ONLINE'" | grep --quiet '0 rows affected' > /dev/null 2>&1 7 | if [ $? -eq 0 ]; then 8 | # All databases are online. 9 | break 10 | fi 11 | # Retry. 12 | done 13 | 14 | echo "SQL Server is up. Running init.sql script." 15 | /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$SA_PASSWORD" -i init.sql 16 | 17 | # TODO(js) Can we check whether this has already been run enforce 'only run once'? 18 | # Or should that be the responsibility of the .sql script? Or? 19 | -------------------------------------------------------------------------------- /docker-db-init-mssql.sql: -------------------------------------------------------------------------------- 1 | CREATE LOGIN test_user WITH PASSWORD = 'Password-123'; 2 | GO 3 | CREATE USER test_user FOR LOGIN test_user; 4 | GO 5 | CREATE SCHEMA test_db AUTHORIZATION test_user; 6 | GO 7 | ALTER USER test_user WITH default_schema = test_db; 8 | GO 9 | EXEC sp_addrolemember db_ddladmin, test_user; 10 | GO 11 | -------------------------------------------------------------------------------- /docker-db-init-oracle.sql: -------------------------------------------------------------------------------- 1 | alter session set "_ORACLE_SCRIPT"=true; 2 | CREATE USER test_user IDENTIFIED BY Password123; 3 | GRANT CONNECT, RESOURCE, DBA TO test_user; 4 | -------------------------------------------------------------------------------- /docker-entrypoint-mssql.sh: -------------------------------------------------------------------------------- 1 | # Start our db-init script in background 2 | /bin/bash /db-init.sh & 3 | 4 | # Start SQLServer 5 | /opt/mssql/bin/sqlservr 6 | -------------------------------------------------------------------------------- /escape_test.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import "testing" 4 | 5 | // Tests for internal methods. 6 | 7 | type test struct { 8 | input string 9 | expect string 10 | } 11 | 12 | func TestEscapeWithDoubleQuotes(t *testing.T) { 13 | 14 | tests := []test{ 15 | {input: `foo`, expect: `"foo"`}, 16 | {input: `fo"o`, expect: `"fo""o"`}, 17 | {input: `foo"`, expect: `"foo"""`}, 18 | } 19 | 20 | for _, x := range tests { 21 | res := escapeWithDoubleQuotes(x.input) 22 | if res != x.expect { 23 | t.Errorf("Failed, got: %s, want: %s.", res, x.expect) 24 | } 25 | } 26 | } 27 | func TestEscapeWithBackticks(t *testing.T) { 28 | 29 | tests := []test{ 30 | {input: "foo", expect: "`foo`"}, 31 | {input: "fo`o", expect: "`fo``o`"}, 32 | {input: "foo`", expect: "`foo```"}, 33 | } 34 | 35 | for _, x := range tests { 36 | res := escapeWithBackticks(x.input) 37 | if res != x.expect { 38 | t.Errorf("Failed, got: %s, want: %s.", res, x.expect) 39 | } 40 | } 41 | } 42 | 43 | func TestEscapeWithBrackets(t *testing.T) { 44 | 45 | tests := []test{ 46 | {input: "foo", expect: "[foo]"}, 47 | {input: "fo]o", expect: "[fo]]o]"}, 48 | {input: "foo]", expect: "[foo]]]"}, 49 | } 50 | 51 | for _, x := range tests { 52 | res := escapeWithBrackets(x.input) 53 | if res != x.expect { 54 | t.Errorf("Failed, got: %s, want: %s.", res, x.expect) 55 | } 56 | } 57 | } 58 | 59 | func TestEscapeWithBraces(t *testing.T) { 60 | 61 | tests := []test{ 62 | {input: "foo", expect: "{foo}"}, 63 | {input: "fo}o", expect: "{fo}}o}"}, 64 | {input: "foo}", expect: "{foo}}}"}, 65 | } 66 | 67 | for _, x := range tests { 68 | res := escapeWithBraces(x.input) 69 | if res != x.expect { 70 | t.Errorf("Failed, got: %s, want: %s.", res, x.expect) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jimsmart/schema 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/denisenkom/go-mssqldb v0.12.3 7 | github.com/fsnotify/fsnotify v1.6.0 // indirect 8 | github.com/go-sql-driver/mysql v1.7.0 9 | github.com/godror/godror v0.36.0 10 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect 11 | github.com/lib/pq v1.10.7 12 | github.com/mattn/go-sqlite3 v1.14.16 13 | github.com/onsi/ginkgo v1.16.5 14 | github.com/onsi/gomega v1.24.2 15 | golang.org/x/crypto v0.5.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= 2 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= 3 | github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= 4 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 5 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 6 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw= 10 | github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo= 11 | github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= 12 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 13 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 14 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 15 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 16 | github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= 17 | github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 18 | github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= 19 | github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 20 | github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= 21 | github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 22 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 23 | github.com/godror/godror v0.36.0 h1:4kymETiaTOJcyF5+47JSUs44Pi0R9bTwsWtBTWqAVRs= 24 | github.com/godror/godror v0.36.0/go.mod h1:jW1+pN+z/V0h28p9XZXVNtEvfZP/2EBfaSjKJLp3E4g= 25 | github.com/godror/knownpb v0.1.0 h1:dJPK8s/I3PQzGGaGcUStL2zIaaICNzKKAK8BzP1uLio= 26 | github.com/godror/knownpb v0.1.0/go.mod h1:4nRFbQo1dDuwKnblRXDxrfCFYeT4hjg3GjMqef58eRE= 27 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 28 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= 29 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 30 | github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= 31 | github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= 32 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 33 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 34 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 35 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 36 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 37 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 38 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 39 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 40 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 41 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 42 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 43 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 44 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 45 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 46 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 47 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 48 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 49 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 50 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 51 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 52 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 53 | github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= 54 | github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 55 | github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= 56 | github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 57 | github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= 58 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 59 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 60 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 61 | github.com/oklog/ulid/v2 v2.0.2 h1:r4fFzBm+bv0wNKNh5eXTwU7i85y5x+uwkxCUTNVQqLc= 62 | github.com/oklog/ulid/v2 v2.0.2/go.mod h1:mtBL0Qe/0HAx6/a4Z30qxVIAL1eQDweXq5lxOEiwQ68= 63 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 64 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 65 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 66 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 67 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 68 | github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= 69 | github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= 70 | github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= 71 | github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0= 72 | github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo= 73 | github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw= 74 | github.com/onsi/ginkgo/v2 v2.6.1 h1:1xQPCjcqYw/J5LchOcp4/2q/jzJFjiAOc25chhnDw+Q= 75 | github.com/onsi/ginkgo/v2 v2.6.1/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= 76 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 77 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 78 | github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 79 | github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= 80 | github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= 81 | github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= 82 | github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= 83 | github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= 84 | github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= 85 | github.com/onsi/gomega v1.24.2 h1:J/tulyYK6JwBldPViHJReihxxZ+22FHs0piGjQAvoUE= 86 | github.com/onsi/gomega v1.24.2/go.mod h1:gs3J10IS7Z7r7eXRoNJIrNqU4ToQukCJhFtKrWgHWnk= 87 | github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= 88 | github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= 89 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 90 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 91 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 92 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 93 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 94 | github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 95 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 96 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 97 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 98 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 99 | golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 100 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 101 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 102 | golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= 103 | golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= 104 | golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= 105 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 106 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= 107 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 108 | golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= 109 | golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 110 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 111 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 112 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 113 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 114 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 115 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 116 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 117 | golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 118 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 119 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 120 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 121 | golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 122 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 123 | golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 124 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 125 | golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= 126 | golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= 127 | golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= 128 | golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= 129 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 130 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 131 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 132 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 133 | golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 134 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 135 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 136 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 137 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 138 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 139 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 140 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 141 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 142 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 143 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 144 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 145 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 146 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 147 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 148 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 149 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 150 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 151 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 152 | golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 153 | golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 154 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 155 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 156 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 157 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 158 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 159 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 160 | golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= 161 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 162 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 163 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 164 | golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 165 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 166 | golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= 167 | golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= 168 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 169 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 170 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 171 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 172 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 173 | golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 174 | golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= 175 | golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 176 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 177 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 178 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 179 | golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= 180 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 181 | golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= 182 | golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= 183 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 184 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 185 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 186 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 187 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 188 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 189 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 190 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 191 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 192 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 193 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 194 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 195 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 196 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 197 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= 198 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 199 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 200 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 201 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 202 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 203 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 204 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 205 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 206 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 207 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 208 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 209 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 210 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 211 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 212 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 213 | -------------------------------------------------------------------------------- /schema.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | ) 7 | 8 | // https://github.com/golang/go/issues/7408 9 | // 10 | // https://github.com/golang/go/issues/7408#issuecomment-252046876 11 | // 12 | // If this package were to be part of database/sql, then the API would become like:- 13 | // 14 | // func (db *DB) Table(schema, table string) ([]*ColumnType, error) 15 | // func (db *DB) TableNames() ([][2]string, error) 16 | // func (db *DB) Tables() (map[[2]string][]*ColumnType, error) 17 | // func (db *DB) View(schema, view string) ([]*ColumnType, error) 18 | // func (db *DB) ViewNames() ([][2]string, error) 19 | // func (db *DB) Views() (map[[2]string][]*ColumnType, error) 20 | // 21 | 22 | // 23 | 24 | // UnknownDriverError is returned when there is no matching 25 | // database driver type name in the driverDialect table. 26 | // 27 | // Errors of this kind are caused by using an unsupported 28 | // database driver/dialect, or if/when a database driver 29 | // developer renames the type underlying calls to db.Driver(). 30 | type UnknownDriverError struct { 31 | Driver string 32 | } 33 | 34 | // Error returns a formatted string description. 35 | func (e UnknownDriverError) Error() string { 36 | return fmt.Sprintf("unknown database driver: %s", e.Driver) 37 | } 38 | 39 | // 40 | 41 | // Tables returns column type metadata for all tables in the current schema. 42 | // 43 | // The returned map is keyed by table name tuples. 44 | func Tables(db *sql.DB) (map[[2]string][]*sql.ColumnType, error) { 45 | d, err := getDialect(db) 46 | if err != nil { 47 | return nil, err 48 | } 49 | names, err := d.TableNames(db) 50 | if err != nil { 51 | return nil, err 52 | } 53 | if len(names) == 0 { 54 | return nil, nil 55 | } 56 | m := make(map[[2]string][]*sql.ColumnType, len(names)) 57 | for _, n := range names { 58 | ct, err := d.ColumnTypes(db, n[0], n[1]) 59 | if err != nil { 60 | return nil, err 61 | } 62 | m[n] = ct 63 | } 64 | return m, nil 65 | } 66 | 67 | // Views returns column type metadata for all views in the current schema. 68 | // 69 | // The returned map is keyed by view name tuples. 70 | func Views(db *sql.DB) (map[[2]string][]*sql.ColumnType, error) { 71 | d, err := getDialect(db) 72 | if err != nil { 73 | return nil, err 74 | } 75 | names, err := d.ViewNames(db) 76 | if err != nil { 77 | return nil, err 78 | } 79 | if len(names) == 0 { 80 | return nil, nil 81 | } 82 | m := make(map[[2]string][]*sql.ColumnType, len(names)) 83 | for _, n := range names { 84 | ct, err := d.ColumnTypes(db, n[0], n[1]) 85 | if err != nil { 86 | return nil, err 87 | } 88 | m[n] = ct 89 | } 90 | return m, nil 91 | } 92 | 93 | // TableNames returns a list of all table names. 94 | // 95 | // Each name consists of a [2]string tuple: schema name, table name. 96 | func TableNames(db *sql.DB) ([][2]string, error) { 97 | d, err := getDialect(db) 98 | if err != nil { 99 | return nil, err 100 | } 101 | return d.TableNames(db) 102 | } 103 | 104 | // ViewNames returns a list of all view names. 105 | // 106 | // Each name consists of a [2]string tuple: schema name, view name. 107 | func ViewNames(db *sql.DB) ([][2]string, error) { 108 | d, err := getDialect(db) 109 | if err != nil { 110 | return nil, err 111 | } 112 | return d.ViewNames(db) 113 | } 114 | 115 | // ColumnTypes returns the column type metadata for the given object (table or view) in the given schema. 116 | // 117 | // Setting schema to an empty string results in the current schema being used. 118 | func ColumnTypes(db *sql.DB, schema, object string) ([]*sql.ColumnType, error) { 119 | d, err := getDialect(db) 120 | if err != nil { 121 | return nil, err 122 | } 123 | return d.ColumnTypes(db, schema, object) 124 | } 125 | 126 | // PrimaryKey returns a list of column names making up the primary 127 | // key for the given table in the given schema. 128 | func PrimaryKey(db *sql.DB, schema, table string) ([]string, error) { 129 | d, err := getDialect(db) 130 | if err != nil { 131 | return nil, err 132 | } 133 | return d.PrimaryKey(db, schema, table) 134 | } 135 | 136 | // fetchNames executes the given query with an optional name parameter, 137 | // and returns a list of table/view/column names. 138 | // 139 | // The name parameter (if not "") is passed as a parameter to db.Query. 140 | func fetchNames(db *sql.DB, query, schema, name string) ([]string, error) { 141 | var rows *sql.Rows 142 | var err error 143 | if len(schema) > 0 { 144 | rows, err = db.Query(query, schema, name) 145 | } else { 146 | rows, err = db.Query(query, name) 147 | } 148 | if err != nil { 149 | return nil, err 150 | } 151 | defer rows.Close() 152 | // Scan result into list of names. 153 | var names []string 154 | n := "" 155 | for rows.Next() { 156 | err = rows.Scan(&n) 157 | if err != nil { 158 | return nil, err 159 | } 160 | names = append(names, n) 161 | } 162 | return names, nil 163 | } 164 | 165 | // fetchObjectNames executes the given query 166 | // and returns a list of table/view/column names. 167 | func fetchObjectNames(db *sql.DB, query string) ([][2]string, error) { 168 | var rows *sql.Rows 169 | var err error 170 | rows, err = db.Query(query) 171 | if err != nil { 172 | return nil, err 173 | } 174 | defer rows.Close() 175 | // Scan result into list of names. 176 | var names [][2]string 177 | s := "" 178 | n := "" 179 | for rows.Next() { 180 | err = rows.Scan(&s, &n) 181 | if err != nil { 182 | return nil, err 183 | } 184 | names = append(names, [2]string{s, n}) 185 | } 186 | return names, nil 187 | } 188 | 189 | func getDialect(db *sql.DB) (dialect, error) { 190 | dt := fmt.Sprintf("%T", db.Driver()) 191 | d, ok := driverDialect[dt] 192 | if !ok { 193 | return nil, UnknownDriverError{Driver: dt} 194 | } 195 | return d, nil 196 | } 197 | 198 | // fetchColumnTypes queries the database and returns column's type metadata 199 | // for a single table or view. 200 | func fetchColumnTypes(db *sql.DB, query, schema, name string, escapeIdent func(string) string) ([]*sql.ColumnType, error) { 201 | if schema == "" { 202 | query = fmt.Sprintf(query, escapeIdent(name)) 203 | } else { 204 | n := fmt.Sprintf("%s.%s", escapeIdent(schema), escapeIdent(name)) 205 | query = fmt.Sprintf(query, n) 206 | } 207 | rows, err := db.Query(query) 208 | if err != nil { 209 | return nil, err 210 | } 211 | defer rows.Close() 212 | return rows.ColumnTypes() 213 | } 214 | -------------------------------------------------------------------------------- /schema_mssql_test.go: -------------------------------------------------------------------------------- 1 | package schema_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | _ "github.com/denisenkom/go-mssqldb" // mssql 7 | // _ "github.com/minus5/gofreetds" // mssql 8 | 9 | . "github.com/onsi/ginkgo" 10 | // . "github.com/onsi/gomega" 11 | ) 12 | 13 | // Database/user setup script, run by Docker: docker-db-init-mssql.sql 14 | 15 | var _ = Describe("schema", func() { 16 | Context("using github.com/denisenkom/go-mssqldb (Microsoft SQL-Server)", func() { 17 | 18 | const ( 19 | user = "test_user" 20 | pass = "Password-123" 21 | host = "localhost" 22 | port = "41433" 23 | ) 24 | 25 | var mssql = &testParams{ 26 | DriverName: "mssql", 27 | ConnStr: fmt.Sprintf("user id=%s;password=%s;server=%s;port=%s", user, pass, host, port), 28 | // ConnStr: fmt.Sprintf("user id=%s;password=%s;server=%s:%s", user, pass, host, port), // gofreetds 29 | 30 | CreateDDL: []string{` 31 | CREATE TABLE web_resource ( 32 | id INTEGER NOT NULL, 33 | url NVARCHAR NOT NULL UNIQUE, 34 | content VARBINARY, 35 | compressed_size INTEGER NOT NULL, 36 | content_length INTEGER NOT NULL, 37 | content_type NVARCHAR NOT NULL, 38 | etag NVARCHAR NOT NULL, 39 | last_modified NVARCHAR NOT NULL, 40 | created_at DATETIME NOT NULL, 41 | modified_at DATETIME, 42 | PRIMARY KEY (id) 43 | )`, 44 | `CREATE INDEX idx_web_resource_url ON web_resource(url)`, 45 | `CREATE INDEX idx_web_resource_created_at ON web_resource (created_at)`, 46 | `CREATE INDEX idx_web_resource_modified_at ON web_resource (modified_at)`, 47 | `CREATE VIEW web_resource_view AS SELECT id, url FROM web_resource`, // TODO gofreetds barfs on this!? 48 | // `CREATE VIEW web_resource_view AS SELECT t.id, t.url FROM web_resource t`, 49 | // `CREATE VIEW web_resource_view AS (SELECT id, url FROM web_resource)`, 50 | // `CREATE VIEW web_resource_view AS (SELECT t.id, t.url FROM web_resource t)`, 51 | `CREATE TABLE person ( 52 | given_name NVARCHAR NOT NULL, 53 | family_name NVARCHAR NOT NULL, 54 | PRIMARY KEY (family_name, given_name) 55 | )`, 56 | }, 57 | DropDDL: []string{ 58 | `DROP TABLE person`, 59 | `DROP VIEW IF EXISTS web_resource_view`, 60 | `DROP INDEX IF EXISTS idx_web_resource_modified_at ON web_resource`, 61 | `DROP INDEX IF EXISTS idx_web_resource_created_at ON web_resource`, 62 | `DROP INDEX IF EXISTS idx_web_resource_url ON web_resource`, 63 | `DROP TABLE web_resource`, 64 | }, 65 | 66 | TableExpRes: []string{ 67 | "id", 68 | "url", 69 | "content", 70 | "compressed_size", 71 | "content_length", 72 | "content_type", 73 | "etag", 74 | "last_modified", 75 | "created_at", 76 | "modified_at", 77 | }, 78 | ViewExpRes: []string{ 79 | "id", 80 | "url", 81 | }, 82 | 83 | TableNamesExpRes: [][2]string{ 84 | {"test_db", "person"}, 85 | {"test_db", "web_resource"}, 86 | }, 87 | ViewNamesExpRes: [][2]string{ 88 | {"test_db", "web_resource_view"}, 89 | }, 90 | 91 | PrimaryKeysExpRes: []string{"family_name", "given_name"}, 92 | } 93 | 94 | SchemaTestRunner(mssql) 95 | }) 96 | }) 97 | -------------------------------------------------------------------------------- /schema_mysql_test.go: -------------------------------------------------------------------------------- 1 | package schema_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | _ "github.com/go-sql-driver/mysql" // mysql 7 | // _ "github.com/ziutek/mymysql/godrv" // mymysql 8 | 9 | . "github.com/onsi/ginkgo" 10 | // . "github.com/onsi/gomega" 11 | ) 12 | 13 | // Database/user setup by Docker in: docker-compose.yml 14 | 15 | var _ = Describe("schema", func() { 16 | Context("using github.com/go-sql-driver/mysql (MySQL)", func() { 17 | 18 | const ( 19 | user = "test_user" 20 | pass = "password-123" 21 | host = "localhost" 22 | port = "43306" 23 | dbs = "test_db" 24 | ) 25 | 26 | var mysql = &testParams{ 27 | DriverName: "mysql", 28 | // DriverName: "mymysql", 29 | ConnStr: fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", user, pass, host, port, dbs), // mysql 30 | // ConnStr: fmt.Sprintf("tcp:%s:%s*%s/%s/%s", host, port, dbs, user, pass), // mymysql 31 | 32 | CreateDDL: []string{` 33 | CREATE TABLE IF NOT EXISTS web_resource ( 34 | id INTEGER NOT NULL, 35 | url VARCHAR(255) NOT NULL UNIQUE, -- TODO(js) Earlier MySQL cannot handle UNIQUE with 1024 length. 36 | content BLOB, 37 | compressed_size INTEGER NOT NULL, 38 | content_length INTEGER NOT NULL, 39 | content_type VARCHAR(128) NOT NULL, 40 | etag VARCHAR(128) NOT NULL, 41 | last_modified VARCHAR(128) NOT NULL, 42 | created_at TIMESTAMP NOT NULL, 43 | modified_at TIMESTAMP NULL DEFAULT NULL, 44 | PRIMARY KEY (id), 45 | INDEX (url), 46 | INDEX (created_at), 47 | INDEX (modified_at) 48 | )`, 49 | `CREATE VIEW web_resource_view AS SELECT id, url FROM web_resource`, 50 | `CREATE TABLE IF NOT EXISTS person ( 51 | given_name VARCHAR(128) NOT NULL, 52 | family_name VARCHAR(128) NOT NULL, 53 | PRIMARY KEY (family_name, given_name) 54 | )`, 55 | }, 56 | DropDDL: []string{ 57 | `DROP TABLE person`, 58 | `DROP VIEW web_resource_view`, 59 | `DROP TABLE web_resource`, 60 | }, 61 | 62 | TableExpRes: []string{ 63 | "id", 64 | "url", 65 | "content", 66 | "compressed_size", 67 | "content_length", 68 | "content_type", 69 | "etag", 70 | "last_modified", 71 | "created_at", 72 | "modified_at", 73 | }, 74 | ViewExpRes: []string{ 75 | "id", 76 | "url", 77 | }, 78 | 79 | TableNamesExpRes: [][2]string{ 80 | {"test_db", "person"}, 81 | {"test_db", "web_resource"}, 82 | }, 83 | ViewNamesExpRes: [][2]string{ 84 | {"test_db", "web_resource_view"}, 85 | }, 86 | 87 | PrimaryKeysExpRes: []string{"family_name", "given_name"}, 88 | } 89 | 90 | SchemaTestRunner(mysql) 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /schema_oracle_test.go: -------------------------------------------------------------------------------- 1 | // +build !travis 2 | 3 | package schema_test 4 | 5 | import ( 6 | "fmt" 7 | 8 | // _ "github.com/mattn/go-oci8" // oci8 9 | // _ "gopkg.in/rana/ora.v4" // ora 10 | 11 | _ "github.com/godror/godror" // godror 12 | 13 | . "github.com/onsi/ginkgo" 14 | // . "github.com/onsi/gomega" 15 | ) 16 | 17 | // See README.md to learn how to set up Oracle for testing purposes. 18 | 19 | // Database/user setup script, run by Docker: docker-db-init-oracle.sql 20 | 21 | var _ = Describe("schema", func() { 22 | Context("using github.com/godror/godror (Oracle)", func() { 23 | 24 | const ( 25 | user = "test_user" 26 | pass = "Password123" 27 | host = "localhost" 28 | port = "41521" 29 | dbs = "xe" 30 | ) 31 | 32 | var oracle = &testParams{ 33 | DriverName: "godror", 34 | // DriverName: "oci8", 35 | // DriverName: "ora", 36 | ConnStr: fmt.Sprintf("%s/%s@%s:%s/%s", user, pass, host, port, dbs), 37 | 38 | CreateDDL: []string{` 39 | CREATE TABLE web_resource ( 40 | id NUMBER NOT NULL, 41 | url NVARCHAR2(1024) NOT NULL UNIQUE, 42 | content BLOB, 43 | compressed_size NUMBER NOT NULL, 44 | content_length NUMBER NOT NULL, 45 | content_type NVARCHAR2(128) NOT NULL, 46 | etag NVARCHAR2(128) NOT NULL, 47 | last_modified NVARCHAR2(128) NOT NULL, 48 | created_at TIMESTAMP WITH TIME ZONE NOT NULL, 49 | modified_at TIMESTAMP WITH TIME ZONE, 50 | PRIMARY KEY (id) 51 | )`, 52 | // `CREATE INDEX idx_web_resource_url ON web_resource(url)`, 53 | `CREATE INDEX idx_web_resource_created_at ON web_resource(created_at)`, 54 | `CREATE INDEX idx_web_resource_modified_at ON web_resource(modified_at)`, 55 | `CREATE VIEW web_resource_view AS SELECT id, url FROM web_resource`, 56 | `CREATE TABLE person ( 57 | given_name NVARCHAR2(128) NOT NULL, 58 | family_name NVARCHAR2(128) NOT NULL, 59 | PRIMARY KEY (family_name, given_name) 60 | )`, 61 | }, 62 | DropDDL: []string{ 63 | `DROP TABLE person`, 64 | `DROP VIEW web_resource_view`, 65 | `DROP INDEX idx_web_resource_modified_at`, 66 | `DROP INDEX idx_web_resource_created_at`, 67 | // `DROP INDEX idx_web_resource_url`, 68 | `DROP TABLE web_resource`, 69 | }, 70 | 71 | TableExpRes: []string{ 72 | "ID", 73 | "URL", 74 | "CONTENT", 75 | "COMPRESSED_SIZE", 76 | "CONTENT_LENGTH", 77 | "CONTENT_TYPE", 78 | "ETAG", 79 | "LAST_MODIFIED", 80 | "CREATED_AT", 81 | "MODIFIED_AT", 82 | }, 83 | ViewExpRes: []string{ 84 | "ID", 85 | "URL", 86 | }, 87 | 88 | TableNamesExpRes: [][2]string{ 89 | {"TEST_USER", "PERSON"}, 90 | {"TEST_USER", "WEB_RESOURCE"}, 91 | }, 92 | ViewNamesExpRes: [][2]string{ 93 | {"TEST_USER", "WEB_RESOURCE_VIEW"}, 94 | }, 95 | 96 | PrimaryKeysExpRes: []string{"FAMILY_NAME", "GIVEN_NAME"}, 97 | } 98 | 99 | SchemaTestRunner(oracle) 100 | }) 101 | }) 102 | 103 | // func oraDump(db *sql.DB) error { 104 | 105 | // //SELECT table_name FROM user_tables 106 | // rows, err := db.Query(` 107 | // SELECT * 108 | // FROM user_tables 109 | // `) 110 | // if err != nil { 111 | // return err 112 | // } 113 | // defer rows.Close() 114 | 115 | // ci, err := rows.ColumnTypes() 116 | // if err != nil { 117 | // return err 118 | // } 119 | // for _, c := range ci { 120 | // log.Printf("%v", c) 121 | // } 122 | 123 | // cols, err := rows.Columns() 124 | // if err != nil { 125 | // return err 126 | // } 127 | // vals := make([]interface{}, len(cols)) 128 | // for i, _ := range cols { 129 | // vals[i] = new(sql.RawBytes) 130 | // } 131 | 132 | // for rows.Next() { 133 | // err = rows.Scan(vals...) 134 | // if err != nil { 135 | // // return err 136 | // log.Printf("%v", err) 137 | // } 138 | // s := "" 139 | // for _, v := range vals { 140 | // s = s + fmt.Sprintf("%s ", v) 141 | // } 142 | // log.Print(s) 143 | // } 144 | // return nil 145 | // } 146 | -------------------------------------------------------------------------------- /schema_postgres_test.go: -------------------------------------------------------------------------------- 1 | package schema_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | _ "github.com/lib/pq" // postgres 7 | // _ "github.com/jackc/pgx/stdlib" // pgx 8 | // _ "github.com/jbarham/gopgsqldriver" // postgres 9 | 10 | . "github.com/onsi/ginkgo" 11 | // . "github.com/onsi/gomega" 12 | ) 13 | 14 | // Database/user setup not needed: default database and schema are empty on Postgres. 15 | 16 | var _ = Describe("schema", func() { 17 | Context("using github.com/lib/pq (Postgres)", func() { 18 | 19 | const ( 20 | user = "postgres" 21 | host = "localhost" 22 | port = "45432" 23 | ) 24 | 25 | var postgres = &testParams{ 26 | DriverName: "postgres", 27 | // DriverName: "pgx", 28 | ConnStr: fmt.Sprintf("user=%s host=%s port=%s sslmode=disable", user, host, port), 29 | 30 | CreateDDL: []string{` 31 | CREATE TABLE web_resource ( 32 | id INTEGER NOT NULL, 33 | url TEXT NOT NULL UNIQUE, 34 | content BYTEA, 35 | compressed_size INTEGER NOT NULL, 36 | content_length INTEGER NOT NULL, 37 | content_type TEXT NOT NULL, 38 | etag TEXT NOT NULL, 39 | last_modified TEXT NOT NULL, 40 | created_at TIMESTAMP NOT NULL, 41 | modified_at TIMESTAMP, 42 | PRIMARY KEY (id) 43 | )`, 44 | `CREATE INDEX idx_web_resource_url ON web_resource(url)`, 45 | `CREATE INDEX idx_web_resource_created_at ON web_resource(created_at)`, 46 | `CREATE INDEX idx_web_resource_modified_at ON web_resource(modified_at)`, 47 | `CREATE VIEW web_resource_view AS SELECT id, url FROM web_resource`, 48 | `CREATE TABLE person ( 49 | given_name TEXT NOT NULL, 50 | family_name TEXT NOT NULL, 51 | PRIMARY KEY (family_name, given_name) 52 | )`, 53 | }, 54 | DropDDL: []string{ 55 | `DROP TABLE person`, 56 | `DROP VIEW web_resource_view`, 57 | `DROP INDEX idx_web_resource_modified_at`, 58 | `DROP INDEX idx_web_resource_created_at`, 59 | `DROP INDEX idx_web_resource_url`, 60 | `DROP TABLE web_resource`, 61 | }, 62 | 63 | TableExpRes: []string{ 64 | "id", 65 | "url", 66 | "content", 67 | "compressed_size", 68 | "content_length", 69 | "content_type", 70 | "etag", 71 | "last_modified", 72 | "created_at", 73 | "modified_at", 74 | }, 75 | ViewExpRes: []string{ 76 | "id", 77 | "url", 78 | }, 79 | 80 | TableNamesExpRes: [][2]string{ 81 | {"public", "person"}, 82 | {"public", "web_resource"}, 83 | }, 84 | ViewNamesExpRes: [][2]string{ 85 | {"public", "web_resource_view"}, 86 | }, 87 | 88 | PrimaryKeysExpRes: []string{"family_name", "given_name"}, 89 | } 90 | 91 | SchemaTestRunner(postgres) 92 | }) 93 | }) 94 | -------------------------------------------------------------------------------- /schema_sqlite_test.go: -------------------------------------------------------------------------------- 1 | package schema_test 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | _ "github.com/mattn/go-sqlite3" // sqlite3 8 | // _ "github.com/gwenn/gosqlite" // sqlite3 9 | // _ "github.com/mxk/go-sqlite/sqlite3" // sqlite3 10 | 11 | . "github.com/onsi/ginkgo" 12 | // . "github.com/onsi/gomega" 13 | ) 14 | 15 | var _ = Describe("schema", func() { 16 | Context("using github.com/mattn/go-sqlite3 (SQLite)", func() { 17 | 18 | const ( 19 | dbs = ":memory:" 20 | // dbs = "./test.sqlite" 21 | ) 22 | 23 | var sqlite = &testParams{ 24 | DriverName: "sqlite3", 25 | ConnStr: dbs, 26 | 27 | CreateDDL: []string{` 28 | CREATE TABLE IF NOT EXISTS web_resource ( 29 | id INTEGER NOT NULL, 30 | url TEXT NOT NULL UNIQUE, 31 | content BLOB, 32 | compressed_size INTEGER NOT NULL, 33 | content_length INTEGER NOT NULL, 34 | content_type TEXT NOT NULL, 35 | etag TEXT NOT NULL, 36 | last_modified TEXT NOT NULL, 37 | created_at DATETIME NOT NULL, 38 | modified_at DATETIME, 39 | PRIMARY KEY (id) 40 | )`, 41 | `CREATE INDEX IF NOT EXISTS idx_web_resource_url ON web_resource(url)`, 42 | `CREATE INDEX IF NOT EXISTS idx_web_resource_created_at ON web_resource(created_at)`, 43 | `CREATE INDEX IF NOT EXISTS idx_web_resource_modified_at ON web_resource(modified_at)`, 44 | `CREATE VIEW web_resource_view AS SELECT id, url FROM web_resource`, 45 | `CREATE TABLE IF NOT EXISTS person ( 46 | given_name TEXT NOT NULL, 47 | family_name TEXT NOT NULL, 48 | PRIMARY KEY (family_name, given_name) 49 | )`, 50 | }, 51 | DropDDL: []string{ 52 | `DROP TABLE person`, 53 | `DROP VIEW web_resource_view`, 54 | `DROP INDEX idx_web_resource_modified_at`, 55 | `DROP INDEX idx_web_resource_created_at`, 56 | `DROP INDEX idx_web_resource_url`, 57 | `DROP TABLE web_resource`, 58 | }, 59 | DropFn: func() { 60 | if dbs == ":memory:" { 61 | return 62 | } 63 | err := os.Remove(dbs) 64 | if err != nil { 65 | log.Printf("os.Remove error %v", err) 66 | } 67 | }, 68 | 69 | TableExpRes: []string{ 70 | "id", 71 | "url", 72 | "content", 73 | "compressed_size", 74 | "content_length", 75 | "content_type", 76 | "etag", 77 | "last_modified", 78 | "created_at", 79 | "modified_at", 80 | }, 81 | ViewExpRes: []string{ 82 | "id", 83 | "url", 84 | }, 85 | 86 | TableNamesExpRes: [][2]string{ 87 | {"", "person"}, 88 | {"", "web_resource"}, 89 | }, 90 | ViewNamesExpRes: [][2]string{ 91 | {"", "web_resource_view"}, 92 | }, 93 | 94 | PrimaryKeysExpRes: []string{"family_name", "given_name"}, 95 | } 96 | 97 | SchemaTestRunner(sqlite) 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /schema_suite_test.go: -------------------------------------------------------------------------------- 1 | package schema_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestSchema(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Schema Suite") 13 | } 14 | -------------------------------------------------------------------------------- /schema_test.go: -------------------------------------------------------------------------------- 1 | package schema_test 2 | 3 | import ( 4 | "database/sql" 5 | "database/sql/driver" 6 | "log" 7 | "strings" 8 | 9 | "github.com/jimsmart/schema" 10 | 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/gomega" 13 | ) 14 | 15 | type testParams struct { 16 | DriverName string 17 | ConnStr string 18 | 19 | CreateDDL []string 20 | DropDDL []string 21 | DropFn func() 22 | 23 | TableExpRes []string 24 | ViewExpRes []string 25 | 26 | TableNamesExpRes [][2]string 27 | ViewNamesExpRes [][2]string 28 | 29 | PrimaryKeysExpRes []string 30 | } 31 | 32 | func SchemaTestRunner(params *testParams) { 33 | 34 | setup := func() (*sql.DB, func()) { 35 | db, err := sql.Open(params.DriverName, params.ConnStr) 36 | if err != nil { 37 | log.Fatalf("sql.Open error %v", err) 38 | } 39 | 40 | for _, ddl := range params.CreateDDL { 41 | _, err = db.Exec(ddl) 42 | if err != nil { 43 | // log.Fatalf("db.Exec (create) error %v", err) 44 | log.Printf("db.Exec (create) error %v exec %s", err, ddl) 45 | } 46 | } 47 | 48 | doneFn := func() { 49 | for _, ddl := range params.DropDDL { 50 | _, err = db.Exec(ddl) 51 | if err != nil { 52 | // log.Fatalf("db.Exec (drop) error %v", err) 53 | log.Printf("db.Exec (drop) error %v exec %s", err, ddl) 54 | } 55 | } 56 | err = db.Close() 57 | if err != nil { 58 | log.Printf("db.Close error %v", err) 59 | } 60 | if params.DropFn != nil { 61 | params.DropFn() 62 | } 63 | } 64 | 65 | return db, doneFn 66 | } 67 | 68 | // TODO(js) We should test Tables and Views against empty databases. 69 | 70 | Describe("ColumnTypes", func() { 71 | It("should return the column type info for an existing table", func() { 72 | db, done := setup() 73 | defer done() 74 | ci, err := schema.ColumnTypes(db, params.TableNamesExpRes[1][0], params.TableNamesExpRes[1][1]) 75 | Expect(err).To(BeNil()) 76 | var list []string 77 | for _, c := range ci { 78 | list = append(list, c.Name()) 79 | } 80 | Expect(list).To(Equal(params.TableExpRes)) 81 | }) 82 | It("should return the column type info for an existing table with empty schema param", func() { 83 | db, done := setup() 84 | defer done() 85 | ci, err := schema.ColumnTypes(db, "", params.TableNamesExpRes[1][1]) 86 | Expect(err).To(BeNil()) 87 | var list []string 88 | for _, c := range ci { 89 | list = append(list, c.Name()) 90 | } 91 | Expect(list).To(Equal(params.TableExpRes)) 92 | }) 93 | It("should return an error for a non-existing table", func() { 94 | db, done := setup() 95 | defer done() 96 | _, err := schema.ColumnTypes(db, "", "XXX-NO-SUCH-TABLE-XXX") 97 | Expect(err).ToNot(BeNil()) 98 | }) 99 | It("should return the column type info for the view", func() { 100 | db, done := setup() 101 | defer done() 102 | ci, err := schema.ColumnTypes(db, params.ViewNamesExpRes[0][0], params.ViewNamesExpRes[0][1]) 103 | Expect(err).To(BeNil()) 104 | var list []string 105 | for _, c := range ci { 106 | list = append(list, c.Name()) 107 | } 108 | Expect(list).To(Equal(params.ViewExpRes)) 109 | }) 110 | // TODO(js) check with empty schema param 111 | It("should return the column type info for the view with empty schema param", func() { 112 | db, done := setup() 113 | defer done() 114 | ci, err := schema.ColumnTypes(db, "", params.ViewNamesExpRes[0][1]) 115 | Expect(err).To(BeNil()) 116 | var list []string 117 | for _, c := range ci { 118 | list = append(list, c.Name()) 119 | } 120 | Expect(list).To(Equal(params.ViewExpRes)) 121 | }) 122 | }) 123 | 124 | Describe("TableNames", func() { 125 | It("should return the table names", func() { 126 | db, done := setup() 127 | defer done() 128 | sn, err := schema.TableNames(db) 129 | Expect(err).To(BeNil()) 130 | Expect(sn).To(Equal(params.TableNamesExpRes)) 131 | }) 132 | }) 133 | 134 | Describe("Tables", func() { 135 | It("should return the column type info for all tables", func() { 136 | db, done := setup() 137 | defer done() 138 | sc, err := schema.Tables(db) 139 | Expect(err).To(BeNil()) 140 | Expect(sc).To(HaveLen(2)) 141 | // TODO(js) Improve / cleanup tests. 142 | // Expect(sc).To(HaveKey()) 143 | ci, ok := sc[params.TableNamesExpRes[1]] 144 | Expect(ok).To(BeTrue()) 145 | Expect(ci).To(HaveLen(10)) 146 | }) 147 | }) 148 | 149 | Describe("ViewNames", func() { 150 | It("should return the view names", func() { 151 | db, done := setup() 152 | defer done() 153 | sn, err := schema.ViewNames(db) 154 | Expect(err).To(BeNil()) 155 | Expect(sn).To(Equal(params.ViewNamesExpRes)) 156 | }) 157 | }) 158 | 159 | Describe("Views", func() { 160 | It("should return the column type info for all views", func() { 161 | db, done := setup() 162 | defer done() 163 | sc, err := schema.Views(db) 164 | Expect(err).To(BeNil()) 165 | Expect(sc).To(HaveLen(1)) 166 | ci, ok := sc[params.ViewNamesExpRes[0]] 167 | Expect(ok).To(BeTrue()) 168 | Expect(ci).To(HaveLen(2)) 169 | }) 170 | }) 171 | 172 | Describe("PrimaryKey", func() { 173 | It("should return the primary key", func() { 174 | db, done := setup() 175 | defer done() 176 | pk, err := schema.PrimaryKey(db, params.TableNamesExpRes[0][0], params.TableNamesExpRes[0][1]) 177 | Expect(err).To(BeNil()) 178 | Expect(pk).To(Equal(params.PrimaryKeysExpRes)) 179 | }) 180 | It("should return the primary key when schema param is empty", func() { 181 | db, done := setup() 182 | defer done() 183 | pk, err := schema.PrimaryKey(db, "", params.TableNamesExpRes[0][1]) 184 | Expect(err).To(BeNil()) 185 | Expect(pk).To(Equal(params.PrimaryKeysExpRes)) 186 | }) 187 | }) 188 | 189 | } 190 | 191 | var _ = Describe("schema", func() { 192 | Context("using an unsupported (fake) db driver", func() { 193 | sql.Register("fakedb", FakeDb{}) 194 | db, _ := sql.Open("fakedb", "") 195 | 196 | It("should return errors for every method", func() { 197 | 198 | var unknownDriverErr = schema.UnknownDriverError{Driver: "schema_test.FakeDb"} 199 | 200 | ci, err := schema.ColumnTypes(db, "", "web_resource") 201 | Expect(ci).To(BeNil()) 202 | Expect(err).To(MatchError(unknownDriverErr)) 203 | 204 | tn, err := schema.TableNames(db) 205 | Expect(tn).To(BeNil()) 206 | Expect(err).To(MatchError(unknownDriverErr)) 207 | 208 | ta, err := schema.Tables(db) 209 | Expect(ta).To(BeNil()) 210 | Expect(err).To(MatchError(unknownDriverErr)) 211 | 212 | vn, err := schema.ViewNames(db) 213 | Expect(vn).To(BeNil()) 214 | Expect(err).To(MatchError(unknownDriverErr)) 215 | 216 | vw, err := schema.Views(db) 217 | Expect(vw).To(BeNil()) 218 | Expect(err).To(MatchError(unknownDriverErr)) 219 | 220 | pk, err := schema.PrimaryKey(db, "", "web_resource") 221 | Expect(pk).To(BeNil()) 222 | Expect(err).To(MatchError(unknownDriverErr)) 223 | 224 | Expect(err.Error()).To(Equal("unknown database driver: schema_test.FakeDb")) 225 | }) 226 | }) 227 | }) 228 | 229 | type FakeDb struct{} 230 | 231 | func (_ FakeDb) Open(name string) (driver.Conn, error) { 232 | return nil, nil 233 | } 234 | 235 | // pack a string, normalising its whitespace. 236 | func pack(s string) string { 237 | return strings.Join(strings.Fields(s), " ") 238 | } 239 | --------------------------------------------------------------------------------