Repository: ariga/atlas
Pull request #1439 (2023-02-02 14:26:36)

doc/md: updating getting started instruction from tokens to CI bot


Patch:
diff --git a/cmd/atlas/internal/cmdapi/cmdapi.go b/cmd/atlas/internal/cmdapi/cmdapi.go
index a88f5d42..ecec0237 100644
--- a/cmd/atlas/internal/cmdapi/cmdapi.go
+++ b/cmd/atlas/internal/cmdapi/cmdapi.go
@@ -541,7 +541,7 @@ func sqlStateReader(ctx context.Context, config *stateReaderConfig, urls []*url.
 		if err != nil {
 			return nil, err
 		}
-		dir = &migrate.MemDir{}
+		dir = &validMemDir{}
 		if err := dir.WriteFile(fi.Name(), b); err != nil {
 			return nil, err
 		}
@@ -583,6 +583,11 @@ func (sr *stateReadCloser) Close() {
 	}
 }
 
+// validMemDir will not throw an error when put into migrate.Validate.
+type validMemDir struct{ migrate.MemDir }
+
+func (d *validMemDir) Validate() error { return nil }
+
 const (
 	extHCL = ".hcl"
 	extSQL = ".sql"
diff --git a/doc/md/cloud/getting-started.md b/doc/md/cloud/getting-started.md
index 95bd3e52..f8f21c40 100644
--- a/doc/md/cloud/getting-started.md
+++ b/doc/md/cloud/getting-started.md
@@ -34,6 +34,7 @@ While logged in to one organization, you would be able to switch to another by c
 ![Switching organization](https://atlasgo.io/uploads/images/switch-org.png)
 
 ### Connecting to the Atlas GitHub action
+In order to connect to the Atlas GitHub Action, we will create a bot with permission to report CI run results. Bots are used to manage permissions and make sure dedicated actions, such as reporting CI results, are executed only by the entity with the desired permission.
 At first you will notice that your projects and CI runs pages are empty. In order to connect the organization
 to your GitHub repository, you will need to setup the Atlas GitHub action on your repository by following these steps:
 
@@ -42,15 +43,17 @@ If you already have the Atlas GitHub action set up, you may skip step 4. In step
 `ariga-token: ${{ secrets.ARIGA_TOKEN }}` to your yaml file.
 :::
 
-1. From the Settings page, generate an access token under 'Tokens'.
-2. On your GitHub repo, under the 'Settings' section, click on 'Secrets' > 'Actions' to create a new repository secret.
+1. From the Settings page, under 'Bots', click on "Create Bot" to create a new CI bot.  
+The CI bot will be used to connect your GitHub and will allow Atlas Cloud to lint your pull requests.
+2. After saving your CI bot, click on "Create Token", and generate a token for your bot.
+3. On your GitHub repo, under the 'Settings' section, click on 'Secrets' > 'Actions' to create a new repository secret.
 ![GitHub Secrets](https://release.ariga.io/images/assets/github-secrets.png)
 :::note
 If you do not see this on your GitHub repository, ask your repository owner for access or help.
 :::
-3. Name your secret (for example, ARIGA_TOKEN) and paste the generated token from step 2.
-4. Install the Atlas GitHub Action by adding a file named `.github/workflows/atlas-ci.yaml` to your repo.
-5. Based on the type of database you are using, copy the following code into the workflow definition file. Set up
+4. Name your secret (for example, ARIGA_TOKEN) and paste the generated token from step 2.
+5. Install the Atlas GitHub Action by adding a file named `.github/workflows/atlas-ci.yaml` to your repo.
+6. Based on the type of database you are using, copy the following code into the workflow definition file. Set up
 the `ariga-token` input parameter to the secret name you chose in the previous step, and ensure your
 mainline branch and migration directory path are configured correctly:
 
@@ -279,9 +282,9 @@ Under 'Settings' > 'Members', you can invite team members to your organization.
 These members will receive an email with a link to Atlas Cloud, and will be required to sign up with the same email
 in order to access the organization.
 
-### Regenerating Tokens
-It is possible to regenerate the access token, however once you do so the old token will be **invalidated**.
-When choosing to regenerate the token, you must remember to copy the new one into your GitHub project's 'Secrets'.
+### Deleting Bots and Tokens
+It is possible to delete a bot, however once you do all of its tokens will be **invalidated**.  
+If you would like to generate a new bot with a new token, make sure to update your GitHub project's 'Secrets' with the new token.
 
 :::info
 For more help, reach out to us on our [Discord server](https://discord.gg/zZ6sWVg6NT).
diff --git a/sql/migrate/dir.go b/sql/migrate/dir.go
index 787ce788..ce3f655a 100644
--- a/sql/migrate/dir.go
+++ b/sql/migrate/dir.go
@@ -217,25 +217,12 @@ type MemDir struct {
 
 // Open implements fs.FS.
 func (d *MemDir) Open(name string) (fs.File, error) {
-	var b []byte
-	switch name {
-	case HashFileName:
-		h, err := d.Checksum()
-		if err != nil {
-			return nil, err
-		}
-		if b, err = h.MarshalText(); err != nil {
-			return nil, err
-		}
-	default:
-		f, ok := d.files[name]
-		if !ok {
-			return nil, fs.ErrNotExist
-		}
-		b = f.Bytes()
+	f, ok := d.files[name]
+	if !ok {
+		return nil, fs.ErrNotExist
 	}
 	return &memFile{
-		ReadCloser: io.NopCloser(bytes.NewReader(b)),
+		ReadCloser: io.NopCloser(bytes.NewReader(f.Bytes())),
 	}, nil
 }
 
@@ -252,7 +239,9 @@ func (d *MemDir) WriteFile(name string, data []byte) error {
 func (d *MemDir) Files() ([]File, error) {
 	files := make([]File, 0, len(d.files))
 	for _, f := range d.files {
-		files = append(files, f)
+		if filepath.Ext(f.Name()) == ".sql" {
+			files = append(files, f)
+		}
 	}
 	sort.Slice(files, func(i, j int) bool {
 		return files[i].Name() < files[j].Name()
@@ -425,6 +414,11 @@ var (
 // Validate checks if the migration dir is in sync with its sum file.
 // If they don't match ErrChecksumMismatch is returned.
 func Validate(dir Dir) error {
+	// If a migration directory implements the Validate() method,
+	// it will be used to determine the validity instead.
+	if v, ok := dir.(interface{ Validate() error }); ok {
+		return v.Validate()
+	}
 	fh, err := readHashFile(dir)
 	if errors.Is(err, fs.ErrNotExist) {
 		// If there are no migration files yet this is okay.
diff --git a/sql/postgres/convert.go b/sql/postgres/convert.go
index 7b6ec0fe..d12c478e 100644
--- a/sql/postgres/convert.go
+++ b/sql/postgres/convert.go
@@ -62,7 +62,7 @@ func FormatType(t schema.Type) (string, error) {
 		}
 	case *schema.StringType:
 		switch f = strings.ToLower(t.T); f {
-		case TypeText:
+		case TypeText, typeName:
 		// CHAR(n) is alias for CHARACTER(n). If not length was
 		// specified, the definition is equivalent to CHARACTER(1).
 		case TypeChar, TypeCharacter:
@@ -149,6 +149,13 @@ func FormatType(t schema.Type) (string, error) {
 		default:
 			return "", fmt.Errorf("postgres: unsupported range type: %q", t.T)
 		}
+	case *OIDType:
+		switch f = strings.ToLower(t.T); f {
+		case typeOID, typeRegClass, typeRegCollation, typeRegConfig, typeRegDictionary, typeRegNamespace,
+			typeRegOper, typeRegOperator, typeRegProc, typeRegProcedure, typeRegRole, typeRegType:
+		default:
+			return "", fmt.Errorf("postgres: unsupported object identfier type: %q", t.T)
+		}
 	case *TextSearchType:
 		if f = strings.ToLower(t.T); f != TypeTSVector && f != TypeTSQuery {
 			return "", fmt.Errorf("postgres: unsupported text search type: %q", t.T)
@@ -203,7 +210,7 @@ func columnType(c *columnDesc) (schema.Type, error) {
 		typ = &schema.BoolType{T: t}
 	case TypeBytea:
 		typ = &schema.BinaryType{T: t}
-	case TypeCharacter, TypeChar, TypeCharVar, TypeVarChar, TypeText:
+	case TypeCharacter, TypeChar, TypeCharVar, TypeVarChar, TypeText, typeName:
 		// A `character` column without length specifier is equivalent to `character(1)`,
 		// but `varchar` without length accepts strings of any size (same as `text`).
 		typ = &schema.StringType{T: t, Size: int(c.size)}
@@ -268,6 +275,9 @@ func columnType(c *columnDesc) (schema.Type, error) {
 	case TypeInt4Range, TypeInt4MultiRange, TypeInt8Range, TypeInt8MultiRange, TypeNumRange, TypeNumMultiRange,
 		TypeTSRange, TypeTSMultiRange, TypeTSTZRange, TypeTSTZMultiRange, TypeDateRange, TypeDateMultiRange:
 		typ = &RangeType{T: t}
+	case typeOID, typeRegClass, typeRegCollation, typeRegConfig, typeRegDictionary, typeRegNamespace,
+		typeRegOper, typeRegOperator, typeRegProc, typeRegProcedure, typeRegRole, typeRegType:
+		typ = &OIDType{T: t}
 	case TypeUserDefined:
 		typ = &UserDefinedType{T: c.fmtype}
 	default:
diff --git a/sql/postgres/diff.go b/sql/postgres/diff.go
index 74fd6edf..86d8d669 100644
--- a/sql/postgres/diff.go
+++ b/sql/postgres/diff.go
@@ -236,8 +236,8 @@ func (d *diff) typeChanged(from, to *schema.Column) (bool, error) {
 	}
 	var changed bool
 	switch fromT := fromT.(type) {
-	case *schema.BinaryType, *BitType, *schema.BoolType, *schema.DecimalType, *schema.FloatType,
-		*IntervalType, *schema.IntegerType, *schema.JSONType, *RangeType, *SerialType, *schema.SpatialType,
+	case *schema.BinaryType, *BitType, *schema.BoolType, *schema.DecimalType, *schema.FloatType, *IntervalType,
+		*schema.IntegerType, *schema.JSONType, *OIDType, *RangeType, *SerialType, *schema.SpatialType,
 		*schema.StringType, *schema.TimeType, *TextSearchType, *NetworkType, *UserDefinedType, *schema.UUIDType:
 		t1, err := FormatType(toT)
 		if err != nil {
diff --git a/sql/postgres/driver.go b/sql/postgres/driver.go
index a978c416..5ad263c3 100644
--- a/sql/postgres/driver.go
+++ b/sql/postgres/driver.go
@@ -333,6 +333,7 @@ const (
 	TypeCharVar   = "character varying"
 	TypeVarChar   = "varchar" // character varying
 	TypeText      = "text"
+	typeName      = "name" // internal type for object names
 
 	TypeSmallInt = "smallint"
 	TypeInteger  = "integer"
@@ -404,6 +405,20 @@ const (
 	TypeTSTZMultiRange = "tstzmultirange"
 	TypeDateRange      = "daterange"
 	TypeDateMultiRange = "datemultirange"
+
+	// PostgreSQL internal object types and their aliases.
+	typeOID           = "oid"
+	typeRegClass      = "regclass"
+	typeRegCollation  = "regcollation"
+	typeRegConfig     = "regconfig"
+	typeRegDictionary = "regdictionary"
+	typeRegNamespace  = "regnamespace"
+	typeRegOper       = "regoper"
+	typeRegOperator   = "regoperator"
+	typeRegProc       = "regproc"
+	typeRegProcedure  = "regprocedure"
+	typeRegRole       = "regrole"
+	typeRegType       = "regtype"
 )
 
 // List of supported index types.
diff --git a/sql/postgres/inspect.go b/sql/postgres/inspect.go
index 8f74a996..ca193e38 100644
--- a/sql/postgres/inspect.go
+++ b/sql/postgres/inspect.go
@@ -741,6 +741,12 @@ type (
 	// Defined here for backward compatibility reasons.
 	UUIDType = schema.UUIDType
 
+	// OIDType defines an object identifier type.
+	OIDType struct {
+		schema.Type
+		T string
+	}
+
 	// A XMLType defines an XML type.
 	XMLType struct {
 		schema.Type
diff --git a/sql/postgres/sqlspec.go b/sql/postgres/sqlspec.go
index 3366fc87..a7d3fb31 100644
--- a/sql/postgres/sqlspec.go
+++ b/sql/postgres/sqlspec.go
@@ -734,6 +734,16 @@ var TypeRegistry = schemahcl.NewRegistry(
 		schemahcl.NewTypeSpec("hstore"),
 		schemahcl.NewTypeSpec("sql", schemahcl.WithAttributes(&schemahcl.TypeAttr{Name: "def", Required: true, Kind: reflect.String})),
 	),
+	// PostgreSQL internal and special types.
+	schemahcl.WithSpecs(func() (specs []*schemahcl.TypeSpec) {
+		for _, t := range []string{
+			typeOID, typeRegClass, typeRegCollation, typeRegConfig, typeRegDictionary, typeRegNamespace,
+			typeName, typeRegOper, typeRegOperator, typeRegProc, typeRegProcedure, typeRegRole, typeRegType,
+		} {
+			specs = append(specs, schemahcl.NewTypeSpec(t))
+		}
+		return specs
+	}()...),
 	schemahcl.WithSpecs(func() (specs []*schemahcl.TypeSpec) {
 		opts := []schemahcl.TypeSpecOption{
 			schemahcl.WithToSpec(func(t schema.Type) (*schemahcl.Type, error) {


Test patch:
diff --git a/cmd/atlas/internal/cmdapi/schema_test.go b/cmd/atlas/internal/cmdapi/schema_test.go
index 6d3129c8..0c064b45 100644
--- a/cmd/atlas/internal/cmdapi/schema_test.go
+++ b/cmd/atlas/internal/cmdapi/schema_test.go
@@ -412,7 +412,7 @@ func TestSchema_ApplyLog(t *testing.T) {
 	})
 }
 
-func TesSchema_ApplySources(t *testing.T) {
+func TestSchema_ApplySources(t *testing.T) {
 	var (
 		p   = t.TempDir()
 		cfg = filepath.Join(p, "atlas.hcl")
@@ -458,9 +458,9 @@ env "local" {
 	require.Equal(t, []string{
 		"-- Planned Changes:",
 		`-- Create "one" table`,
-		"CREATE TABLE `one` (`id` int NOT NULL)",
+		"CREATE TABLE `one` (`id` int NOT NULL);",
 		`-- Create "two" table`,
-		"CREATE TABLE `two` (`id` int NOT NULL)",
+		"CREATE TABLE `two` (`id` int NOT NULL);",
 	}, lines)
 }
 
diff --git a/sql/migrate/dir_test.go b/sql/migrate/dir_test.go
index 7cb5de1c..19654a23 100644
--- a/sql/migrate/dir_test.go
+++ b/sql/migrate/dir_test.go
@@ -214,6 +214,14 @@ func TestMemDir(t *testing.T) {
 	hs2, err := migrate.NewHashFile(files)
 	require.NoError(t, err)
 	require.Equal(t, hs1, hs2)
+
+	// Will fail without checksum file.
+	require.ErrorIs(t, migrate.Validate(&d), migrate.ErrChecksumNotFound)
+
+	// Will not return the non-sql checksum file.
+	files, err = d.Files()
+	require.NoError(t, err)
+	require.Len(t, files, 1) // 1.sql
 }
 
 func TestLocalFile_Directive(t *testing.T) {
diff --git a/sql/postgres/sqlspec_test.go b/sql/postgres/sqlspec_test.go
index 0c0189bb..21d82d26 100644
--- a/sql/postgres/sqlspec_test.go
+++ b/sql/postgres/sqlspec_test.go
@@ -1518,6 +1518,18 @@ func TestTypes(t *testing.T) {
 			typeExpr: `sql("varchar(2) [ 2 ] [  ]")`,
 			expected: &ArrayType{Type: &schema.StringType{T: "varchar", Size: 2}, T: "varchar(2)[]"},
 		},
+		{
+			typeExpr: "oid",
+			expected: &OIDType{T: typeOID},
+		},
+		{
+			typeExpr: "regclass",
+			expected: &OIDType{T: typeRegClass},
+		},
+		{
+			typeExpr: "name",
+			expected: &schema.StringType{T: typeName},
+		},
 	} {
 		t.Run(tt.typeExpr, func(t *testing.T) {
 			var test schema.Schema