2018-06-24 16:40:48 +03:00
package migrate
import (
"fmt"
nurl "net/url"
2021-01-18 20:11:05 +03:00
"os"
"path/filepath"
2020-04-07 12:23:20 +03:00
"runtime"
2018-06-24 16:40:48 +03:00
"strings"
2020-04-07 12:23:20 +03:00
2022-11-23 20:13:52 +03:00
"github.com/hasura/graphql-engine/cli/v2"
"github.com/hasura/graphql-engine/cli/v2/internal/errors"
"github.com/hasura/graphql-engine/cli/v2/internal/hasura"
2021-06-22 11:03:58 +03:00
"github.com/hasura/graphql-engine/cli/v2/internal/scripts"
"github.com/hasura/graphql-engine/cli/v2/internal/statestore"
"github.com/hasura/graphql-engine/cli/v2/internal/statestore/migrations"
2021-06-16 14:44:15 +03:00
migratedb "github.com/hasura/graphql-engine/cli/v2/migrate/database"
2018-06-24 16:40:48 +03:00
)
// MultiError holds multiple errors.
type MultiError struct {
Errs [ ] error
}
// NewMultiError returns an error type holding multiple errors.
func NewMultiError ( errs ... error ) MultiError {
compactErrs := make ( [ ] error , 0 )
for _ , e := range errs {
if e != nil {
compactErrs = append ( compactErrs , e )
}
}
return MultiError { compactErrs }
}
// Error implements error. Mulitple errors are concatenated with 'and's.
func ( m MultiError ) Error ( ) string {
var strs = make ( [ ] string , 0 )
for _ , e := range m . Errs {
if len ( e . Error ( ) ) > 0 {
strs = append ( strs , e . Error ( ) )
}
}
return strings . Join ( strs , " and " )
}
// suint64 safely converts int to uint64
// see https://goo.gl/wEcqof
// see https://goo.gl/pai7Dr
func suint64 ( n int64 ) uint64 {
if n < 0 {
panic ( fmt . Sprintf ( "suint(%v) expects input >= 0" , n ) )
}
return uint64 ( n )
}
2020-11-18 16:35:25 +03:00
/ *
2018-06-24 16:40:48 +03:00
// newSlowReader turns an io.ReadCloser into a slow io.ReadCloser.
// Use this to simulate a slow internet connection.
func newSlowReader ( r io . ReadCloser ) io . ReadCloser {
return & slowReader {
rx : r ,
reader : bufio . NewReader ( r ) ,
}
}
type slowReader struct {
rx io . ReadCloser
reader * bufio . Reader
}
func ( b * slowReader ) Read ( p [ ] byte ) ( n int , err error ) {
time . Sleep ( 10 * time . Millisecond )
c , err := b . reader . ReadByte ( )
if err != nil {
return 0 , err
} else {
copy ( p , [ ] byte { c } )
return 1 , nil
}
}
func ( b * slowReader ) Close ( ) error {
return b . rx . Close ( )
2020-10-29 10:08:12 +03:00
} * /
2018-06-24 16:40:48 +03:00
var errNoScheme = fmt . Errorf ( "no scheme" )
// schemeFromUrl returns the scheme from a URL string
func schemeFromUrl ( url string ) ( string , error ) {
2022-11-23 20:13:52 +03:00
var op errors . Op = "migrate.schemeFromUrl"
2018-06-24 16:40:48 +03:00
u , err := nurl . Parse ( url )
if err != nil {
2022-11-23 20:13:52 +03:00
return "" , errors . E ( op , err )
2018-06-24 16:40:48 +03:00
}
if len ( u . Scheme ) == 0 {
2022-11-23 20:13:52 +03:00
return "" , errors . E ( op , errNoScheme )
2018-06-24 16:40:48 +03:00
}
return u . Scheme , nil
}
// FilterCustomQuery filters all query values starting with `x-`
func FilterCustomQuery ( u * nurl . URL ) * nurl . URL {
ux := * u
vx := make ( nurl . Values )
for k , v := range ux . Query ( ) {
if len ( k ) <= 1 || ( len ( k ) > 1 && k [ 0 : 2 ] != "x-" ) {
vx [ k ] = v
}
}
ux . RawQuery = vx . Encode ( )
return & ux
}
2020-04-07 12:23:20 +03:00
2021-03-08 14:59:35 +03:00
func NewMigrate ( ec * cli . ExecutionContext , isCmd bool , sourceName string , sourceKind hasura . SourceKind ) ( * Migrate , error ) {
2022-11-23 20:13:52 +03:00
var op errors . Op = "migrate.NewMigrate"
2021-03-08 14:59:35 +03:00
// set a default source kind
if len ( sourceKind ) < 1 {
2022-11-23 20:13:52 +03:00
return nil , errors . E ( op , fmt . Errorf ( "invalid source kind" ) )
2021-03-08 14:59:35 +03:00
}
2021-04-19 10:05:21 +03:00
// create a new directory for the database if it doesn't exists
2021-03-08 14:59:35 +03:00
if f , _ := os . Stat ( filepath . Join ( ec . MigrationDir , sourceName ) ) ; f == nil {
err := os . MkdirAll ( filepath . Join ( ec . MigrationDir , sourceName ) , 0755 )
2021-01-18 20:11:05 +03:00
if err != nil {
2022-11-23 20:13:52 +03:00
return nil , errors . E ( op , err )
2021-01-18 20:11:05 +03:00
}
}
2020-04-08 13:59:21 +03:00
dbURL := GetDataPath ( ec )
2021-03-08 14:59:35 +03:00
fileURL := GetFilePath ( filepath . Join ( ec . MigrationDir , sourceName ) )
2021-01-18 20:11:05 +03:00
opts := NewMigrateOpts {
fileURL . String ( ) ,
dbURL . String ( ) ,
isCmd , int ( ec . Config . Version ) ,
ec . Logger ,
2021-08-31 12:56:31 +03:00
ec . Stdout ,
ec . Stderr ,
2021-02-17 15:51:43 +03:00
& migratedb . HasuraOpts {
2021-02-18 08:36:50 +03:00
HasMetadataV3 : ec . HasMetadataV3 ,
2021-03-08 14:59:35 +03:00
SourceName : sourceName ,
SourceKind : sourceKind ,
2021-02-18 08:36:50 +03:00
Client : ec . APIClient ,
V2MetadataOps : func ( ) hasura . V2CommonMetadataOperations {
if ec . Config . Version >= cli . V3 {
return ec . APIClient . V1Metadata
}
return nil
} ( ) ,
2021-02-17 07:20:19 +03:00
MetadataOps : cli . GetCommonMetadataOps ( ec ) ,
MigrationsStateStore : cli . GetMigrationsStateStore ( ec ) ,
2021-05-28 09:04:36 +03:00
SettingsStateStore : cli . GetSettingsStateStore ( ec , sourceName ) ,
2021-01-18 20:11:05 +03:00
} ,
}
2022-02-04 14:10:33 +03:00
opts . hasuraOpts . PGDumpClient = ec . APIClient . PGDump
2021-03-08 14:59:35 +03:00
if ec . HasMetadataV3 {
opts . hasuraOpts . PGSourceOps = ec . APIClient . V2Query
opts . hasuraOpts . MSSQLSourceOps = ec . APIClient . V2Query
2021-06-21 17:34:10 +03:00
opts . hasuraOpts . CitusSourceOps = ec . APIClient . V2Query
2021-03-08 14:59:35 +03:00
opts . hasuraOpts . GenericQueryRequest = ec . APIClient . V2Query . Send
} else {
opts . hasuraOpts . PGSourceOps = ec . APIClient . V1Query
opts . hasuraOpts . GenericQueryRequest = ec . APIClient . V1Query . Send
}
2021-01-18 20:11:05 +03:00
t , err := New ( opts )
2020-04-07 12:23:20 +03:00
if err != nil {
2022-11-23 20:13:52 +03:00
return nil , errors . E ( op , fmt . Errorf ( "cannot create migrate instance: %w" , err ) )
2020-04-07 12:23:20 +03:00
}
2021-01-18 20:11:05 +03:00
if ec . Config . Version >= cli . V2 {
2021-04-01 08:13:24 +03:00
t . databaseDrv . EnableCheckMetadataConsistency ( true )
2020-04-22 13:22:02 +03:00
}
2021-05-17 18:19:15 +03:00
if ok , err := copyStateToCatalogStateAPIIfRequired ( ec , sourceName ) ; err != nil {
ec . Logger . Warn ( err )
} else if ok {
if err := t . ReScan ( ) ; err != nil {
2022-11-23 20:13:52 +03:00
return nil , errors . E ( op , err )
2021-05-17 18:19:15 +03:00
}
}
2020-04-07 12:23:20 +03:00
return t , nil
}
2020-04-08 13:59:21 +03:00
func GetDataPath ( ec * cli . ExecutionContext ) * nurl . URL {
url := ec . Config . ServerConfig . ParsedEndpoint
2020-04-07 12:23:20 +03:00
host := & nurl . URL {
2020-04-08 13:59:21 +03:00
Scheme : "hasuradb" ,
Host : url . Host ,
Path : url . Path ,
RawQuery : ec . Config . ServerConfig . APIPaths . GetQueryParams ( ) . Encode ( ) ,
2020-04-07 12:23:20 +03:00
}
2020-04-08 13:59:21 +03:00
q := host . Query ( )
2020-04-07 12:23:20 +03:00
// Set sslmode in query
switch scheme := url . Scheme ; scheme {
case "https" :
q . Set ( "sslmode" , "enable" )
default :
q . Set ( "sslmode" , "disable" )
}
2020-04-09 12:30:47 +03:00
for k , v := range ec . HGEHeaders {
q . Add ( "headers" , fmt . Sprintf ( "%s:%s" , k , v ) )
2020-04-07 12:23:20 +03:00
}
host . RawQuery = q . Encode ( )
return host
}
func GetFilePath ( dir string ) * nurl . URL {
host := & nurl . URL {
Scheme : "file" ,
Path : dir ,
}
// Add Prefix / to path if runtime.GOOS equals to windows
if runtime . GOOS == "windows" && ! strings . HasPrefix ( host . Path , "/" ) {
host . Path = "/" + host . Path
}
return host
}
2021-04-19 10:05:21 +03:00
func IsMigrationsSupported ( kind hasura . SourceKind ) bool {
switch kind {
2023-01-19 13:49:47 +03:00
case hasura . SourceKindMSSQL , hasura . SourceKindPG , hasura . SourceKindCitus , hasura . SourceKindCockroach :
2021-04-19 10:05:21 +03:00
return true
}
return false
}
2021-05-17 18:19:15 +03:00
func copyStateToCatalogStateAPIIfRequired ( ec * cli . ExecutionContext , sourceName string ) ( bool , error ) {
2022-11-23 20:13:52 +03:00
var op errors . Op = "migrate.copyStateToCatalogStateAPIIfRequired"
2021-05-17 18:19:15 +03:00
// if
// the project is in config v3
// isStateCopyCompleted is false in catalog state
// hdb_catalog.schema_migrations is not empty
2021-05-28 09:04:36 +03:00
if ! ec . DisableAutoStateMigration && ec . Config . Version >= cli . V3 {
2021-05-17 18:19:15 +03:00
// get cli catalog and check isStateCopyCompleted is false
cs := statestore . NewCLICatalogState ( ec . APIClient . V1Metadata )
state , err := cs . Get ( )
if err != nil {
2022-11-23 20:13:52 +03:00
return false , errors . E ( op , err )
2021-05-17 18:19:15 +03:00
}
markStateMigrationCompleted := func ( ) error {
state . IsStateCopyCompleted = true
if _ , err := cs . Set ( * state ) ; err != nil {
2022-11-23 20:13:52 +03:00
return errors . E ( op , fmt . Errorf ( "error settting state: %w" , err ) )
2021-05-17 18:19:15 +03:00
}
return nil
}
if ! state . IsStateCopyCompleted {
// if control reaches this block we'll set IsStateCopyCompleted to true
// this makes sure we only attempt to automatically do the state migration once
// we'll leave it up to the user to correct the errors and use
// scripts update-project-v3 --move-state-only to move state
//
// this will also make sure new config v3 projects will not repeatedly reach this block
// for a example a user connecting a custom source named default
// with no read permissions to other schemas ie we cannot access `hdb_catalog.schema_migrations`
// in the first run it'll encounter an error but will also mark IsStateCopyCompleted to true
// thereby not running this block again
// check if hdb_catalog.schema_migrations exists
// check if migrations state table exists
query := hasura . PGRunSQLInput {
2021-06-16 14:44:15 +03:00
Source : sourceName ,
SQL : ` SELECT COUNT(1) FROM information_schema.tables WHERE table_name = ' ` + migrations . DefaultMigrationsTable + ` ' AND table_schema = ' ` + migrations . DefaultSchema + ` ' LIMIT 1 ` ,
2021-05-17 18:19:15 +03:00
}
runsqlResp , err := ec . APIClient . V2Query . PGRunSQL ( query )
if err != nil {
2021-09-29 20:46:19 +03:00
ec . Logger . Debug ( "encountered error when trying to move migrations from hdb_catalog.schema_migrations to catalog state\n" , err ,
2021-05-17 18:19:15 +03:00
"\nnote: ignore this if you are not updating your project from config v2 -> config v3" )
ec . Logger . Debug ( "marking IsStateCopyCompleted as true %w" , markStateMigrationCompleted ( ) )
return false , nil
}
if runsqlResp . ResultType != hasura . TuplesOK {
2021-06-22 11:03:58 +03:00
ec . Logger . Debug ( "encountered error when trying to move migrations from hdb_catalog.schema_migrations to catalog state" , fmt . Errorf ( "invalid result Type %s" , runsqlResp . ResultType ) ,
2021-05-17 18:19:15 +03:00
"\nnote: ignore this if you are not updating your project from config v2 -> config v3" )
ec . Logger . Debug ( "marking IsStateCopyCompleted as true %w" , markStateMigrationCompleted ( ) )
return false , nil
}
result := runsqlResp . Result
if result [ 1 ] [ 0 ] == "0" {
// hdb_catalog.schema_migrations doesn't exist
ec . Logger . Debug ( "hdb_catalog.schema_migrations was not found, skipping state migration" )
ec . Logger . Debug ( "marking IsStateCopyCompleted as true %w" , markStateMigrationCompleted ( ) )
return false , nil
}
ec . Logger . Debug ( "copying cli state from hdb_catalog.schema_migrations to catalog state" )
// COPY STATE
2021-05-28 09:04:36 +03:00
if err := scripts . CopyState ( ec , sourceName , sourceName ) ; err != nil {
2022-11-23 20:13:52 +03:00
return false , errors . E ( op , err )
2021-05-17 18:19:15 +03:00
}
ec . Logger . Debug ( "copying cli state from hdb_catalog.schema_migrations to catalog state success" )
return true , nil
}
ec . Logger . Debugf ( "skipping state migration, found IsStateCopyCompleted: %v Migrations: %v" , state . IsStateCopyCompleted , state . Migrations )
return false , nil
}
return false , nil
}