├── .github └── workflows │ └── ci.yaml ├── .golangci.yml ├── LICENSE ├── README.md ├── cmd └── entimport │ └── entimport.go ├── go.mod ├── go.sum └── internal ├── entimport ├── import.go ├── import_test.go ├── mysql.go ├── mysql_test.go ├── postgres.go └── postgres_test.go ├── integration ├── README.md ├── compose │ └── docker-compose.yaml └── integration_test.go └── mux ├── mux.go └── provider.go /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | jobs: 8 | lintgo: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-go@v2 13 | with: 14 | go-version: 1.17 15 | - name: Run Go linters 16 | uses: golangci/golangci-lint-action@v3.1.0 17 | with: 18 | version: v1.45.2 19 | args: --verbose --enable whitespace,gocritic,goimports 20 | unit: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v2.3.4 24 | - uses: actions/setup-go@v2 25 | with: 26 | go-version: 1.17 27 | - uses: actions/cache@v2.1.6 28 | with: 29 | path: | 30 | ~/.cache/go-build 31 | ~/go/pkg/mod 32 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 33 | restore-keys: | 34 | ${{ runner.os }}-go- 35 | - name: Run tests 36 | run: go test -race . 37 | working-directory: internal/entimport 38 | 39 | integration: 40 | runs-on: ubuntu-latest 41 | services: 42 | mysql: 43 | image: mysql 44 | env: 45 | MYSQL_DATABASE: test 46 | MYSQL_ROOT_PASSWORD: pass 47 | ports: 48 | - 3306:3306 49 | options: >- 50 | --health-cmd "mysqladmin ping -ppass" 51 | --health-interval 10s 52 | --health-start-period 10s 53 | --health-timeout 5s 54 | --health-retries 10 55 | postgres13: 56 | image: postgres:13 57 | env: 58 | POSTGRES_DB: test 59 | POSTGRES_PASSWORD: pass 60 | ports: 61 | - 5432:5432 62 | options: >- 63 | --health-cmd pg_isready 64 | --health-interval 10s 65 | --health-timeout 5s 66 | --health-retries 5 67 | steps: 68 | - uses: actions/checkout@v2.3.4 69 | - uses: actions/setup-go@v2 70 | with: 71 | go-version: '1.17' 72 | - uses: actions/cache@v2.1.6 73 | with: 74 | path: | 75 | ~/.cache/go-build 76 | ~/go/pkg/mod 77 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 78 | restore-keys: | 79 | ${{ runner.os }}-go- 80 | - name: Run integration tests 81 | working-directory: internal/integration 82 | run: go test -race . -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | goimports: 3 | # place these last 4 | local-prefixes: golang.org,entgo.io,github.com 5 | 6 | run: 7 | go: '1.20' 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Ariga Technologies LTD 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # entimport 2 | 3 | `entimport` is a tool for creating [Ent](https://entgo.io/) schemas from existing SQL databases. Currently, `MySQL` 4 | and `PostgreSQL` are supported. The tool can import to [ent schema](https://entgo.io/docs/schema-def) any number of 5 | tables, including relations between them. 6 | 7 | ## Installation 8 | 9 | ### Setup A Go Environment 10 | 11 | If your project directory is outside [GOPATH](https://github.com/golang/go/wiki/GOPATH) or you are not familiar with 12 | GOPATH, setup a [Go module](https://github.com/golang/go/wiki/Modules#quick-start) project as follows: 13 | 14 | ```shell 15 | go mod init 16 | ``` 17 | 18 | ### Install ent 19 | 20 | ```shell 21 | go install entgo.io/ent/cmd/ent 22 | ``` 23 | 24 | After installing `ent` codegen tool, you should have it in your `PATH`. If you don't find it your path, you can also 25 | run: `go run entgo.io/ent/cmd/ent ` 26 | 27 | ### Create Schema Directory 28 | 29 | Go to the root directory of your project, and run: 30 | 31 | ```shell 32 | ent init 33 | ``` 34 | 35 | The command above will create `/ent/schema/` directory and the file inside `/ent/generate.go` 36 | 37 | ### Importing a Schema 38 | 39 | Installing and running `entimport` 40 | 41 | ```shell 42 | go run ariga.io/entimport/cmd/entimport 43 | ``` 44 | 45 | - For example, importing a MySQL schema with `users` table: 46 | 47 | ```shell 48 | go run ariga.io/entimport/cmd/entimport -dsn "mysql://root:pass@tcp(localhost:3308)/test" -tables "users" 49 | ``` 50 | 51 | The command above will write a valid ent schema into the directory specified (or the default `./ent/schema`): 52 | 53 | ``` 54 | . 55 | ├── generate.go 56 | └── schema 57 | └── user.go 58 | 59 | 1 directory, 2 files 60 | ``` 61 | 62 | ### Code Generation: 63 | 64 | In order to [generate](https://entgo.io/docs/code-gen) `ent` files from the produced schemas, run: 65 | 66 | ```shell 67 | go run -mod=mod entgo.io/ent/cmd/ent generate ./schema 68 | 69 | # OR `ent` init: 70 | 71 | go generate ./ent 72 | ``` 73 | 74 | If you are not yet familiar with `ent`, you can also follow 75 | the [quick start guide](https://entgo.io/docs/getting-started). 76 | 77 | ## Usage 78 | 79 | ```shell 80 | entimport -h 81 | ``` 82 | 83 | ``` 84 | Usage of ./entimport: 85 | -dsn string 86 | data source name (connection information), for example: 87 | "mysql://user:pass@tcp(localhost:3306)/dbname" 88 | "postgres://user:pass@host:port/dbname" 89 | -schema-path string 90 | output path for ent schema (default "./ent/schema") 91 | -tables value 92 | comma-separated list of tables to inspect (all if empty) 93 | ``` 94 | 95 | ## Examples: 96 | 97 | 1. Import ent schema from Postgres database 98 | 99 | > Note: add search_path=foo if you use non `public` schema. 100 | 101 | ```shell 102 | go run ariga.io/entimport/cmd/entimport -dsn "postgres://postgres:pass@localhost:5432/test?sslmode=disable" 103 | ``` 104 | 105 | 2. Import ent schema from MySQL database 106 | 107 | ```shell 108 | go run ariga.io/entimport/cmd/entimport -dsn "mysql://root:pass@tcp(localhost:3308)/test" 109 | ``` 110 | 111 | 3. Import only specific tables: 112 | 113 | > Note: When importing specific tables: 114 | > if the table is a join table, you must also provide referenced tables. 115 | > If the table is only one part of a relation, the other part won't be imported unless specified. 116 | > If the `-tables` flags is omitted all tables in current `database schema` will be imported 117 | 118 | ```shell 119 | go run ariga.io/entimport/cmd/entimport -dsn "..." -tables "users,user_friends" 120 | ``` 121 | 122 | 4. Import to another directory: 123 | 124 | ```shell 125 | go run ariga.io/entimport/cmd/entimport -dsn "..." -schema-path "/some/path/here" 126 | ``` 127 | 128 | ## Future Work 129 | 130 | - Index support (currently Unique index is supported). 131 | - Support for all data types (for example `uuid` in Postgres). 132 | - Support for Default value in columns. 133 | - Support for editing schema both manually and automatically (real upsert and not only overwrite) 134 | - Postgres special types: postgres.NetworkType, postgres.BitType, *schema.SpatialType, postgres.CurrencyType, 135 | postgres.XMLType, postgres.ArrayType, postgres.UserDefinedType. 136 | 137 | ### Known Caveats: 138 | 139 | - Schema files are overwritten by new calls to `entimport`. 140 | - There is no difference in DB schema between `O2O Bidirectional` and `O2O Same Type` - both will result in the same 141 | `ent` schema. 142 | - There is no difference in DB schema between `M2M Bidirectional` and `M2M Same Type` - both will result in the same 143 | `ent` schema. 144 | - In recursive relations the `edge` names will be prefixed with `child_` & `parent_`. 145 | - For example: `users` with M2M relation to itself will result in: 146 | 147 | ```go 148 | func (User) Edges() []ent.Edge { 149 | return []ent.Edge{edge.To("child_users", User.Type), edge.From("parent_users", User.Type)} 150 | } 151 | ``` 152 | 153 | ## Feedback & Support 154 | 155 | For discussion and support, [open an issue](https://github.com/ariga/entimport/issues/new/choose) or join 156 | our [channel](https://gophers.slack.com/archives/C01FMSQDT53) in the gophers Slack. 157 | -------------------------------------------------------------------------------- /cmd/entimport/entimport.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | "strings" 10 | 11 | "ariga.io/entimport/internal/entimport" 12 | "ariga.io/entimport/internal/mux" 13 | ) 14 | 15 | var ( 16 | tablesFlag tables 17 | excludeTablesFlag tables 18 | ) 19 | 20 | func init() { 21 | flag.Var(&tablesFlag, "tables", "comma-separated list of tables to inspect (all if empty)") 22 | flag.Var(&excludeTablesFlag, "exclude-tables", "comma-separated list of tables to exclude") 23 | } 24 | 25 | func main() { 26 | dsn := flag.String("dsn", "", 27 | `data source name (connection information), for example: 28 | "mysql://user:pass@tcp(localhost:3306)/dbname" 29 | "postgres://user:pass@host:port/dbname"`) 30 | schemaPath := flag.String("schema-path", "./ent/schema", "output path for ent schema") 31 | flag.Parse() 32 | if *dsn == "" { 33 | log.Println("entimport: data source name (dsn) must be provided") 34 | flag.Usage() 35 | os.Exit(2) 36 | } 37 | ctx := context.Background() 38 | drv, err := mux.Default.OpenImport(*dsn) 39 | if err != nil { 40 | log.Fatalf("entimport: failed to create import driver - %v", err) 41 | } 42 | i, err := entimport.NewImport( 43 | entimport.WithTables(tablesFlag), 44 | entimport.WithExcludedTables(excludeTablesFlag), 45 | entimport.WithDriver(drv), 46 | ) 47 | if err != nil { 48 | log.Fatalf("entimport: create importer failed: %v", err) 49 | } 50 | mutations, err := i.SchemaMutations(ctx) 51 | if err != nil { 52 | log.Fatalf("entimport: schema import failed - %v", err) 53 | } 54 | if err = entimport.WriteSchema(mutations, entimport.WithSchemaPath(*schemaPath)); err != nil { 55 | log.Fatalf("entimport: schema writing failed - %v", err) 56 | } 57 | } 58 | 59 | type tables []string 60 | 61 | func (t *tables) String() string { 62 | return fmt.Sprint(*t) 63 | } 64 | 65 | func (t *tables) Set(s string) error { 66 | *t = strings.Split(s, ",") 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module ariga.io/entimport 2 | 3 | go 1.20 4 | 5 | require ( 6 | ariga.io/atlas v0.3.8-0.20220314111236-b2171e04c5b2 7 | entgo.io/contrib v0.2.1-0.20220405071655-7dbe27ee8fec 8 | entgo.io/ent v0.10.2-0.20220321093754-edd968490ea2 9 | github.com/go-openapi/inflect v0.19.0 10 | github.com/go-sql-driver/mysql v1.6.0 11 | github.com/google/uuid v1.3.0 12 | github.com/lib/pq v1.10.4 13 | github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942 14 | ) 15 | 16 | require ( 17 | github.com/agext/levenshtein v1.2.1 // indirect 18 | github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect 19 | github.com/davecgh/go-spew v1.1.1 // indirect 20 | github.com/golang/protobuf v1.5.2 // indirect 21 | github.com/google/go-cmp v0.5.6 // indirect 22 | github.com/hashicorp/hcl/v2 v2.10.0 // indirect 23 | github.com/jhump/protoreflect v1.10.1 // indirect 24 | github.com/kr/text v0.2.0 // indirect 25 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect 26 | github.com/mitchellh/mapstructure v1.4.3 // indirect 27 | github.com/pmezard/go-difflib v1.0.0 // indirect 28 | github.com/sergi/go-diff v1.1.0 // indirect 29 | github.com/stretchr/objx v0.3.0 // indirect 30 | github.com/zclconf/go-cty v1.8.0 // indirect 31 | go.uber.org/atomic v1.7.0 // indirect 32 | go.uber.org/multierr v1.7.0 // indirect 33 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect 34 | golang.org/x/sys v0.0.0-20211205182925-97ca703d548d // indirect 35 | golang.org/x/text v0.3.7 // indirect 36 | golang.org/x/tools v0.1.10 // indirect 37 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 38 | google.golang.org/protobuf v1.27.1 // indirect 39 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 40 | ) 41 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | ariga.io/atlas v0.3.8-0.20220314111236-b2171e04c5b2 h1:qbH+CDPAMsV1FIKkHGYzy2aWP9k5QAqPbi9PYZGqz60= 2 | ariga.io/atlas v0.3.8-0.20220314111236-b2171e04c5b2/go.mod h1:ipw7dUlFanAylr9nvs8lCvOUC8hFG6PGd/gtr+uJMvk= 3 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 4 | entgo.io/contrib v0.2.1-0.20220405071655-7dbe27ee8fec h1:2tglVwq2CdtCyiyIpQWepaFryHKNzksvP2dEaOKNGhM= 5 | entgo.io/contrib v0.2.1-0.20220405071655-7dbe27ee8fec/go.mod h1:sQnSIhUoDHuXamN7gUYpJkscmszTNe07qaFmqHSzlA8= 6 | entgo.io/ent v0.10.2-0.20220321093754-edd968490ea2 h1:7Q6cHQaXfWU2EC7efffZJ6wd8s1GNLZ8Vi2eg/wXkgs= 7 | entgo.io/ent v0.10.2-0.20220321093754-edd968490ea2/go.mod h1:o83ze2N538zx2fCa9ZEKFD1V1qGVAOblVssnzciVBiI= 8 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 9 | github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= 10 | github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= 11 | github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= 12 | github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= 13 | github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= 14 | github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= 15 | github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= 16 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 17 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 18 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 19 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 21 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 22 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 23 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 24 | github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4= 25 | github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= 26 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 27 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 28 | github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= 29 | github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 30 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 31 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 32 | github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 33 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 34 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 35 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 36 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 37 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 38 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 39 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 40 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 41 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 42 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 43 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 44 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 45 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 46 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 47 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 48 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 49 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 50 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 51 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 52 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 53 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 54 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 55 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 56 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 57 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 58 | github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= 59 | github.com/hashicorp/hcl/v2 v2.10.0 h1:1S1UnuhDGlv3gRFV4+0EdwB+znNP5HmcGbIqwnSCByg= 60 | github.com/hashicorp/hcl/v2 v2.10.0/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg= 61 | github.com/jhump/protoreflect v1.10.1 h1:iH+UZfsbRE6vpyZH7asAjTPWJf7RJbpZ9j/N3lDlKs0= 62 | github.com/jhump/protoreflect v1.10.1/go.mod h1:7GcYQDdMU/O/BBrl/cX6PNHpXh6cenjd8pneu5yW7Tg= 63 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 64 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 65 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 66 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 67 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 68 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 69 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 70 | github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= 71 | github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= 72 | github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= 73 | github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 74 | github.com/mattn/go-sqlite3 v1.14.10 h1:MLn+5bFRlWMGoSRmJour3CL1w/qL96mvipqpwQW/Sfk= 75 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= 76 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 77 | github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= 78 | github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 79 | github.com/nishanths/predeclared v0.0.0-20200524104333-86fad755b4d3/go.mod h1:nt3d53pc1VYcphSCIaYAJtnPYnr3Zyn8fMq2wvPGPso= 80 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 81 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 82 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 83 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 84 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 85 | github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= 86 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 87 | github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 88 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 89 | github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As= 90 | github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 91 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 92 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 93 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 94 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 95 | github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942 h1:t0lM6y/M5IiUZyvbBTcngso8SZEZICH7is9B6g/obVU= 96 | github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 97 | github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= 98 | github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= 99 | github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= 100 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 101 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 102 | github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= 103 | github.com/zclconf/go-cty v1.8.0 h1:s4AvqaeQzJIu3ndv4gVIhplVD0krU+bgrcLSVUnaWuA= 104 | github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= 105 | github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= 106 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 107 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 108 | go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec= 109 | go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= 110 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 111 | golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 112 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 113 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 114 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 115 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 116 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 117 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 118 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 119 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 120 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 121 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 122 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= 123 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= 124 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 125 | golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 126 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 127 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 128 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 129 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 130 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 131 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 132 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 133 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 134 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 135 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY= 136 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 137 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 138 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 139 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 140 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 141 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 142 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 143 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 144 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 145 | golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 146 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 147 | golang.org/x/sys v0.0.0-20211205182925-97ca703d548d h1:FjkYO/PPp4Wi0EAUOVLxePm7qVW4r4ctbWpURyuOD0E= 148 | golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 149 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 150 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 151 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 152 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 153 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 154 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 155 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 156 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 157 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 158 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 159 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 160 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 161 | golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 162 | golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 163 | golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= 164 | golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= 165 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 166 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 167 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 168 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 169 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 170 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 171 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 172 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 173 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 174 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 175 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 176 | google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa h1:I0YcKz0I7OAhddo7ya8kMnvprhcWM045PmkBdMO9zN0= 177 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 178 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 179 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 180 | google.golang.org/grpc v1.42.0 h1:XT2/MFpuPFsEX2fWh3YQtHkZ+WYZFQRfaUgLZYj/p6A= 181 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 182 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 183 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 184 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 185 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 186 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 187 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 188 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 189 | google.golang.org/protobuf v1.25.1-0.20200805231151-a709e31e5d12/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 190 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 191 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 192 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 193 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 194 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 195 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 196 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 197 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 198 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 199 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 200 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 201 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 202 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 203 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 204 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 205 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 206 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 207 | -------------------------------------------------------------------------------- /internal/entimport/import.go: -------------------------------------------------------------------------------- 1 | package entimport 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "ariga.io/atlas/sql/schema" 9 | "ariga.io/entimport/internal/mux" 10 | 11 | "entgo.io/contrib/schemast" 12 | "entgo.io/ent" 13 | "entgo.io/ent/dialect" 14 | "entgo.io/ent/dialect/entsql" 15 | entschema "entgo.io/ent/schema" 16 | "entgo.io/ent/schema/edge" 17 | "github.com/go-openapi/inflect" 18 | ) 19 | 20 | const ( 21 | header = "Code generated " + "by entimport, DO NOT EDIT." 22 | to edgeDir = iota 23 | from 24 | ) 25 | 26 | var joinTableErr = errors.New("entimport: join tables must be inspected with ref tables - append `tables` flag") 27 | 28 | type ( 29 | edgeDir int 30 | 31 | // relOptions are the options passed down to the functions that creates a relation. 32 | relOptions struct { 33 | uniqueEdgeToChild bool 34 | recursive bool 35 | uniqueEdgeFromParent bool 36 | refName string 37 | edgeField string 38 | } 39 | 40 | // fieldFunc receives an Atlas column and converts it to an Ent field. 41 | fieldFunc func(column *schema.Column) (f ent.Field, err error) 42 | 43 | // SchemaImporter is the interface that wraps the SchemaMutations method. 44 | SchemaImporter interface { 45 | // SchemaMutations imports a given schema from a data source and returns a list of schemast mutators. 46 | SchemaMutations(context.Context) ([]schemast.Mutator, error) 47 | } 48 | 49 | // ImportOptions are the options passed on to every SchemaImporter. 50 | ImportOptions struct { 51 | tables []string 52 | excludedTables []string 53 | schemaPath string 54 | driver *mux.ImportDriver 55 | } 56 | 57 | // ImportOption allows for managing import configuration using functional options. 58 | ImportOption func(*ImportOptions) 59 | ) 60 | 61 | // WithSchemaPath provides a DSN (data source name) for reading the schema & tables from. 62 | func WithSchemaPath(path string) ImportOption { 63 | return func(i *ImportOptions) { 64 | i.schemaPath = path 65 | } 66 | } 67 | 68 | // WithTables limits the schema import to a set of given tables (by all tables are imported) 69 | func WithTables(tables []string) ImportOption { 70 | return func(i *ImportOptions) { 71 | i.tables = tables 72 | } 73 | } 74 | 75 | // WithExcludedTables supplies the set of tables to exclude. 76 | func WithExcludedTables(tables []string) ImportOption { 77 | return func(i *ImportOptions) { 78 | i.excludedTables = tables 79 | } 80 | } 81 | 82 | // WithDriver provides an import driver to be used by SchemaImporter. 83 | func WithDriver(drv *mux.ImportDriver) ImportOption { 84 | return func(i *ImportOptions) { 85 | i.driver = drv 86 | } 87 | } 88 | 89 | // NewImport calls the relevant data source importer based on a given dialect. 90 | func NewImport(opts ...ImportOption) (SchemaImporter, error) { 91 | var ( 92 | si SchemaImporter 93 | err error 94 | ) 95 | i := &ImportOptions{} 96 | for _, apply := range opts { 97 | apply(i) 98 | } 99 | switch i.driver.Dialect { 100 | case dialect.MySQL: 101 | si, err = NewMySQL(i) 102 | if err != nil { 103 | return nil, err 104 | } 105 | case dialect.Postgres: 106 | si, err = NewPostgreSQL(i) 107 | if err != nil { 108 | return nil, err 109 | } 110 | default: 111 | return nil, fmt.Errorf("entimport: unsupported dialect %q", i.driver.Dialect) 112 | } 113 | return si, err 114 | } 115 | 116 | // WriteSchema receives a list of mutators, and writes an ent schema to a given location in the file system. 117 | func WriteSchema(mutations []schemast.Mutator, opts ...ImportOption) error { 118 | i := &ImportOptions{} 119 | for _, apply := range opts { 120 | apply(i) 121 | } 122 | ctx, err := schemast.Load(i.schemaPath) 123 | if err != nil { 124 | return err 125 | } 126 | if err = schemast.Mutate(ctx, mutations...); err != nil { 127 | return err 128 | } 129 | return ctx.Print(i.schemaPath, schemast.Header(header)) 130 | } 131 | 132 | // entEdge creates an edge based on the given params and direction. 133 | func entEdge(nodeName, nodeType string, currentNode *schemast.UpsertSchema, dir edgeDir, opts relOptions) (e ent.Edge) { 134 | var desc *edge.Descriptor 135 | switch dir { 136 | case to: 137 | e = edge.To(nodeName, ent.Schema.Type) 138 | desc = e.Descriptor() 139 | if opts.uniqueEdgeToChild { 140 | desc.Unique = true 141 | desc.Name = inflect.Singularize(nodeName) 142 | } 143 | if opts.recursive { 144 | desc.Name = "child_" + desc.Name 145 | } 146 | case from: 147 | e = edge.From(nodeName, ent.Schema.Type) 148 | desc = e.Descriptor() 149 | if opts.uniqueEdgeFromParent { 150 | desc.Unique = true 151 | desc.Name = inflect.Singularize(nodeName) 152 | } 153 | if opts.edgeField != "" { 154 | setEdgeField(e, opts, currentNode) 155 | } 156 | // RefName describes which entEdge of the Parent Node we're referencing 157 | // because there can be multiple references from one node to another. 158 | refName := opts.refName 159 | if opts.uniqueEdgeToChild { 160 | refName = inflect.Singularize(refName) 161 | } 162 | desc.RefName = refName 163 | if opts.recursive { 164 | desc.Name = "parent_" + desc.Name 165 | desc.RefName = "child_" + desc.RefName 166 | } 167 | } 168 | desc.Type = nodeType 169 | return e 170 | } 171 | 172 | // setEdgeField is a function to properly name edge fields. 173 | func setEdgeField(e ent.Edge, opts relOptions, childNode *schemast.UpsertSchema) { 174 | edgeField := opts.edgeField 175 | // rename the field in case the edge and the field have the same name 176 | if e.Descriptor().Name == edgeField { 177 | edgeField += "_id" 178 | for _, f := range childNode.Fields { 179 | if f.Descriptor().Name == opts.edgeField { 180 | f.Descriptor().Name = edgeField 181 | } 182 | } 183 | } 184 | e.Descriptor().Field = edgeField 185 | } 186 | 187 | // upsertRelation takes 2 nodes and created the edges between them. 188 | func upsertRelation(nodeA *schemast.UpsertSchema, nodeB *schemast.UpsertSchema, opts relOptions) { 189 | tableA := tableName(nodeA.Name) 190 | tableB := tableName(nodeB.Name) 191 | fromA := entEdge(tableA, nodeA.Name, nodeB, from, opts) 192 | toB := entEdge(tableB, nodeB.Name, nodeA, to, opts) 193 | nodeA.Edges = append(nodeA.Edges, toB) 194 | nodeB.Edges = append(nodeB.Edges, fromA) 195 | } 196 | 197 | // upsertManyToMany handles the creation of M2M relations. 198 | func upsertManyToMany(mutations map[string]schemast.Mutator, table *schema.Table) error { 199 | tableA := table.ForeignKeys[0].RefTable 200 | tableB := table.ForeignKeys[1].RefTable 201 | var opts relOptions 202 | if tableA.Name == tableB.Name { 203 | opts.recursive = true 204 | } 205 | nodeA, ok := mutations[tableA.Name].(*schemast.UpsertSchema) 206 | if !ok { 207 | return joinTableErr 208 | } 209 | nodeB, ok := mutations[tableB.Name].(*schemast.UpsertSchema) 210 | if !ok { 211 | return joinTableErr 212 | } 213 | opts.refName = tableName(nodeB.Name) 214 | upsertRelation(nodeA, nodeB, opts) 215 | return nil 216 | } 217 | 218 | // Note: at this moment ent doesn't support fields on m2m relations. 219 | func isJoinTable(table *schema.Table) bool { 220 | if table.PrimaryKey == nil || len(table.PrimaryKey.Parts) != 2 || len(table.ForeignKeys) != 2 { 221 | return false 222 | } 223 | // Make sure that the foreign key columns exactly match primary key column. 224 | for _, fk := range table.ForeignKeys { 225 | if len(fk.Columns) != 1 { 226 | return false 227 | } 228 | if fk.Columns[0] != table.PrimaryKey.Parts[0].C && fk.Columns[0] != table.PrimaryKey.Parts[1].C { 229 | return false 230 | } 231 | } 232 | return true 233 | } 234 | 235 | func typeName(tableName string) string { 236 | return inflect.Camelize(inflect.Singularize(tableName)) 237 | } 238 | 239 | func tableName(typeName string) string { 240 | return inflect.Underscore(inflect.Pluralize(typeName)) 241 | } 242 | 243 | // resolvePrimaryKey returns the primary key as an ent field for a given table. 244 | func resolvePrimaryKey(field fieldFunc, table *schema.Table) (f ent.Field, err error) { 245 | if table.PrimaryKey == nil { 246 | return nil, fmt.Errorf("entimport: missing primary key (table: %v)", table.Name) 247 | } 248 | if len(table.PrimaryKey.Parts) != 1 { 249 | return nil, fmt.Errorf("entimport: invalid primary key, single part key must be present (table: %v, got: %v parts)", table.Name, len(table.PrimaryKey.Parts)) 250 | } 251 | if f, err = field(table.PrimaryKey.Parts[0].C); err != nil { 252 | return nil, err 253 | } 254 | if d := f.Descriptor(); d.Name != "id" { 255 | d.StorageKey = d.Name 256 | d.Name = "id" 257 | } 258 | return f, nil 259 | } 260 | 261 | // upsertNode handles the creation of a node from a given table. 262 | func upsertNode(field fieldFunc, table *schema.Table) (*schemast.UpsertSchema, error) { 263 | upsert := &schemast.UpsertSchema{ 264 | Name: typeName(table.Name), 265 | } 266 | if tableName(table.Name) != table.Name { 267 | upsert.Annotations = []entschema.Annotation{ 268 | entsql.Annotation{Table: table.Name}, 269 | } 270 | } 271 | fields := make(map[string]ent.Field, len(upsert.Fields)) 272 | for _, f := range upsert.Fields { 273 | fields[f.Descriptor().StorageKey] = f 274 | } 275 | pk, err := resolvePrimaryKey(field, table) 276 | if err != nil { 277 | return nil, err 278 | } 279 | if _, ok := fields[pk.Descriptor().StorageKey]; !ok { 280 | fields[pk.Descriptor().StorageKey] = pk 281 | upsert.Fields = append(upsert.Fields, pk) 282 | } 283 | for _, column := range table.Columns { 284 | if table.PrimaryKey != nil && 285 | len(table.PrimaryKey.Parts) != 0 && 286 | table.PrimaryKey.Parts[0].C.Name == column.Name { 287 | continue 288 | } 289 | fld, err := field(column) 290 | if err != nil { 291 | return nil, err 292 | } 293 | if _, ok := fields[column.Name]; !ok { 294 | fields[column.Name] = fld 295 | upsert.Fields = append(upsert.Fields, fld) 296 | } 297 | } 298 | for _, index := range table.Indexes { 299 | if index.Unique && len(index.Parts) == 1 { 300 | fields[index.Parts[0].C.Name].Descriptor().Unique = true 301 | } 302 | } 303 | for _, fk := range table.ForeignKeys { 304 | for _, column := range fk.Columns { 305 | // FK / Reference column 306 | fld, ok := fields[column.Name] 307 | if !ok { 308 | return nil, fmt.Errorf("foreign key for column: %q doesn't exist in referenced table", column.Name) 309 | } 310 | fld.Descriptor().Optional = true 311 | } 312 | } 313 | return upsert, err 314 | } 315 | 316 | // applyColumnAttributes adds column attributes to a given ent field. 317 | func applyColumnAttributes(f ent.Field, col *schema.Column) { 318 | desc := f.Descriptor() 319 | desc.Optional = col.Type.Null 320 | for _, attr := range col.Attrs { 321 | if a, ok := attr.(*schema.Comment); ok { 322 | desc.Comment = a.Text 323 | } 324 | } 325 | } 326 | 327 | // schemaMutations is in charge of creating all the schema mutations needed for an ent schema. 328 | func schemaMutations(field fieldFunc, tables []*schema.Table) ([]schemast.Mutator, error) { 329 | mutations := make(map[string]schemast.Mutator) 330 | joinTables := make(map[string]*schema.Table) 331 | for _, table := range tables { 332 | if isJoinTable(table) { 333 | joinTables[table.Name] = table 334 | continue 335 | } 336 | node, err := upsertNode(field, table) 337 | if err != nil { 338 | return nil, fmt.Errorf("entimport: issue with table %v: %w", table.Name, err) 339 | } 340 | mutations[table.Name] = node 341 | } 342 | for _, table := range tables { 343 | if t, ok := joinTables[table.Name]; ok { 344 | err := upsertManyToMany(mutations, t) 345 | if err != nil { 346 | return nil, err 347 | } 348 | continue 349 | } 350 | upsertOneToX(mutations, table) 351 | } 352 | ml := make([]schemast.Mutator, 0, len(mutations)) 353 | for _, mutator := range mutations { 354 | ml = append(ml, mutator) 355 | } 356 | return ml, nil 357 | } 358 | 359 | // O2O Two Types - Child Table has a unique reference (FK) to Parent table 360 | // O2O Same Type - Child Table has a unique reference (FK) to Parent table (itself) 361 | // O2M (The "Many" side, keeps a reference to the "One" side). 362 | // O2M Two Types - Parent has a non-unique reference to Child, and Child has a unique back-reference to Parent 363 | // O2M Same Type - Parent has a non-unique reference to Child, and Child doesn't have a back-reference to Parent. 364 | func upsertOneToX(mutations map[string]schemast.Mutator, table *schema.Table) { 365 | if table.ForeignKeys == nil { 366 | return 367 | } 368 | idxs := make(map[string]*schema.Index) 369 | for _, idx := range table.Indexes { 370 | if len(idx.Parts) != 1 { 371 | continue 372 | } 373 | idxs[idx.Parts[0].C.Name] = idx 374 | } 375 | for _, fk := range table.ForeignKeys { 376 | if len(fk.Columns) != 1 { 377 | continue 378 | } 379 | parent := fk.RefTable 380 | child := table 381 | colName := fk.Columns[0].Name 382 | opts := relOptions{ 383 | uniqueEdgeFromParent: true, 384 | refName: tableName(child.Name), 385 | edgeField: colName, 386 | } 387 | if child.Name == parent.Name { 388 | opts.recursive = true 389 | } 390 | idx, ok := idxs[colName] 391 | if ok && idx.Unique { 392 | opts.uniqueEdgeToChild = true 393 | } 394 | // If at least one table in the relation does not exist, there is no point to create it. 395 | parentNode, ok := mutations[parent.Name].(*schemast.UpsertSchema) 396 | if !ok { 397 | return 398 | } 399 | childNode, ok := mutations[child.Name].(*schemast.UpsertSchema) 400 | if !ok { 401 | return 402 | } 403 | upsertRelation(parentNode, childNode, opts) 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /internal/entimport/mysql.go: -------------------------------------------------------------------------------- 1 | package entimport 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "ariga.io/atlas/sql/mysql" 9 | "ariga.io/atlas/sql/schema" 10 | 11 | "entgo.io/contrib/schemast" 12 | "entgo.io/ent" 13 | "entgo.io/ent/schema/field" 14 | ) 15 | 16 | const ( 17 | mTinyInt = "tinyint" // MYSQL_TYPE_TINY 18 | mSmallInt = "smallint" // MYSQL_TYPE_SHORT 19 | mInt = "int" // MYSQL_TYPE_LONG 20 | mMediumInt = "mediumint" // MYSQL_TYPE_INT24 21 | mBigInt = "bigint" // MYSQL_TYPE_LONGLONG 22 | ) 23 | 24 | // MySQL holds the schema import options and an Atlas inspector instance 25 | type MySQL struct { 26 | *ImportOptions 27 | } 28 | 29 | // NewMySQL - create aמ import structure for MySQL. 30 | func NewMySQL(i *ImportOptions) (*MySQL, error) { 31 | return &MySQL{ 32 | ImportOptions: i, 33 | }, nil 34 | } 35 | 36 | // SchemaMutations implements SchemaImporter. 37 | func (m *MySQL) SchemaMutations(ctx context.Context) ([]schemast.Mutator, error) { 38 | inspectOptions := &schema.InspectOptions{ 39 | Tables: m.tables, 40 | } 41 | s, err := m.driver.InspectSchema(ctx, m.driver.SchemaName, inspectOptions) 42 | if err != nil { 43 | return nil, err 44 | } 45 | tables := s.Tables 46 | if m.excludedTables != nil { 47 | tables = nil 48 | excludedTableNames := make(map[string]bool) 49 | for _, t := range m.excludedTables { 50 | excludedTableNames[t] = true 51 | } 52 | // filter out tables that are in excludedTables: 53 | for _, t := range s.Tables { 54 | if !excludedTableNames[t.Name] { 55 | tables = append(tables, t) 56 | } 57 | } 58 | } 59 | return schemaMutations(m.field, tables) 60 | } 61 | 62 | func (m *MySQL) field(column *schema.Column) (f ent.Field, err error) { 63 | name := column.Name 64 | switch typ := column.Type.Type.(type) { 65 | case *schema.BinaryType: 66 | f = field.Bytes(name) 67 | case *schema.BoolType: 68 | f = field.Bool(name) 69 | case *schema.DecimalType: 70 | f = field.Float(name) 71 | case *schema.EnumType: 72 | f = field.Enum(name).Values(typ.Values...) 73 | case *schema.FloatType: 74 | f = m.convertFloat(typ, name) 75 | case *schema.IntegerType: 76 | f = m.convertInteger(typ, name) 77 | case *schema.JSONType: 78 | f = field.JSON(name, json.RawMessage{}) 79 | case *schema.StringType: 80 | f = field.String(name) 81 | case *schema.TimeType: 82 | f = field.Time(name) 83 | default: 84 | return nil, fmt.Errorf("entimport: unsupported type %q for column %v", typ, column.Name) 85 | } 86 | applyColumnAttributes(f, column) 87 | return f, err 88 | } 89 | 90 | func (m *MySQL) convertFloat(typ *schema.FloatType, name string) (f ent.Field) { 91 | // A precision from 0 to 23 results in a 4-byte single-precision FLOAT column. 92 | // A precision from 24 to 53 results in an 8-byte double-precision DOUBLE column: 93 | // https://dev.mysql.com/doc/refman/8.0/en/floating-point-types.html 94 | if typ.T == mysql.TypeDouble { 95 | return field.Float(name) 96 | } 97 | return field.Float32(name) 98 | } 99 | 100 | func (m *MySQL) convertInteger(typ *schema.IntegerType, name string) (f ent.Field) { 101 | if typ.Unsigned { 102 | switch typ.T { 103 | case mTinyInt: 104 | f = field.Uint8(name) 105 | case mSmallInt: 106 | f = field.Uint16(name) 107 | case mMediumInt: 108 | f = field.Uint32(name) 109 | case mInt: 110 | f = field.Uint32(name) 111 | case mBigInt: 112 | f = field.Uint64(name) 113 | } 114 | return f 115 | } 116 | switch typ.T { 117 | case mTinyInt: 118 | f = field.Int8(name) 119 | case mSmallInt: 120 | f = field.Int16(name) 121 | case mMediumInt: 122 | f = field.Int32(name) 123 | case mInt: 124 | f = field.Int32(name) 125 | case mBigInt: 126 | // Int64 is not used on purpose. 127 | f = field.Int(name) 128 | } 129 | return f 130 | } 131 | -------------------------------------------------------------------------------- /internal/entimport/mysql_test.go: -------------------------------------------------------------------------------- 1 | package entimport_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "go/parser" 7 | "go/printer" 8 | "go/token" 9 | "testing" 10 | 11 | "ariga.io/atlas/sql/schema" 12 | "ariga.io/entimport/internal/entimport" 13 | 14 | "entgo.io/ent/dialect" 15 | "github.com/go-openapi/inflect" 16 | _ "github.com/go-sql-driver/mysql" 17 | "github.com/stretchr/testify/require" 18 | ) 19 | 20 | func TestMySQL(t *testing.T) { 21 | var ( 22 | r = require.New(t) 23 | ctx = context.Background() 24 | testSchema = "test" 25 | ) 26 | tests := []struct { 27 | name string 28 | entities []string 29 | expectedFields map[string]string 30 | mock *schema.Schema 31 | expectedEdges map[string]string 32 | expectedAnnotations map[string]string 33 | }{ 34 | { 35 | name: "table_name_does_not_use_plural_form", 36 | mock: MockMySQLTableNameDoesNotUsePluralForm(), 37 | expectedFields: map[string]string{ 38 | "pet": `func (Pet) Fields() []ent.Field { 39 | return []ent.Field{field.Int("id"), field.Int8("age"), field.String("name")} 40 | }`, 41 | }, 42 | expectedEdges: map[string]string{ 43 | `pet`: `func (Pet) Edges() []ent.Edge { 44 | return nil 45 | }`, 46 | }, 47 | expectedAnnotations: map[string]string{ 48 | `pet`: `func (Pet) Annotations() []schema.Annotation { 49 | return []schema.Annotation{entsql.Annotation{Table: "pet"}} 50 | }`, 51 | }, 52 | entities: []string{"pet"}, 53 | }, { 54 | name: "single_table_fields", 55 | mock: MockMySQLSingleTableFields(), 56 | expectedFields: map[string]string{ 57 | "user": `func (User) Fields() []ent.Field { 58 | return []ent.Field{field.Int("id"), field.Int8("age"), field.String("name")} 59 | }`, 60 | }, 61 | expectedEdges: map[string]string{ 62 | `user`: `func (User) Edges() []ent.Edge { 63 | return nil 64 | }`, 65 | }, 66 | expectedAnnotations: map[string]string{ 67 | `user`: `func (User) Annotations() []schema.Annotation { 68 | return nil 69 | }`, 70 | }, 71 | entities: []string{"user"}, 72 | }, 73 | { 74 | name: "fields_with_attributes", 75 | mock: MockMySQLTableFieldsWithAttributes(), 76 | expectedFields: map[string]string{ 77 | "user": `func (User) Fields() []ent.Field { 78 | return []ent.Field{field.Int("id").Comment("some id"), field.Int8("age").Optional(), field.String("name").Comment("first name"), field.String("last_name").Optional().Comment("family name")} 79 | }`, 80 | }, 81 | expectedEdges: map[string]string{ 82 | `user`: `func (User) Edges() []ent.Edge { 83 | return nil 84 | }`, 85 | }, 86 | expectedAnnotations: map[string]string{ 87 | `user`: `func (User) Annotations() []schema.Annotation { 88 | return nil 89 | }`, 90 | }, 91 | entities: []string{"user"}, 92 | }, 93 | { 94 | name: "fields_with_unique_indexes", 95 | mock: MockMySQLTableFieldsWithUniqueIndexes(), 96 | expectedFields: map[string]string{ 97 | "user": `func (User) Fields() []ent.Field { 98 | return []ent.Field{field.Int("id"), field.Int8("age").Unique(), field.String("last_name").Optional().Comment("not so boring"), field.String("name")} 99 | }`, 100 | }, 101 | expectedEdges: map[string]string{ 102 | `user`: `func (User) Edges() []ent.Edge { 103 | return nil 104 | }`, 105 | }, 106 | expectedAnnotations: map[string]string{ 107 | `user`: `func (User) Annotations() []schema.Annotation { 108 | return nil 109 | }`, 110 | }, 111 | entities: []string{"user"}, 112 | }, 113 | { 114 | name: "multi_table_fields", 115 | mock: MockMySQLMultiTableFields(), 116 | expectedFields: map[string]string{ 117 | "user": `func (User) Fields() []ent.Field { 118 | return []ent.Field{field.Int("id"), field.Int8("age").Unique(), field.String("last_name").Optional().Comment("not so boring"), field.String("name")} 119 | }`, 120 | "pet": `func (Pet) Fields() []ent.Field { 121 | return []ent.Field{field.Int("id").Comment("pet id"), field.Int8("age").Optional(), field.String("name")} 122 | }`, 123 | }, 124 | expectedEdges: map[string]string{ 125 | `user`: `func (User) Edges() []ent.Edge { 126 | return nil 127 | }`, 128 | `pet`: `func (Pet) Edges() []ent.Edge { 129 | return nil 130 | }`, 131 | }, 132 | expectedAnnotations: map[string]string{ 133 | `user`: `func (User) Annotations() []schema.Annotation { 134 | return nil 135 | }`, 136 | `pet`: `func (Pet) Annotations() []schema.Annotation { 137 | return nil 138 | }`, 139 | }, 140 | entities: []string{"user", "pet"}, 141 | }, 142 | { 143 | name: "non_default_primary_key", 144 | mock: MockMySQLNonDefaultPrimaryKey(), 145 | expectedFields: map[string]string{ 146 | "user": `func (User) Fields() []ent.Field { 147 | return []ent.Field{field.String("id").StorageKey("name"), field.String("last_name").Unique()} 148 | }`, 149 | }, 150 | expectedEdges: map[string]string{ 151 | `user`: `func (User) Edges() []ent.Edge { 152 | return nil 153 | }`, 154 | }, 155 | expectedAnnotations: map[string]string{ 156 | `user`: `func (User) Annotations() []schema.Annotation { 157 | return nil 158 | }`, 159 | }, 160 | entities: []string{"user"}, 161 | }, 162 | { 163 | name: "relation_m2m_two_types", 164 | mock: MockMySQLM2MTwoTypes(), 165 | expectedFields: map[string]string{ 166 | "user": `func (User) Fields() []ent.Field { 167 | return []ent.Field{field.Int("id"), field.Int("age"), field.String("name")} 168 | }`, 169 | "group": `func (Group) Fields() []ent.Field { 170 | return []ent.Field{field.Int("id"), field.String("name")} 171 | }`, 172 | }, 173 | expectedEdges: map[string]string{ 174 | "user": `func (User) Edges() []ent.Edge { 175 | return []ent.Edge{edge.From("groups", Group.Type).Ref("users")} 176 | }`, 177 | "group": `func (Group) Edges() []ent.Edge { 178 | return []ent.Edge{edge.To("users", User.Type)} 179 | }`, 180 | }, 181 | expectedAnnotations: map[string]string{ 182 | `user`: `func (User) Annotations() []schema.Annotation { 183 | return nil 184 | }`, 185 | `group`: `func (Group) Annotations() []schema.Annotation { 186 | return nil 187 | }`, 188 | }, 189 | entities: []string{"user", "group"}, 190 | }, 191 | { 192 | name: "relation_m2m_same_type", 193 | mock: MockMySQLM2MSameType(), 194 | expectedFields: map[string]string{ 195 | "user": `func (User) Fields() []ent.Field { 196 | return []ent.Field{field.Int("id"), field.Int("age"), field.String("name")} 197 | }`, 198 | }, 199 | expectedEdges: map[string]string{ 200 | "user": `func (User) Edges() []ent.Edge { 201 | return []ent.Edge{edge.To("child_users", User.Type), edge.From("parent_users", User.Type).Ref("child_users")} 202 | }`, 203 | }, 204 | expectedAnnotations: map[string]string{ 205 | `user`: `func (User) Annotations() []schema.Annotation { 206 | return nil 207 | }`, 208 | }, 209 | entities: []string{"user"}, 210 | }, 211 | { 212 | name: "relation_m2m_bidirectional", 213 | mock: MockMySQLM2MBidirectional(), 214 | expectedFields: map[string]string{ 215 | "user": `func (User) Fields() []ent.Field { 216 | return []ent.Field{field.Int("id"), field.Int("age"), field.String("name")} 217 | }`, 218 | }, 219 | expectedEdges: map[string]string{ 220 | "user": `func (User) Edges() []ent.Edge { 221 | return []ent.Edge{edge.To("child_users", User.Type), edge.From("parent_users", User.Type).Ref("child_users")} 222 | }`, 223 | }, 224 | expectedAnnotations: map[string]string{ 225 | `user`: `func (User) Annotations() []schema.Annotation { 226 | return nil 227 | }`, 228 | }, 229 | entities: []string{"user"}, 230 | }, 231 | { 232 | name: "relation_o2o_two_types", 233 | mock: MockMySQLO2OTwoTypes(), 234 | expectedFields: map[string]string{ 235 | "user": `func (User) Fields() []ent.Field { 236 | return []ent.Field{field.Int("id"), field.Int("age"), field.String("name")} 237 | }`, 238 | "card": `func (Card) Fields() []ent.Field { 239 | return []ent.Field{field.Int("id"), field.String("number"), field.Int("user_card").Optional().Unique()} 240 | }`, 241 | }, 242 | expectedEdges: map[string]string{ 243 | "user": `func (User) Edges() []ent.Edge { 244 | return []ent.Edge{edge.To("card", Card.Type).Unique()} 245 | }`, 246 | "card": `func (Card) Edges() []ent.Edge { 247 | return []ent.Edge{edge.From("user", User.Type).Ref("card").Unique().Field("user_card")} 248 | }`, 249 | }, 250 | expectedAnnotations: map[string]string{ 251 | `user`: `func (User) Annotations() []schema.Annotation { 252 | return nil 253 | }`, 254 | `card`: `func (Card) Annotations() []schema.Annotation { 255 | return nil 256 | }`, 257 | }, 258 | entities: []string{"user", "card"}, 259 | }, 260 | { 261 | name: "relation_o2o_same_type", 262 | mock: MockMySQLO2OSameType(), 263 | expectedFields: map[string]string{ 264 | "node": `func (Node) Fields() []ent.Field { 265 | return []ent.Field{field.Int("id"), field.Int("value"), field.Int("node_next").Optional().Unique()} 266 | }`, 267 | }, 268 | expectedEdges: map[string]string{ 269 | "node": `func (Node) Edges() []ent.Edge { 270 | return []ent.Edge{edge.To("child_node", Node.Type).Unique(), edge.From("parent_node", Node.Type).Ref("child_node").Unique().Field("node_next")} 271 | }`, 272 | }, 273 | expectedAnnotations: map[string]string{ 274 | `node`: `func (Node) Annotations() []schema.Annotation { 275 | return nil 276 | }`, 277 | }, 278 | entities: []string{"node"}, 279 | }, 280 | { 281 | name: "relation_o2o_bidirectional", 282 | mock: MockMySQLO2OBidirectional(), 283 | expectedFields: map[string]string{ 284 | "user": `func (User) Fields() []ent.Field { 285 | return []ent.Field{field.Int("id"), field.Int("age"), field.String("name"), field.Int("user_spouse").Optional().Unique()} 286 | }`, 287 | }, 288 | expectedEdges: map[string]string{ 289 | "user": `func (User) Edges() []ent.Edge { 290 | return []ent.Edge{edge.To("child_user", User.Type).Unique(), edge.From("parent_user", User.Type).Ref("child_user").Unique().Field("user_spouse")} 291 | }`, 292 | }, 293 | expectedAnnotations: map[string]string{ 294 | `user`: `func (User) Annotations() []schema.Annotation { 295 | return nil 296 | }`, 297 | }, 298 | entities: []string{"user"}, 299 | }, 300 | { 301 | name: "relation_o2m_two_types", 302 | mock: MockMySQLO2MTwoTypes(), 303 | expectedFields: map[string]string{ 304 | "user": `func (User) Fields() []ent.Field { 305 | return []ent.Field{field.Int("id"), field.Int("age"), field.String("name")} 306 | }`, 307 | "pet": `func (Pet) Fields() []ent.Field { 308 | return []ent.Field{field.Int("id"), field.String("name"), field.Int("user_pets").Optional()} 309 | }`, 310 | }, 311 | expectedEdges: map[string]string{ 312 | "user": `func (User) Edges() []ent.Edge { 313 | return []ent.Edge{edge.To("pets", Pet.Type)} 314 | }`, 315 | "pet": `func (Pet) Edges() []ent.Edge { 316 | return []ent.Edge{edge.From("user", User.Type).Ref("pets").Unique().Field("user_pets")} 317 | }`, 318 | }, 319 | expectedAnnotations: map[string]string{ 320 | `user`: `func (User) Annotations() []schema.Annotation { 321 | return nil 322 | }`, 323 | `pet`: `func (Pet) Annotations() []schema.Annotation { 324 | return nil 325 | }`, 326 | }, 327 | entities: []string{"user", "pet"}, 328 | }, 329 | { 330 | name: "relation_o2m_same_type", 331 | mock: MockMySQLO2MSameType(), 332 | expectedFields: map[string]string{ 333 | "node": `func (Node) Fields() []ent.Field { 334 | return []ent.Field{field.Int("id"), field.Int("value"), field.Int("node_children").Optional()} 335 | }`, 336 | }, 337 | expectedEdges: map[string]string{ 338 | "node": `func (Node) Edges() []ent.Edge { 339 | return []ent.Edge{edge.To("child_nodes", Node.Type), edge.From("parent_node", Node.Type).Ref("child_nodes").Unique().Field("node_children")} 340 | }`, 341 | }, 342 | expectedAnnotations: map[string]string{ 343 | `node`: `func (Node) Annotations() []schema.Annotation { 344 | return nil 345 | }`, 346 | }, 347 | entities: []string{"node"}, 348 | }, 349 | { 350 | name: "relation_o2x_other_side_ignored", 351 | mock: MockMySQLO2XOtherSideIgnored(), 352 | expectedFields: map[string]string{ 353 | "pet": `func (Pet) Fields() []ent.Field { 354 | return []ent.Field{field.Int("id"), field.String("name"), field.Int("user_pets").Optional()} 355 | }`, 356 | }, 357 | expectedEdges: map[string]string{ 358 | "pet": `func (Pet) Edges() []ent.Edge { 359 | return nil 360 | }`, 361 | }, 362 | expectedAnnotations: map[string]string{ 363 | `pet`: `func (Pet) Annotations() []schema.Annotation { 364 | return nil 365 | }`, 366 | }, 367 | entities: []string{"pet"}, 368 | }, 369 | } 370 | for _, tt := range tests { 371 | t.Run(tt.name, func(t *testing.T) { 372 | m := mockMux(ctx, dialect.MySQL, tt.mock, testSchema) 373 | drv, err := m.OpenImport("mysql://root:pass@tcp(localhost:3308)/test?parseTime=True") 374 | r.NoError(err) 375 | importer, err := entimport.NewImport( 376 | entimport.WithDriver(drv), 377 | ) 378 | r.NoError(err) 379 | schemas := createTempDir(t) 380 | mutations, err := importer.SchemaMutations(ctx) 381 | r.NoError(err) 382 | err = entimport.WriteSchema(mutations, entimport.WithSchemaPath(schemas)) 383 | r.NoError(err) 384 | actualFiles := readDir(t, schemas) 385 | r.EqualValues(len(tt.entities), len(actualFiles)) 386 | for _, e := range tt.entities { 387 | f, err := parser.ParseFile(token.NewFileSet(), "", actualFiles[e+".go"], 0) 388 | r.NoError(err) 389 | typeName := inflect.Camelize(e) 390 | fieldMethod := lookupMethod(f, typeName, "Fields") 391 | r.NotNil(fieldMethod) 392 | var actualFields bytes.Buffer 393 | err = printer.Fprint(&actualFields, token.NewFileSet(), fieldMethod) 394 | r.NoError(err) 395 | r.EqualValues(tt.expectedFields[e], actualFields.String()) 396 | 397 | edgeMethod := lookupMethod(f, typeName, "Edges") 398 | r.NotNil(edgeMethod) 399 | var actualEdges bytes.Buffer 400 | err = printer.Fprint(&actualEdges, token.NewFileSet(), edgeMethod) 401 | r.NoError(err) 402 | r.EqualValues(tt.expectedEdges[e], actualEdges.String()) 403 | 404 | annotationsMethod := lookupMethod(f, typeName, "Annotations") 405 | r.NotNil(annotationsMethod) 406 | var actualAnnotations bytes.Buffer 407 | err = printer.Fprint(&actualAnnotations, token.NewFileSet(), annotationsMethod) 408 | r.NoError(err) 409 | r.EqualValues(tt.expectedAnnotations[e], actualAnnotations.String()) 410 | } 411 | }) 412 | } 413 | } 414 | 415 | func TestMySQLJoinTableOnly(t *testing.T) { 416 | var ( 417 | testSchema = "test" 418 | ctx = context.Background() 419 | ) 420 | m := mockMux(ctx, dialect.MySQL, MockMySQLM2MJoinTableOnly(), testSchema) 421 | drv, err := m.OpenImport("mysql://root:pass@tcp(localhost:3308)/test?parseTime=True") 422 | require.NoError(t, err) 423 | importer, err := entimport.NewImport( 424 | entimport.WithDriver(drv), 425 | ) 426 | require.NoError(t, err) 427 | mutations, err := importer.SchemaMutations(ctx) 428 | require.Empty(t, mutations) 429 | require.EqualError(t, err, "entimport: join tables must be inspected with ref tables - append `tables` flag") 430 | } 431 | -------------------------------------------------------------------------------- /internal/entimport/postgres.go: -------------------------------------------------------------------------------- 1 | package entimport 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "ariga.io/atlas/sql/postgres" 9 | "ariga.io/atlas/sql/schema" 10 | 11 | "entgo.io/contrib/schemast" 12 | "entgo.io/ent" 13 | "entgo.io/ent/dialect" 14 | "entgo.io/ent/schema/field" 15 | "github.com/google/uuid" 16 | _ "github.com/lib/pq" 17 | ) 18 | 19 | // Postgres implements SchemaImporter for PostgreSQL databases. 20 | type Postgres struct { 21 | *ImportOptions 22 | } 23 | 24 | // NewPostgreSQL - returns a new *Postgres. 25 | func NewPostgreSQL(i *ImportOptions) (SchemaImporter, error) { 26 | return &Postgres{ 27 | ImportOptions: i, 28 | }, nil 29 | } 30 | 31 | // SchemaMutations implements SchemaImporter. 32 | func (p *Postgres) SchemaMutations(ctx context.Context) ([]schemast.Mutator, error) { 33 | inspectOptions := &schema.InspectOptions{ 34 | Tables: p.tables, 35 | } 36 | s, err := p.driver.InspectSchema(ctx, p.driver.SchemaName, inspectOptions) 37 | if err != nil { 38 | return nil, err 39 | } 40 | tables := s.Tables 41 | if p.excludedTables != nil { 42 | tables = nil 43 | excludedTableNames := make(map[string]bool) 44 | for _, t := range p.excludedTables { 45 | excludedTableNames[t] = true 46 | } 47 | // filter out tables that are in excludedTables: 48 | for _, t := range s.Tables { 49 | if !excludedTableNames[t.Name] { 50 | tables = append(tables, t) 51 | } 52 | } 53 | } 54 | return schemaMutations(p.field, tables) 55 | } 56 | 57 | func (p *Postgres) field(column *schema.Column) (f ent.Field, err error) { 58 | name := column.Name 59 | switch typ := column.Type.Type.(type) { 60 | case *schema.BinaryType: 61 | f = field.Bytes(name) 62 | case *schema.BoolType: 63 | f = field.Bool(name) 64 | case *schema.DecimalType: 65 | f = field.Float(name) 66 | case *schema.EnumType: 67 | f = field.Enum(name).Values(typ.Values...) 68 | case *schema.FloatType: 69 | f = p.convertFloat(typ, name) 70 | case *schema.IntegerType: 71 | f = p.convertInteger(typ, name) 72 | case *schema.JSONType: 73 | f = field.JSON(name, json.RawMessage{}) 74 | case *schema.StringType: 75 | f = field.String(name) 76 | case *schema.TimeType: 77 | f = field.Time(name) 78 | case *postgres.SerialType: 79 | f = p.convertSerial(typ, name) 80 | case *postgres.UUIDType: 81 | f = field.UUID(name, uuid.New()) 82 | default: 83 | return nil, fmt.Errorf("entimport: unsupported type %q for column %v", typ, column.Name) 84 | } 85 | applyColumnAttributes(f, column) 86 | return f, err 87 | } 88 | 89 | // decimal, numeric - user-specified precision, exact up to 131072 digits before the decimal point; 90 | // up to 16383 digits after the decimal point. 91 | // real - 4 bytes variable-precision, inexact 6 decimal digits precision. 92 | // double - 8 bytes variable-precision, inexact 15 decimal digits precision. 93 | func (p *Postgres) convertFloat(typ *schema.FloatType, name string) (f ent.Field) { 94 | if typ.T == postgres.TypeReal { 95 | return field.Float32(name) 96 | } 97 | return field.Float(name) 98 | } 99 | 100 | func (p *Postgres) convertInteger(typ *schema.IntegerType, name string) (f ent.Field) { 101 | switch typ.T { 102 | // smallint - 2 bytes small-range integer -32768 to +32767. 103 | case "smallint": 104 | f = field.Int16(name) 105 | // integer - 4 bytes typical choice for integer -2147483648 to +2147483647. 106 | case "integer": 107 | f = field.Int32(name) 108 | // bigint - 8 bytes large-range integer -9223372036854775808 to 9223372036854775807. 109 | case "bigint": 110 | // Int64 is not used on purpose. 111 | f = field.Int(name) 112 | } 113 | return f 114 | } 115 | 116 | // smallserial- 2 bytes - small autoincrementing integer 1 to 32767 117 | // serial - 4 bytes autoincrementing integer 1 to 2147483647 118 | // bigserial - 8 bytes large autoincrementing integer 1 to 9223372036854775807 119 | func (p *Postgres) convertSerial(typ *postgres.SerialType, name string) ent.Field { 120 | return field.Uint(name). 121 | SchemaType(map[string]string{ 122 | dialect.Postgres: typ.T, // Override Postgres. 123 | }) 124 | } 125 | -------------------------------------------------------------------------------- /internal/entimport/postgres_test.go: -------------------------------------------------------------------------------- 1 | package entimport_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "go/parser" 7 | "go/printer" 8 | "go/token" 9 | "testing" 10 | 11 | "ariga.io/atlas/sql/schema" 12 | 13 | "ariga.io/entimport/internal/entimport" 14 | 15 | "entgo.io/ent/dialect" 16 | "github.com/go-openapi/inflect" 17 | _ "github.com/go-sql-driver/mysql" 18 | "github.com/stretchr/testify/require" 19 | ) 20 | 21 | func TestPostgres(t *testing.T) { 22 | var ( 23 | r = require.New(t) 24 | ctx = context.Background() 25 | testSchema = "public" 26 | ) 27 | tests := []struct { 28 | name string 29 | entities []string 30 | expectedFields map[string]string 31 | mock *schema.Schema 32 | expectedEdges map[string]string 33 | }{ 34 | { 35 | name: "single_table_fields", 36 | mock: MockPostgresSingleTableFields(), 37 | expectedFields: map[string]string{ 38 | "user": `func (User) Fields() []ent.Field { 39 | return []ent.Field{field.Int("id"), field.Int16("age"), field.String("name")} 40 | }`, 41 | }, 42 | expectedEdges: map[string]string{ 43 | 44 | `user`: `func (User) Edges() []ent.Edge { 45 | return nil 46 | }`, 47 | }, 48 | entities: []string{"user"}, 49 | }, 50 | { 51 | name: "fields_with_attributes", 52 | mock: MockPostgresTableFieldsWithAttributes(), 53 | expectedFields: map[string]string{ 54 | "user": `func (User) Fields() []ent.Field { 55 | return []ent.Field{field.Int("id").Comment("some id"), field.Int16("age").Optional(), field.String("name").Comment("first name"), field.String("last_name").Optional().Comment("family name")} 56 | }`, 57 | }, 58 | expectedEdges: map[string]string{ 59 | `user`: `func (User) Edges() []ent.Edge { 60 | return nil 61 | }`, 62 | }, 63 | entities: []string{"user"}, 64 | }, 65 | { 66 | name: "fields_with_unique_indexes", 67 | mock: MockPostgresTableFieldsWithUniqueIndexes(), 68 | expectedFields: map[string]string{ 69 | "user": `func (User) Fields() []ent.Field { 70 | return []ent.Field{field.Int("id").Comment("some id"), field.Int16("age").Unique(), field.String("name").Comment("first name"), field.String("last_name").Optional().Comment("family name")} 71 | }`, 72 | }, 73 | expectedEdges: map[string]string{ 74 | `user`: `func (User) Edges() []ent.Edge { 75 | return nil 76 | }`, 77 | }, 78 | entities: []string{"user"}, 79 | }, 80 | { 81 | name: "multi_table_fields", 82 | mock: MockPostgresMultiTableFields(), 83 | expectedFields: map[string]string{ 84 | "user": `func (User) Fields() []ent.Field { 85 | return []ent.Field{field.Int("id"), field.Int16("age").Unique(), field.String("name"), field.String("last_name").Optional().Comment("not so boring")} 86 | }`, 87 | "pet": `func (Pet) Fields() []ent.Field { 88 | return []ent.Field{field.Int("id").Comment("pet id"), field.Int16("age").Optional(), field.String("name")} 89 | }`, 90 | }, 91 | expectedEdges: map[string]string{ 92 | `user`: `func (User) Edges() []ent.Edge { 93 | return nil 94 | }`, 95 | `pet`: `func (Pet) Edges() []ent.Edge { 96 | return nil 97 | }`, 98 | }, 99 | entities: []string{"user", "pet"}, 100 | }, 101 | { 102 | name: "non_default_primary_key", 103 | mock: MockPostgresNonDefaultPrimaryKey(), 104 | expectedFields: map[string]string{ 105 | "user": `func (User) Fields() []ent.Field { 106 | return []ent.Field{field.String("id").StorageKey("name"), field.String("last_name").Optional().Unique().Comment("not so boring")} 107 | }`, 108 | }, 109 | expectedEdges: map[string]string{ 110 | `user`: `func (User) Edges() []ent.Edge { 111 | return nil 112 | }`, 113 | }, 114 | entities: []string{"user"}, 115 | }, 116 | { 117 | name: "non_default_primary_key_with_indexes", 118 | mock: MockPostgresNonDefaultPrimaryKeyWithIndexes(), 119 | expectedFields: map[string]string{ 120 | "user": `func (User) Fields() []ent.Field { 121 | return []ent.Field{field.String("id").Unique().StorageKey("my_id")} 122 | }`, 123 | }, 124 | expectedEdges: map[string]string{ 125 | `user`: `func (User) Edges() []ent.Edge { 126 | return nil 127 | }`, 128 | }, 129 | entities: []string{"user"}, 130 | }, 131 | { 132 | name: "relation_m2m_two_types", 133 | mock: MockPostgresM2MTwoTypes(), 134 | expectedFields: map[string]string{ 135 | "user": `func (User) Fields() []ent.Field { 136 | return []ent.Field{field.Int("id"), field.Int("age"), field.String("name")} 137 | }`, 138 | "group": `func (Group) Fields() []ent.Field { 139 | return []ent.Field{field.Int("id"), field.String("name")} 140 | }`, 141 | }, 142 | expectedEdges: map[string]string{ 143 | "user": `func (User) Edges() []ent.Edge { 144 | return []ent.Edge{edge.From("groups", Group.Type).Ref("users")} 145 | }`, 146 | "group": `func (Group) Edges() []ent.Edge { 147 | return []ent.Edge{edge.To("users", User.Type)} 148 | }`, 149 | }, 150 | entities: []string{"user", "group"}, 151 | }, 152 | { 153 | name: "relation_m2m_same_type", 154 | mock: MockPostgresM2MSameType(), 155 | expectedFields: map[string]string{ 156 | "user": `func (User) Fields() []ent.Field { 157 | return []ent.Field{field.Int("id"), field.Int("age"), field.String("name")} 158 | }`, 159 | }, 160 | expectedEdges: map[string]string{ 161 | "user": `func (User) Edges() []ent.Edge { 162 | return []ent.Edge{edge.To("child_users", User.Type), edge.From("parent_users", User.Type).Ref("child_users")} 163 | }`, 164 | }, 165 | entities: []string{"user"}, 166 | }, 167 | { 168 | name: "relation_m2m_bidirectional", 169 | mock: MockPostgresM2MBidirectional(), 170 | expectedFields: map[string]string{ 171 | "user": `func (User) Fields() []ent.Field { 172 | return []ent.Field{field.Int("id"), field.Int("age"), field.String("name")} 173 | }`, 174 | }, 175 | expectedEdges: map[string]string{ 176 | "user": `func (User) Edges() []ent.Edge { 177 | return []ent.Edge{edge.To("child_users", User.Type), edge.From("parent_users", User.Type).Ref("child_users")} 178 | }`, 179 | }, 180 | entities: []string{"user"}, 181 | }, 182 | { 183 | name: "relation_o2o_two_types", 184 | mock: MockPostgresO2OTwoTypes(), 185 | expectedFields: map[string]string{ 186 | "user": `func (User) Fields() []ent.Field { 187 | return []ent.Field{field.Int("id"), field.Int("age"), field.String("name")} 188 | }`, 189 | "card": `func (Card) Fields() []ent.Field { 190 | return []ent.Field{field.Int("id"), field.Time("expired"), field.String("number"), field.Int("user_card").Optional().Unique()} 191 | }`, 192 | }, 193 | expectedEdges: map[string]string{ 194 | "user": `func (User) Edges() []ent.Edge { 195 | return []ent.Edge{edge.To("card", Card.Type).Unique()} 196 | }`, 197 | "card": `func (Card) Edges() []ent.Edge { 198 | return []ent.Edge{edge.From("user", User.Type).Ref("card").Unique().Field("user_card")} 199 | }`, 200 | }, 201 | entities: []string{"user", "card"}, 202 | }, 203 | { 204 | name: "relation_o2o_same_type", 205 | mock: MockPostgresO2OSameType(), 206 | expectedFields: map[string]string{ 207 | "node": `func (Node) Fields() []ent.Field { 208 | return []ent.Field{field.Int("id"), field.Int("value"), field.Int("node_next").Optional().Unique()} 209 | }`, 210 | }, 211 | expectedEdges: map[string]string{ 212 | "node": `func (Node) Edges() []ent.Edge { 213 | return []ent.Edge{edge.To("child_node", Node.Type).Unique(), edge.From("parent_node", Node.Type).Ref("child_node").Unique().Field("node_next")} 214 | }`, 215 | }, 216 | entities: []string{"node"}, 217 | }, 218 | { 219 | name: "relation_o2o_bidirectional", 220 | mock: MockPostgresO2OBidirectional(), 221 | expectedFields: map[string]string{ 222 | "user": `func (User) Fields() []ent.Field { 223 | return []ent.Field{field.Int("id"), field.Int("age"), field.String("name"), field.Int("user_spouse").Optional().Unique()} 224 | }`, 225 | }, 226 | expectedEdges: map[string]string{ 227 | "user": `func (User) Edges() []ent.Edge { 228 | return []ent.Edge{edge.To("child_user", User.Type).Unique(), edge.From("parent_user", User.Type).Ref("child_user").Unique().Field("user_spouse")} 229 | }`, 230 | }, 231 | entities: []string{"user"}, 232 | }, 233 | { 234 | name: "relation_o2m_two_types", 235 | mock: MockPostgresO2MTwoTypes(), 236 | expectedFields: map[string]string{ 237 | "user": `func (User) Fields() []ent.Field { 238 | return []ent.Field{field.Int("id"), field.Int("age"), field.String("name")} 239 | }`, 240 | "pet": `func (Pet) Fields() []ent.Field { 241 | return []ent.Field{field.Int("id"), field.String("name"), field.Int("user_pets").Optional()} 242 | }`, 243 | }, 244 | expectedEdges: map[string]string{ 245 | "user": `func (User) Edges() []ent.Edge { 246 | return []ent.Edge{edge.To("pets", Pet.Type)} 247 | }`, 248 | "pet": `func (Pet) Edges() []ent.Edge { 249 | return []ent.Edge{edge.From("user", User.Type).Ref("pets").Unique().Field("user_pets")} 250 | }`, 251 | }, 252 | entities: []string{"user", "pet"}, 253 | }, 254 | { 255 | name: "relation_o2m_same_type", 256 | mock: MockPostgresO2MSameType(), 257 | expectedFields: map[string]string{ 258 | "node": `func (Node) Fields() []ent.Field { 259 | return []ent.Field{field.Int("id"), field.Int("value"), field.Int("node_children").Optional()} 260 | }`, 261 | }, 262 | expectedEdges: map[string]string{ 263 | "node": `func (Node) Edges() []ent.Edge { 264 | return []ent.Edge{edge.To("child_nodes", Node.Type), edge.From("parent_node", Node.Type).Ref("child_nodes").Unique().Field("node_children")} 265 | }`, 266 | }, 267 | entities: []string{"node"}, 268 | }, 269 | { 270 | name: "relation_o2x_other_side_ignored", 271 | mock: MockPostgresO2XOtherSideIgnored(), 272 | expectedFields: map[string]string{ 273 | "pet": `func (Pet) Fields() []ent.Field { 274 | return []ent.Field{field.Int("id"), field.String("name"), field.Int("user_pets").Optional()} 275 | }`, 276 | }, 277 | expectedEdges: map[string]string{ 278 | "pet": `func (Pet) Edges() []ent.Edge { 279 | return nil 280 | }`, 281 | }, 282 | entities: []string{"pet"}, 283 | }, 284 | } 285 | for _, tt := range tests { 286 | t.Run(tt.name, func(t *testing.T) { 287 | schemas := createTempDir(t) 288 | m := mockMux(ctx, dialect.Postgres, tt.mock, testSchema) 289 | drv, err := m.OpenImport("postgres://postgres:pass@localhost:5434/test") 290 | r.NoError(err) 291 | importer, err := entimport.NewImport( 292 | entimport.WithDriver(drv), 293 | ) 294 | r.NoError(err) 295 | mutations, err := importer.SchemaMutations(ctx) 296 | r.NoError(err) 297 | err = entimport.WriteSchema(mutations, entimport.WithSchemaPath(schemas)) 298 | r.NoError(err) 299 | actualFiles := readDir(t, schemas) 300 | r.EqualValues(len(tt.entities), len(actualFiles)) 301 | for _, e := range tt.entities { 302 | f, err := parser.ParseFile(token.NewFileSet(), "", actualFiles[e+".go"], 0) 303 | r.NoError(err) 304 | typeName := inflect.Camelize(e) 305 | fieldMethod := lookupMethod(f, typeName, "Fields") 306 | r.NotNil(fieldMethod) 307 | var actualFields bytes.Buffer 308 | err = printer.Fprint(&actualFields, token.NewFileSet(), fieldMethod) 309 | r.NoError(err) 310 | r.EqualValues(tt.expectedFields[e], actualFields.String()) 311 | edgeMethod := lookupMethod(f, typeName, "Edges") 312 | r.NotNil(edgeMethod) 313 | var actualEdges bytes.Buffer 314 | err = printer.Fprint(&actualEdges, token.NewFileSet(), edgeMethod) 315 | r.NoError(err) 316 | r.EqualValues(tt.expectedEdges[e], actualEdges.String()) 317 | } 318 | }) 319 | } 320 | } 321 | 322 | func TestPostgresJoinTableOnly(t *testing.T) { 323 | var ctx = context.Background() 324 | m := mockMux(ctx, dialect.Postgres, MockPostgresM2MJoinTableOnly(), "public") 325 | drv, err := m.OpenImport("postgres://postgres:pass@localhost:5434/test") 326 | require.NoError(t, err) 327 | importer, err := entimport.NewImport( 328 | entimport.WithDriver(drv), 329 | ) 330 | require.NoError(t, err) 331 | mutations, err := importer.SchemaMutations(ctx) 332 | require.Empty(t, mutations) 333 | require.Errorf(t, err, "join tables must be inspected with ref tables - append `tables` flag") 334 | } 335 | -------------------------------------------------------------------------------- /internal/integration/README.md: -------------------------------------------------------------------------------- 1 | ## Integration Tests 2 | 3 | #### Running the integration tests 4 | 5 | ```shell 6 | docker-compose -f compose/docker-compose.yaml up -d 7 | go test 8 | ``` 9 | 10 | 11 | -------------------------------------------------------------------------------- /internal/integration/compose/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | 5 | mysql8: 6 | platform: linux/amd64 7 | image: mysql 8 | environment: 9 | MYSQL_DATABASE: test 10 | MYSQL_ROOT_PASSWORD: pass 11 | healthcheck: 12 | test: mysqladmin ping -ppass 13 | ports: 14 | - "3306:3306" 15 | 16 | postgres: 17 | platform: linux/amd64 18 | image: postgres 19 | environment: 20 | POSTGRES_DB: test 21 | POSTGRES_PASSWORD: pass 22 | healthcheck: 23 | test: pg_isready -U postgres 24 | ports: 25 | - "5432:5432" 26 | -------------------------------------------------------------------------------- /internal/integration/integration_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "database/sql" 7 | "go/ast" 8 | "go/parser" 9 | "go/printer" 10 | "go/token" 11 | "io/ioutil" 12 | "os" 13 | "path/filepath" 14 | "testing" 15 | 16 | "ariga.io/entimport/internal/entimport" 17 | "ariga.io/entimport/internal/mux" 18 | 19 | "entgo.io/ent/dialect" 20 | "github.com/go-openapi/inflect" 21 | _ "github.com/go-sql-driver/mysql" 22 | "github.com/stretchr/testify/require" 23 | ) 24 | 25 | func TestMySQL(t *testing.T) { 26 | var ( 27 | r = require.New(t) 28 | ctx = context.Background() 29 | dsn = "root:pass@tcp(localhost:3306)/test?parseTime=True&multiStatements=true" 30 | ) 31 | var tests = []struct { 32 | name string 33 | query string 34 | entities []string 35 | expectedFields map[string]string 36 | expectedEdges map[string]string 37 | }{ 38 | { 39 | name: "one table", 40 | // language=MySQL 41 | query: ` 42 | create table users 43 | ( 44 | id bigint auto_increment primary key, 45 | age bigint not null, 46 | name varchar(255) not null 47 | ); 48 | `, 49 | expectedFields: map[string]string{ 50 | "user": `func (User) Fields() []ent.Field { 51 | return []ent.Field{field.Int("id"), field.Int("age"), field.String("name")} 52 | }`, 53 | }, 54 | expectedEdges: map[string]string{ 55 | "user": `func (User) Edges() []ent.Edge { 56 | return nil 57 | }`, 58 | }, 59 | entities: []string{"user"}, 60 | }, 61 | { 62 | name: "int8 and int16 field types", 63 | // language=MySQL 64 | query: ` 65 | create table field_type_small_int 66 | ( 67 | id bigint auto_increment primary key, 68 | int_8 tinyint not null, 69 | int16 smallint not null, 70 | optional_int8 tinyint null, 71 | optional_int16 smallint null, 72 | nillable_int8 tinyint null, 73 | nillable_int16 smallint null, 74 | optional_uint8 tinyint unsigned null, 75 | optional_uint16 smallint unsigned null 76 | ); 77 | `, 78 | expectedFields: map[string]string{ 79 | "field_type_small_int": `func (FieldTypeSmallInt) Fields() []ent.Field { 80 | return []ent.Field{field.Int("id"), field.Int8("int_8"), field.Int16("int16"), field.Int8("optional_int8").Optional(), field.Int16("optional_int16").Optional(), field.Int8("nillable_int8").Optional(), field.Int16("nillable_int16").Optional(), field.Uint8("optional_uint8").Optional(), field.Uint16("optional_uint16").Optional()} 81 | }`, 82 | }, 83 | expectedEdges: map[string]string{ 84 | "field_type_small_int": `func (FieldTypeSmallInt) Edges() []ent.Edge { 85 | return nil 86 | }`, 87 | }, 88 | entities: []string{"field_type_small_int"}, 89 | }, 90 | { 91 | name: "int32 and int64 field types", 92 | // language=MySQL 93 | query: ` 94 | create table field_type_int 95 | ( 96 | id bigint auto_increment primary key, 97 | int_field bigint not null, 98 | int32 int not null, 99 | int64 bigint not null, 100 | optional_int bigint null, 101 | optional_int32 int null, 102 | optional_int64 bigint null, 103 | nillable_int bigint null, 104 | nillable_int32 int null, 105 | nillable_int64 bigint null, 106 | validate_optional_int32 int null, 107 | optional_uint bigint unsigned null, 108 | optional_uint32 int unsigned null, 109 | optional_uint64 bigint unsigned null 110 | ); 111 | `, 112 | expectedFields: map[string]string{ 113 | "field_type_int": `func (FieldTypeInt) Fields() []ent.Field { 114 | return []ent.Field{field.Int("id"), field.Int("int_field"), field.Int32("int32"), field.Int("int64"), field.Int("optional_int").Optional(), field.Int32("optional_int32").Optional(), field.Int("optional_int64").Optional(), field.Int("nillable_int").Optional(), field.Int32("nillable_int32").Optional(), field.Int("nillable_int64").Optional(), field.Int32("validate_optional_int32").Optional(), field.Uint64("optional_uint").Optional(), field.Uint32("optional_uint32").Optional(), field.Uint64("optional_uint64").Optional()} 115 | }`, 116 | }, 117 | expectedEdges: map[string]string{ 118 | "field_type_int": `func (FieldTypeInt) Edges() []ent.Edge { 119 | return nil 120 | }`, 121 | }, 122 | entities: []string{"field_type_int"}, 123 | }, 124 | { 125 | name: "float field types", 126 | // language=MySQL 127 | query: `create table field_type_float 128 | ( 129 | id bigint auto_increment primary key, 130 | float_field float not null, 131 | optional_float float null, 132 | double_field double not null, 133 | optional_double double null, 134 | decimal_field decimal not null, 135 | optional_decimal decimal null 136 | ); 137 | `, 138 | expectedFields: map[string]string{ 139 | "field_type_float": `func (FieldTypeFloat) Fields() []ent.Field { 140 | return []ent.Field{field.Int("id"), field.Float32("float_field"), field.Float32("optional_float").Optional(), field.Float("double_field"), field.Float("optional_double").Optional(), field.Float("decimal_field"), field.Float("optional_decimal").Optional()} 141 | }`, 142 | }, 143 | expectedEdges: map[string]string{ 144 | "field_type_float": `func (FieldTypeFloat) Edges() []ent.Edge { 145 | return nil 146 | }`, 147 | }, 148 | entities: []string{"field_type_float"}, 149 | }, 150 | { 151 | name: "enum field types", 152 | // language=MySQL 153 | query: ` 154 | create table field_type_enum 155 | ( 156 | id bigint auto_increment primary key, 157 | enum_field enum ('on', 'off') null, 158 | enum_field_default enum ('ADMIN', 'OWNER', 'USER', 'READ', 'WRITE') default 'READ' not null 159 | ); 160 | `, 161 | expectedFields: map[string]string{ 162 | "field_type_enum": `func (FieldTypeEnum) Fields() []ent.Field { 163 | return []ent.Field{field.Int("id"), field.Enum("enum_field").Optional().Values("on", "off"), field.Enum("enum_field_default").Values("ADMIN", "OWNER", "USER", "READ", "WRITE")} 164 | }`, 165 | }, 166 | expectedEdges: map[string]string{ 167 | "field_type_enum": `func (FieldTypeEnum) Edges() []ent.Edge { 168 | return nil 169 | }`, 170 | }, 171 | entities: []string{"field_type_enum"}, 172 | }, 173 | { 174 | name: "other field types", 175 | // language=MySQL 176 | query: ` 177 | create table field_type_other 178 | ( 179 | id bigint auto_increment primary key, 180 | datetime datetime null, 181 | string varchar(255) null, 182 | optional_string varchar(255) not null, 183 | bool tinyint(1) null, 184 | optional_bool tinyint(1) not null, 185 | ts timestamp null 186 | ); 187 | `, 188 | expectedFields: map[string]string{ 189 | "field_type_other": `func (FieldTypeOther) Fields() []ent.Field { 190 | return []ent.Field{field.Int("id"), field.Time("datetime").Optional(), field.String("string").Optional(), field.String("optional_string"), field.Bool("bool").Optional(), field.Bool("optional_bool"), field.Time("ts").Optional()} 191 | }`, 192 | }, 193 | expectedEdges: map[string]string{ 194 | "field_type_other": `func (FieldTypeOther) Edges() []ent.Edge { 195 | return nil 196 | }`, 197 | }, 198 | entities: []string{"field_type_other"}, 199 | }, 200 | { 201 | name: "o2o two types", 202 | // language=MySQL 203 | query: ` 204 | create table users 205 | ( 206 | id bigint auto_increment primary key, 207 | name varchar(255) not null 208 | ); 209 | 210 | create table cards 211 | ( 212 | id bigint auto_increment primary key, 213 | create_time timestamp not null, 214 | user_card bigint null, 215 | constraint user_card unique (user_card), 216 | constraint cards_users_card foreign key (user_card) references users (id) on delete set null 217 | ); 218 | 219 | create index card_id on cards (id); 220 | `, 221 | entities: []string{"user", "card"}, 222 | expectedFields: map[string]string{ 223 | "user": `func (User) Fields() []ent.Field { 224 | return []ent.Field{field.Int("id"), field.String("name")} 225 | }`, 226 | "card": `func (Card) Fields() []ent.Field { 227 | return []ent.Field{field.Int("id"), field.Time("create_time"), field.Int("user_card").Optional().Unique()} 228 | }`, 229 | }, 230 | expectedEdges: map[string]string{ 231 | "user": `func (User) Edges() []ent.Edge { 232 | return []ent.Edge{edge.To("card", Card.Type).Unique()} 233 | }`, 234 | "card": `func (Card) Edges() []ent.Edge { 235 | return []ent.Edge{edge.From("user", User.Type).Ref("card").Unique().Field("user_card")} 236 | }`, 237 | }, 238 | }, 239 | { 240 | name: "o2o same type", 241 | // language=MySQL 242 | query: ` 243 | create table nodes 244 | ( 245 | id bigint auto_increment primary key, 246 | value bigint null, 247 | node_next bigint null, 248 | constraint node_next unique (node_next), 249 | constraint nodes_nodes_next foreign key (node_next) references nodes (id) on delete set null 250 | ); 251 | `, 252 | expectedFields: map[string]string{ 253 | "node": `func (Node) Fields() []ent.Field { 254 | return []ent.Field{field.Int("id"), field.Int("value").Optional(), field.Int("node_next").Optional().Unique()} 255 | }`, 256 | }, 257 | expectedEdges: map[string]string{ 258 | "node": `func (Node) Edges() []ent.Edge { 259 | return []ent.Edge{edge.To("child_node", Node.Type).Unique(), edge.From("parent_node", Node.Type).Ref("child_node").Unique().Field("node_next")} 260 | }`, 261 | }, 262 | entities: []string{"node"}, 263 | }, 264 | { 265 | name: "o2o bidirectional", 266 | // language=MySQL 267 | query: ` 268 | create table users 269 | ( 270 | id bigint auto_increment primary key, 271 | name varchar(255) not null, 272 | nickname varchar(255) null, 273 | user_spouse bigint null, 274 | constraint nickname unique (nickname), 275 | constraint user_spouse unique (user_spouse), 276 | constraint users_users_spouse foreign key (user_spouse) references users (id) on delete set null 277 | ); 278 | `, 279 | expectedFields: map[string]string{ 280 | "user": `func (User) Fields() []ent.Field { 281 | return []ent.Field{field.Int("id"), field.String("name"), field.String("nickname").Optional().Unique(), field.Int("user_spouse").Optional().Unique()} 282 | }`, 283 | }, 284 | expectedEdges: map[string]string{ 285 | "user": `func (User) Edges() []ent.Edge { 286 | return []ent.Edge{edge.To("child_user", User.Type).Unique(), edge.From("parent_user", User.Type).Ref("child_user").Unique().Field("user_spouse")} 287 | }`, 288 | }, 289 | entities: []string{"user"}, 290 | }, 291 | { 292 | name: "o2m two types", 293 | // language=MySQL 294 | query: ` 295 | create table users 296 | ( 297 | id bigint auto_increment primary key, 298 | name varchar(255) not null 299 | ); 300 | 301 | create table pet 302 | ( 303 | id bigint auto_increment primary key, 304 | name varchar(255) not null, 305 | user_pets bigint null, 306 | constraint pet_users_pets foreign key (user_pets) references users (id) on delete set null 307 | ); 308 | 309 | create index pet_name_user_pets on pet (name, user_pets); 310 | `, 311 | expectedFields: map[string]string{ 312 | "user": `func (User) Fields() []ent.Field { 313 | return []ent.Field{field.Int("id"), field.String("name")} 314 | }`, 315 | "pet": `func (Pet) Fields() []ent.Field { 316 | return []ent.Field{field.Int("id"), field.String("name"), field.Int("user_pets").Optional()} 317 | }`, 318 | }, 319 | expectedEdges: map[string]string{ 320 | "user": `func (User) Edges() []ent.Edge { 321 | return []ent.Edge{edge.To("pets", Pet.Type)} 322 | }`, 323 | "pet": `func (Pet) Edges() []ent.Edge { 324 | return []ent.Edge{edge.From("user", User.Type).Ref("pets").Unique().Field("user_pets")} 325 | }`, 326 | }, 327 | entities: []string{"user", "pet"}, 328 | }, 329 | { 330 | name: "o2m same type", 331 | // language=MySQL 332 | query: ` 333 | create table users 334 | ( 335 | id bigint auto_increment 336 | primary key, 337 | name varchar(255) not null, 338 | user_parent bigint null, 339 | constraint users_users_parent foreign key (user_parent) references users (id) on delete set null 340 | ); 341 | `, 342 | expectedFields: map[string]string{ 343 | "user": `func (User) Fields() []ent.Field { 344 | return []ent.Field{field.Int("id"), field.String("name"), field.Int("user_parent").Optional()} 345 | }`, 346 | }, 347 | expectedEdges: map[string]string{ 348 | "user": `func (User) Edges() []ent.Edge { 349 | return []ent.Edge{edge.To("child_users", User.Type), edge.From("parent_user", User.Type).Ref("child_users").Unique().Field("user_parent")} 350 | }`, 351 | }, 352 | entities: []string{"user"}, 353 | }, 354 | { 355 | name: "m2m bidirectional", 356 | // language=MySQL 357 | query: ` 358 | create table users 359 | ( 360 | id bigint auto_increment 361 | primary key, 362 | age bigint not null, 363 | name varchar(255) not null 364 | ); 365 | 366 | create table user_friends 367 | ( 368 | user_id bigint not null, 369 | friend_id bigint not null, 370 | primary key (user_id, friend_id), 371 | constraint user_friends_friend_id foreign key (friend_id) references users (id) on delete cascade, 372 | constraint user_friends_user_id foreign key (user_id) references users (id) on delete cascade 373 | ); 374 | `, 375 | expectedFields: map[string]string{ 376 | "user": `func (User) Fields() []ent.Field { 377 | return []ent.Field{field.Int("id"), field.Int("age"), field.String("name")} 378 | }`, 379 | }, 380 | expectedEdges: map[string]string{ 381 | "user": `func (User) Edges() []ent.Edge { 382 | return []ent.Edge{edge.To("child_users", User.Type), edge.From("parent_users", User.Type).Ref("child_users")} 383 | }`, 384 | }, 385 | entities: []string{"user"}, 386 | }, 387 | { 388 | name: "m2m same type", 389 | // language=MySQL 390 | query: ` 391 | create table users 392 | ( 393 | id bigint auto_increment primary key, 394 | name varchar(255) not null 395 | ); 396 | 397 | create table user_following 398 | ( 399 | user_id bigint not null, 400 | follower_id bigint not null, 401 | primary key (user_id, follower_id), 402 | constraint user_following_follower_id foreign key (follower_id) references users (id) on delete cascade, 403 | constraint user_following_user_id foreign key (user_id) references users (id) on delete cascade 404 | ); 405 | `, 406 | expectedFields: map[string]string{ 407 | "user": `func (User) Fields() []ent.Field { 408 | return []ent.Field{field.Int("id"), field.String("name")} 409 | }`, 410 | }, 411 | expectedEdges: map[string]string{ 412 | "user": `func (User) Edges() []ent.Edge { 413 | return []ent.Edge{edge.To("child_users", User.Type), edge.From("parent_users", User.Type).Ref("child_users")} 414 | }`, 415 | }, 416 | entities: []string{"user"}, 417 | }, 418 | { 419 | // Demonstrate M2M relation between two different types. User and groups. 420 | name: "m2m two types", 421 | // language=MySQL 422 | query: ` 423 | create table some_groups 424 | ( 425 | id bigint auto_increment primary key, 426 | active tinyint(1) default 1 not null, 427 | name varchar(255) not null 428 | ); 429 | 430 | create table users 431 | ( 432 | id bigint auto_increment primary key, 433 | name varchar(255) not null 434 | ); 435 | 436 | create table user_groups 437 | ( 438 | user_id bigint not null, 439 | group_id bigint not null, 440 | primary key (user_id, group_id), 441 | constraint user_groups_some_groups_id foreign key (group_id) references some_groups (id) on delete cascade, 442 | constraint user_groups_user_id foreign key (user_id) references users (id) on delete cascade 443 | ); 444 | `, 445 | expectedFields: map[string]string{ 446 | "user": `func (User) Fields() []ent.Field { 447 | return []ent.Field{field.Int("id"), field.String("name")} 448 | }`, 449 | "some_group": `func (SomeGroup) Fields() []ent.Field { 450 | return []ent.Field{field.Int("id"), field.Bool("active"), field.String("name")} 451 | }`, 452 | }, 453 | expectedEdges: map[string]string{ 454 | "user": `func (User) Edges() []ent.Edge { 455 | return []ent.Edge{edge.From("some_groups", SomeGroup.Type).Ref("users")} 456 | }`, 457 | "some_group": `func (SomeGroup) Edges() []ent.Edge { 458 | return []ent.Edge{edge.To("users", User.Type)} 459 | }`, 460 | }, 461 | entities: []string{"user", "some_group"}, 462 | }, 463 | { 464 | name: "multiple relations", 465 | // language=MySQL 466 | query: ` 467 | create table group_infos 468 | ( 469 | id bigint auto_increment primary key, 470 | description varchar(255) not null, 471 | max_users bigint default 10000 not null 472 | ); 473 | 474 | create table some_groups 475 | ( 476 | id bigint auto_increment primary key, 477 | name varchar(255) not null, 478 | group_info bigint null, 479 | constraint groups_group_infos_info foreign key (group_info) references group_infos (id) on delete set null 480 | ); 481 | 482 | create table users 483 | ( 484 | id bigint auto_increment primary key, 485 | optional_int bigint null, 486 | name varchar(255) not null, 487 | group_blocked bigint null, 488 | constraint users_some_groups_blocked foreign key (group_blocked) references some_groups (id) on delete set null 489 | ); 490 | 491 | create table user_groups 492 | ( 493 | user_id bigint not null, 494 | group_id bigint not null, 495 | primary key (user_id, group_id), 496 | constraint user_groups_some_groups_id foreign key (group_id) references some_groups (id) on delete cascade, 497 | constraint user_groups_user_id foreign key (user_id) references users (id) on delete cascade 498 | ); 499 | `, 500 | expectedFields: map[string]string{ 501 | "user": `func (User) Fields() []ent.Field { 502 | return []ent.Field{field.Int("id"), field.Int("optional_int").Optional(), field.String("name"), field.Int("group_blocked").Optional()} 503 | }`, 504 | "group_info": `func (GroupInfo) Fields() []ent.Field { 505 | return []ent.Field{field.Int("id"), field.String("description"), field.Int("max_users")} 506 | }`, 507 | "some_group": `func (SomeGroup) Fields() []ent.Field { 508 | return []ent.Field{field.Int("id"), field.String("name"), field.Int("group_info_id").Optional()} 509 | }`, 510 | }, 511 | expectedEdges: map[string]string{ 512 | "user": `func (User) Edges() []ent.Edge { 513 | return []ent.Edge{edge.From("some_groups", SomeGroup.Type).Ref("users"), edge.From("some_group", SomeGroup.Type).Ref("users").Unique().Field("group_blocked")} 514 | }`, 515 | "group_info": `func (GroupInfo) Edges() []ent.Edge { 516 | return []ent.Edge{edge.To("some_groups", SomeGroup.Type)} 517 | }`, 518 | "some_group": `func (SomeGroup) Edges() []ent.Edge { 519 | return []ent.Edge{edge.From("group_info", GroupInfo.Type).Ref("some_groups").Unique().Field("group_info_id"), edge.To("users", User.Type), edge.To("users", User.Type)} 520 | }`, 521 | }, 522 | entities: []string{"user", "group_info", "some_group"}, 523 | }, 524 | } 525 | db, err := sql.Open(dialect.MySQL, dsn) 526 | r.NoError(err) 527 | defer db.Close() 528 | r.NoError(db.Ping()) 529 | drv, err := mux.Default.OpenImport("mysql://" + dsn) 530 | r.NoError(err) 531 | defer drv.Close() 532 | si, err := entimport.NewImport( 533 | entimport.WithDriver(drv), 534 | ) 535 | r.NoError(err) 536 | 537 | for _, tt := range tests { 538 | t.Run(tt.name, func(t *testing.T) { 539 | r := require.New(t) 540 | dropMySQL(t, db) 541 | schemas := createTempDir(t) 542 | _, err := db.ExecContext(ctx, tt.query) 543 | r.NoError(err) 544 | mutations, err := si.SchemaMutations(ctx) 545 | r.NoError(err) 546 | err = entimport.WriteSchema(mutations, entimport.WithSchemaPath(schemas)) 547 | r.NoError(err) 548 | r.NotZero(tt.entities) 549 | actualFiles := readDir(t, schemas) 550 | r.EqualValues(len(tt.entities), len(actualFiles)) 551 | for _, e := range tt.entities { 552 | f, err := parser.ParseFile(token.NewFileSet(), "", actualFiles[e+".go"], 0) 553 | r.NoError(err) 554 | typeName := inflect.Camelize(e) 555 | fieldMethod := lookupMethod(f, typeName, "Fields") 556 | r.NotNil(fieldMethod) 557 | var actualFields bytes.Buffer 558 | err = printer.Fprint(&actualFields, token.NewFileSet(), fieldMethod) 559 | r.NoError(err) 560 | r.EqualValues(tt.expectedFields[e], actualFields.String()) 561 | edgeMethod := lookupMethod(f, typeName, "Edges") 562 | r.NotNil(edgeMethod) 563 | var actualEdges bytes.Buffer 564 | err = printer.Fprint(&actualEdges, token.NewFileSet(), edgeMethod) 565 | r.NoError(err) 566 | r.EqualValues(tt.expectedEdges[e], actualEdges.String()) 567 | } 568 | }) 569 | } 570 | } 571 | 572 | func TestPostgres(t *testing.T) { 573 | var ( 574 | r = require.New(t) 575 | ctx = context.Background() 576 | dsn = "postgres://postgres:pass@localhost:5432/test?sslmode=disable" 577 | ) 578 | tests := []struct { 579 | name string 580 | query string 581 | entities []string 582 | expectedFields map[string]string 583 | expectedEdges map[string]string 584 | }{ 585 | { 586 | name: "one table", 587 | // language=PostgreSQL 588 | query: ` 589 | create table users 590 | ( 591 | id bigint primary key, 592 | age bigint not null, 593 | name varchar(255) not null 594 | ) 595 | `, 596 | entities: []string{"user"}, 597 | expectedFields: map[string]string{ 598 | "user": `func (User) Fields() []ent.Field { 599 | return []ent.Field{field.Int("id"), field.Int("age"), field.String("name")} 600 | }`, 601 | }, 602 | expectedEdges: map[string]string{ 603 | "user": `func (User) Edges() []ent.Edge { 604 | return nil 605 | }`, 606 | }, 607 | }, 608 | { 609 | name: "field types - int8 and int16", 610 | // language=PostgreSQL 611 | query: ` 612 | create table field_types 613 | ( 614 | id bigint generated by default as identity 615 | constraint field_types_pkey 616 | primary key, 617 | int8 smallint not null, 618 | int16 smallint not null, 619 | optional_int8 smallint, 620 | optional_int16 smallint 621 | ); 622 | `, 623 | entities: []string{"field_type"}, 624 | expectedFields: map[string]string{ 625 | "field_type": `func (FieldType) Fields() []ent.Field { 626 | return []ent.Field{field.Int("id"), field.Int16("int8"), field.Int16("int16"), field.Int16("optional_int8").Optional(), field.Int16("optional_int16").Optional()} 627 | }`, 628 | }, 629 | expectedEdges: map[string]string{ 630 | "field_type": `func (FieldType) Edges() []ent.Edge { 631 | return nil 632 | }`, 633 | }, 634 | }, 635 | { 636 | name: "int32 and int64 field types", 637 | // language=PostgreSQL 638 | query: ` 639 | create table field_types 640 | ( 641 | id bigint generated by default as identity 642 | constraint field_types_pkey 643 | primary key, 644 | int bigint not null, 645 | int32 integer not null, 646 | int64 bigint not null, 647 | optional_int bigint, 648 | optional_int32 integer, 649 | optional_int64 bigint 650 | ); 651 | `, 652 | entities: []string{"field_type"}, 653 | expectedFields: map[string]string{ 654 | "field_type": `func (FieldType) Fields() []ent.Field { 655 | return []ent.Field{field.Int("id"), field.Int("int"), field.Int32("int32"), field.Int("int64"), field.Int("optional_int").Optional(), field.Int32("optional_int32").Optional(), field.Int("optional_int64").Optional()} 656 | }`, 657 | }, 658 | expectedEdges: map[string]string{ 659 | "field_type": `func (FieldType) Edges() []ent.Edge { 660 | return nil 661 | }`, 662 | }, 663 | }, 664 | { 665 | name: "float field types", 666 | // language=PostgreSQL 667 | query: ` 668 | create table field_types 669 | ( 670 | id bigint generated by default as identity 671 | constraint field_types_pkey 672 | primary key, 673 | optional_float64 double precision, 674 | optional_float32 real 675 | ); 676 | 677 | `, 678 | entities: []string{"field_type"}, 679 | expectedFields: map[string]string{ 680 | "field_type": `func (FieldType) Fields() []ent.Field { 681 | return []ent.Field{field.Int("id"), field.Float("optional_float64").Optional(), field.Float32("optional_float32").Optional()} 682 | }`, 683 | }, 684 | expectedEdges: map[string]string{ 685 | "field_type": `func (FieldType) Edges() []ent.Edge { 686 | return nil 687 | }`, 688 | }, 689 | }, 690 | { 691 | name: "other field types", 692 | // language=PostgreSQL 693 | query: ` 694 | create table field_types 695 | ( 696 | id bigint generated by default as identity 697 | constraint field_types_pkey 698 | primary key, 699 | datetime date, 700 | decimal numeric, 701 | string varchar not null, 702 | optional_string varchar, 703 | bool boolean, 704 | ts timestamp with time zone, 705 | string_default varchar default 'READ':: character varying not null 706 | ); 707 | `, 708 | entities: []string{"field_type"}, 709 | expectedFields: map[string]string{ 710 | "field_type": `func (FieldType) Fields() []ent.Field { 711 | return []ent.Field{field.Int("id"), field.Time("datetime").Optional(), field.Float("decimal").Optional(), field.String("string"), field.String("optional_string").Optional(), field.Bool("bool").Optional(), field.Time("ts").Optional(), field.String("string_default")} 712 | }`, 713 | }, 714 | expectedEdges: map[string]string{ 715 | "field_type": `func (FieldType) Edges() []ent.Edge { 716 | return nil 717 | }`, 718 | }, 719 | }, 720 | { 721 | // See https://entgo.io/docs/schema-edges#relationship. 722 | name: "o2o two types", 723 | // language=PostgreSQL 724 | query: ` 725 | create table users 726 | ( 727 | id bigint generated by default as identity 728 | constraint users_pkey primary key, 729 | optional_int bigint, 730 | name varchar not null, 731 | nickname varchar 732 | constraint users_nickname_key unique, 733 | role varchar default 'user':: character varying not null 734 | ); 735 | 736 | create table cards 737 | ( 738 | id bigint generated by default as identity 739 | constraint cards_pkey 740 | primary key, 741 | create_time timestamp with time zone not null, 742 | balance double precision default 0 not null, 743 | name varchar, 744 | user_card bigint 745 | constraint cards_user_card_key 746 | unique 747 | constraint cards_users_card 748 | references users 749 | on delete set null 750 | ); 751 | 752 | create index card_id on cards (id); 753 | `, 754 | entities: []string{"user", "card"}, 755 | expectedFields: map[string]string{ 756 | "user": `func (User) Fields() []ent.Field { 757 | return []ent.Field{field.Int("id"), field.Int("optional_int").Optional(), field.String("name"), field.String("nickname").Optional().Unique(), field.String("role")} 758 | }`, 759 | "card": `func (Card) Fields() []ent.Field { 760 | return []ent.Field{field.Int("id"), field.Time("create_time"), field.Float("balance"), field.String("name").Optional(), field.Int("user_card").Optional().Unique()} 761 | }`, 762 | }, 763 | expectedEdges: map[string]string{ 764 | "user": `func (User) Edges() []ent.Edge { 765 | return []ent.Edge{edge.To("card", Card.Type).Unique()} 766 | }`, 767 | "card": `func (Card) Edges() []ent.Edge { 768 | return []ent.Edge{edge.From("user", User.Type).Ref("card").Unique().Field("user_card")} 769 | }`, 770 | }, 771 | }, 772 | { 773 | name: "o2o same type", 774 | // language=PostgreSQL 775 | query: ` 776 | create table nodes 777 | ( 778 | id bigint generated by default as identity 779 | constraint nodes_pkey primary key, 780 | value bigint, 781 | node_next bigint 782 | constraint nodes_node_next_key unique 783 | constraint nodes_nodes_next 784 | references nodes 785 | on delete set null 786 | ); 787 | `, 788 | entities: []string{"node"}, 789 | expectedFields: map[string]string{ 790 | "node": `func (Node) Fields() []ent.Field { 791 | return []ent.Field{field.Int("id"), field.Int("value").Optional(), field.Int("node_next").Optional().Unique()} 792 | }`, 793 | }, 794 | expectedEdges: map[string]string{ 795 | "node": `func (Node) Edges() []ent.Edge { 796 | return []ent.Edge{edge.To("child_node", Node.Type).Unique(), edge.From("parent_node", Node.Type).Ref("child_node").Unique().Field("node_next")} 797 | }`, 798 | }, 799 | }, 800 | { 801 | name: "o2o bidirectional", 802 | // language=PostgreSQL 803 | query: ` 804 | create table users 805 | ( 806 | id bigint generated by default as identity 807 | constraint users_pkey 808 | primary key, 809 | name varchar not null, 810 | user_spouse bigint 811 | constraint users_user_spouse_key 812 | unique 813 | constraint users_users_spouse 814 | references users 815 | on delete set null 816 | ); 817 | 818 | `, 819 | entities: []string{"user"}, 820 | expectedFields: map[string]string{ 821 | "user": `func (User) Fields() []ent.Field { 822 | return []ent.Field{field.Int("id"), field.String("name"), field.Int("user_spouse").Optional().Unique()} 823 | }`, 824 | }, 825 | expectedEdges: map[string]string{ 826 | "user": `func (User) Edges() []ent.Edge { 827 | return []ent.Edge{edge.To("child_user", User.Type).Unique(), edge.From("parent_user", User.Type).Ref("child_user").Unique().Field("user_spouse")} 828 | }`, 829 | }, 830 | }, 831 | { 832 | name: "o2m two types", 833 | // language=PostgreSQL 834 | query: ` 835 | create table users 836 | ( 837 | id bigint generated by default as identity 838 | constraint users_pkey primary key, 839 | name varchar not null 840 | ); 841 | 842 | create table pet 843 | ( 844 | id bigint generated by default as identity 845 | constraint pet_pkey 846 | primary key, 847 | name varchar not null, 848 | user_pets bigint 849 | constraint pet_users_pets 850 | references users 851 | on delete set null 852 | ); 853 | 854 | create index pet_name_user_pets 855 | on pet (name, user_pets); 856 | `, 857 | entities: []string{"user", "pet"}, 858 | expectedFields: map[string]string{ 859 | "user": `func (User) Fields() []ent.Field { 860 | return []ent.Field{field.Int("id"), field.String("name")} 861 | }`, 862 | "pet": `func (Pet) Fields() []ent.Field { 863 | return []ent.Field{field.Int("id"), field.String("name"), field.Int("user_pets").Optional()} 864 | }`, 865 | }, 866 | expectedEdges: map[string]string{ 867 | "user": `func (User) Edges() []ent.Edge { 868 | return []ent.Edge{edge.To("pets", Pet.Type)} 869 | }`, 870 | "pet": `func (Pet) Edges() []ent.Edge { 871 | return []ent.Edge{edge.From("user", User.Type).Ref("pets").Unique().Field("user_pets")} 872 | }`, 873 | }, 874 | }, 875 | { 876 | name: "o2m same type", 877 | // language=PostgreSQL 878 | query: ` 879 | create table users 880 | ( 881 | id bigint generated by default as identity 882 | constraint users_pkey 883 | primary key, 884 | name varchar not null, 885 | user_parent bigint 886 | constraint users_users_parent 887 | references users 888 | on delete set null 889 | ); 890 | `, 891 | entities: []string{"user"}, 892 | expectedFields: map[string]string{ 893 | "user": `func (User) Fields() []ent.Field { 894 | return []ent.Field{field.Int("id"), field.String("name"), field.Int("user_parent").Optional()} 895 | }`, 896 | }, 897 | expectedEdges: map[string]string{ 898 | "user": `func (User) Edges() []ent.Edge { 899 | return []ent.Edge{edge.To("child_users", User.Type), edge.From("parent_user", User.Type).Ref("child_users").Unique().Field("user_parent")} 900 | }`, 901 | }, 902 | }, 903 | { 904 | name: "m2m bidirectional", 905 | // language=PostgreSQL 906 | query: ` 907 | create table users 908 | ( 909 | id bigint generated by default as identity 910 | constraint users_pkey 911 | primary key, 912 | name varchar not null 913 | ); 914 | 915 | create table user_friends 916 | ( 917 | user_id bigint not null 918 | constraint user_friends_user_id 919 | references users 920 | on delete cascade, 921 | friend_id bigint not null 922 | constraint user_friends_friend_id 923 | references users 924 | on delete cascade, 925 | constraint user_friends_pkey 926 | primary key (user_id, friend_id) 927 | ); 928 | 929 | `, 930 | entities: []string{"user"}, 931 | expectedFields: map[string]string{ 932 | "user": `func (User) Fields() []ent.Field { 933 | return []ent.Field{field.Int("id"), field.String("name")} 934 | }`, 935 | }, 936 | expectedEdges: map[string]string{ 937 | "user": `func (User) Edges() []ent.Edge { 938 | return []ent.Edge{edge.To("child_users", User.Type), edge.From("parent_users", User.Type).Ref("child_users")} 939 | }`, 940 | }, 941 | }, 942 | { 943 | name: "m2m same type", 944 | // language=PostgreSQL 945 | query: ` 946 | create table users 947 | ( 948 | id bigint generated by default as identity 949 | constraint users_pkey 950 | primary key, 951 | name varchar not null, 952 | last varchar default 'unknown'::character varying not null 953 | ); 954 | 955 | create table user_following 956 | ( 957 | user_id bigint not null 958 | constraint user_following_user_id 959 | references users 960 | on delete cascade, 961 | follower_id bigint not null 962 | constraint user_following_follower_id 963 | references users 964 | on delete cascade, 965 | constraint user_following_pkey 966 | primary key (user_id, follower_id) 967 | ); 968 | `, 969 | entities: []string{"user"}, 970 | expectedFields: map[string]string{ 971 | "user": `func (User) Fields() []ent.Field { 972 | return []ent.Field{field.Int("id"), field.String("name"), field.String("last")} 973 | }`, 974 | }, 975 | expectedEdges: map[string]string{ 976 | "user": `func (User) Edges() []ent.Edge { 977 | return []ent.Edge{edge.To("child_users", User.Type), edge.From("parent_users", User.Type).Ref("child_users")} 978 | }`, 979 | }, 980 | }, 981 | { 982 | name: "m2m two types", 983 | // language=PostgreSQL 984 | query: ` 985 | create table users 986 | ( 987 | id bigint generated by default as identity 988 | constraint users_pkey 989 | primary key, 990 | name varchar not null 991 | ); 992 | 993 | create table groups 994 | ( 995 | id bigint generated by default as identity 996 | constraint groups_pkey 997 | primary key, 998 | active boolean default true not null, 999 | name varchar not null 1000 | ); 1001 | 1002 | create table user_groups 1003 | ( 1004 | user_id bigint not null 1005 | constraint user_groups_user_id 1006 | references users 1007 | on delete cascade, 1008 | group_id bigint not null 1009 | constraint user_groups_group_id 1010 | references groups 1011 | on delete cascade, 1012 | constraint user_groups_pkey 1013 | primary key (user_id, group_id) 1014 | ); 1015 | `, 1016 | entities: []string{"user", "group"}, 1017 | expectedFields: map[string]string{ 1018 | "user": `func (User) Fields() []ent.Field { 1019 | return []ent.Field{field.Int("id"), field.String("name")} 1020 | }`, 1021 | "group": `func (Group) Fields() []ent.Field { 1022 | return []ent.Field{field.Int("id"), field.Bool("active"), field.String("name")} 1023 | }`, 1024 | }, 1025 | expectedEdges: map[string]string{ 1026 | "user": `func (User) Edges() []ent.Edge { 1027 | return []ent.Edge{edge.From("groups", Group.Type).Ref("users")} 1028 | }`, 1029 | "group": `func (Group) Edges() []ent.Edge { 1030 | return []ent.Edge{edge.To("users", User.Type)} 1031 | }`, 1032 | }, 1033 | }, 1034 | { 1035 | name: "multiple relations", 1036 | // language=PostgreSQL 1037 | query: ` 1038 | create table group_infos 1039 | ( 1040 | id bigint generated by default as identity 1041 | constraint group_infos_pkey primary key, 1042 | description varchar not null, 1043 | max_users bigint default 10000 not null 1044 | ); 1045 | 1046 | create table users 1047 | ( 1048 | id bigint generated by default as identity 1049 | constraint users_pkey 1050 | primary key, 1051 | name varchar not null 1052 | ); 1053 | 1054 | create table groups 1055 | ( 1056 | id bigint generated by default as identity 1057 | constraint groups_pkey 1058 | primary key, 1059 | name varchar not null, 1060 | group_info bigint 1061 | constraint groups_group_infos_info 1062 | references group_infos 1063 | on delete set null 1064 | ); 1065 | 1066 | create table user_groups 1067 | ( 1068 | user_id bigint not null 1069 | constraint user_groups_user_id 1070 | references users 1071 | on delete cascade, 1072 | group_id bigint not null 1073 | constraint user_groups_group_id 1074 | references groups 1075 | on delete cascade, 1076 | constraint user_groups_pkey 1077 | primary key (user_id, group_id) 1078 | ); 1079 | `, 1080 | entities: []string{"user", "group", "group_info"}, 1081 | expectedFields: map[string]string{ 1082 | "user": `func (User) Fields() []ent.Field { 1083 | return []ent.Field{field.Int("id"), field.String("name")} 1084 | }`, 1085 | "group": `func (Group) Fields() []ent.Field { 1086 | return []ent.Field{field.Int("id"), field.String("name"), field.Int("group_info_id").Optional()} 1087 | }`, 1088 | "group_info": `func (GroupInfo) Fields() []ent.Field { 1089 | return []ent.Field{field.Int("id"), field.String("description"), field.Int("max_users")} 1090 | }`, 1091 | }, 1092 | expectedEdges: map[string]string{ 1093 | "user": `func (User) Edges() []ent.Edge { 1094 | return []ent.Edge{edge.From("groups", Group.Type).Ref("users")} 1095 | }`, 1096 | "group": `func (Group) Edges() []ent.Edge { 1097 | return []ent.Edge{edge.From("group_info", GroupInfo.Type).Ref("groups").Unique().Field("group_info_id"), edge.To("users", User.Type)} 1098 | }`, 1099 | "group_info": `func (GroupInfo) Edges() []ent.Edge { 1100 | return []ent.Edge{edge.To("groups", Group.Type)} 1101 | }`, 1102 | }, 1103 | }, 1104 | } 1105 | 1106 | db, err := sql.Open(dialect.Postgres, dsn) 1107 | r.NoError(err) 1108 | defer db.Close() 1109 | r.NoError(db.Ping()) 1110 | drv, err := mux.Default.OpenImport(dsn) 1111 | r.NoError(err) 1112 | defer drv.Close() 1113 | si, err := entimport.NewImport( 1114 | entimport.WithDriver(drv), 1115 | ) 1116 | r.NoError(err) 1117 | 1118 | for _, tt := range tests { 1119 | t.Run(tt.name, func(t *testing.T) { 1120 | r := require.New(t) 1121 | dropPostgres(t, db) 1122 | schemas := createTempDir(t) 1123 | _, err := db.ExecContext(ctx, tt.query) 1124 | r.NoError(err) 1125 | mutations, err := si.SchemaMutations(ctx) 1126 | r.NoError(err) 1127 | err = entimport.WriteSchema(mutations, entimport.WithSchemaPath(schemas)) 1128 | r.NoError(err) 1129 | r.NotZero(tt.entities) 1130 | actualFiles := readDir(t, schemas) 1131 | r.EqualValues(len(tt.entities), len(actualFiles)) 1132 | for _, e := range tt.entities { 1133 | f, err := parser.ParseFile(token.NewFileSet(), "", actualFiles[e+".go"], 0) 1134 | r.NoError(err) 1135 | typeName := inflect.Camelize(e) 1136 | fieldMethod := lookupMethod(f, typeName, "Fields") 1137 | r.NotNil(fieldMethod) 1138 | var actualFields bytes.Buffer 1139 | err = printer.Fprint(&actualFields, token.NewFileSet(), fieldMethod) 1140 | r.NoError(err) 1141 | r.EqualValues(tt.expectedFields[e], actualFields.String()) 1142 | 1143 | edgeMethod := lookupMethod(f, typeName, "Edges") 1144 | r.NotNil(edgeMethod) 1145 | var actualEdges bytes.Buffer 1146 | err = printer.Fprint(&actualEdges, token.NewFileSet(), edgeMethod) 1147 | r.NoError(err) 1148 | r.EqualValues(tt.expectedEdges[e], actualEdges.String()) 1149 | } 1150 | }) 1151 | } 1152 | } 1153 | 1154 | func createTempDir(t *testing.T) string { 1155 | r := require.New(t) 1156 | tmpDir, err := ioutil.TempDir("", "entimport-*") 1157 | r.NoError(err) 1158 | t.Cleanup(func() { 1159 | err = os.RemoveAll(tmpDir) 1160 | r.NoError(err) 1161 | }) 1162 | return tmpDir 1163 | } 1164 | 1165 | func readDir(t *testing.T, path string) map[string]string { 1166 | files := make(map[string]string) 1167 | err := filepath.Walk(path, func(path string, info os.FileInfo, _ error) error { 1168 | if info.IsDir() { 1169 | return nil 1170 | } 1171 | buf, err := os.ReadFile(path) 1172 | if err != nil { 1173 | return err 1174 | } 1175 | files[filepath.Base(path)] = string(buf) 1176 | return nil 1177 | }) 1178 | require.NoError(t, err) 1179 | return files 1180 | } 1181 | 1182 | func dropMySQL(t *testing.T, db *sql.DB) { 1183 | r := require.New(t) 1184 | t.Log("drop data from database") 1185 | ctx := context.Background() 1186 | _, err := db.ExecContext(ctx, "DROP DATABASE IF EXISTS test") 1187 | r.NoError(err) 1188 | _, err = db.ExecContext(ctx, "CREATE DATABASE test") 1189 | r.NoError(err) 1190 | _, _ = db.ExecContext(ctx, "USE test") 1191 | } 1192 | 1193 | func dropPostgres(t *testing.T, db *sql.DB) { 1194 | r := require.New(t) 1195 | t.Log("drop data from database") 1196 | ctx := context.Background() 1197 | _, err := db.ExecContext(ctx, `DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public;`) 1198 | r.NoError(err) 1199 | } 1200 | 1201 | func lookupMethod(file *ast.File, typeName string, methodName string) (m *ast.FuncDecl) { 1202 | ast.Inspect(file, func(node ast.Node) bool { 1203 | if decl, ok := node.(*ast.FuncDecl); ok { 1204 | if decl.Name.Name != methodName || decl.Recv == nil || len(decl.Recv.List) != 1 { 1205 | return true 1206 | } 1207 | if id, ok := decl.Recv.List[0].Type.(*ast.Ident); ok && id.Name == typeName { 1208 | m = decl 1209 | return false 1210 | } 1211 | } 1212 | return true 1213 | }) 1214 | return m 1215 | } 1216 | -------------------------------------------------------------------------------- /internal/mux/mux.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | 8 | "ariga.io/atlas/sql/schema" 9 | ) 10 | 11 | type ( 12 | // importProvider - returns an ImportDriver for a given dialect. 13 | importProvider func(string) (*ImportDriver, error) 14 | 15 | // Mux is used for routing dsn to correct provider. 16 | Mux struct { 17 | providers map[string]importProvider 18 | } 19 | 20 | // ImportDriver implements Inspector interface and holds inspection information. 21 | ImportDriver struct { 22 | io.Closer 23 | schema.Inspector 24 | Dialect string 25 | SchemaName string 26 | } 27 | ) 28 | 29 | // New returns a new Mux. 30 | func New() *Mux { 31 | return &Mux{ 32 | providers: make(map[string]importProvider), 33 | } 34 | } 35 | 36 | var Default = New() 37 | 38 | // RegisterProvider is used to register an Atlas provider by key. 39 | func (u *Mux) RegisterProvider(p importProvider, scheme ...string) { 40 | for _, s := range scheme { 41 | u.providers[s] = p 42 | } 43 | } 44 | 45 | // OpenImport is used for opening an import driver on a specific data source. 46 | func (u *Mux) OpenImport(dsn string) (*ImportDriver, error) { 47 | scheme, host, err := parseDSN(dsn) 48 | if err != nil { 49 | return nil, fmt.Errorf("failed to parse DSN: %v", err) 50 | } 51 | p, ok := u.providers[scheme] 52 | if !ok { 53 | return nil, fmt.Errorf("provider does not exist: %q", scheme) 54 | } 55 | return p(host) 56 | } 57 | 58 | func parseDSN(url string) (string, string, error) { 59 | a := strings.SplitN(url, "://", 2) 60 | if len(a) != 2 { 61 | return "", "", fmt.Errorf(`failed to parse dsn: "%s"`, url) 62 | } 63 | return a[0], a[1], nil 64 | } 65 | -------------------------------------------------------------------------------- /internal/mux/provider.go: -------------------------------------------------------------------------------- 1 | package mux 2 | 3 | import ( 4 | "database/sql" 5 | "net/url" 6 | 7 | atlasmysql "ariga.io/atlas/sql/mysql" 8 | "ariga.io/atlas/sql/postgres" 9 | 10 | "entgo.io/ent/dialect" 11 | "github.com/go-sql-driver/mysql" 12 | ) 13 | 14 | func init() { 15 | Default.RegisterProvider(mysqlProvider, "mysql") 16 | Default.RegisterProvider(postgresProvider, "postgres", "postgresql") 17 | } 18 | 19 | func mysqlProvider(dsn string) (*ImportDriver, error) { 20 | db, err := sql.Open(dialect.MySQL, dsn) 21 | if err != nil { 22 | return nil, err 23 | } 24 | drv, err := atlasmysql.Open(db) 25 | if err != nil { 26 | return nil, err 27 | } 28 | // dsn example: root:pass@tcp(localhost:3308)/test?parseTime=True 29 | cfg, err := mysql.ParseDSN(dsn) 30 | if err != nil { 31 | return nil, err 32 | } 33 | return &ImportDriver{ 34 | Closer: db, 35 | Inspector: drv, 36 | Dialect: dialect.MySQL, 37 | SchemaName: cfg.DBName, 38 | }, nil 39 | } 40 | 41 | func postgresProvider(dsn string) (*ImportDriver, error) { 42 | dsn = "postgres://" + dsn 43 | db, err := sql.Open(dialect.Postgres, dsn) 44 | if err != nil { 45 | return nil, err 46 | } 47 | drv, err := postgres.Open(db) 48 | if err != nil { 49 | return nil, err 50 | } 51 | // dsn example: postgresql://user:pass@localhost:5432/atlas?search_path=some_schema 52 | parsed, err := url.Parse(dsn) 53 | if err != nil { 54 | return nil, err 55 | } 56 | schemaName := "public" 57 | if s := parsed.Query().Get("search_path"); s != "" { 58 | schemaName = s 59 | } 60 | return &ImportDriver{ 61 | Closer: db, 62 | Inspector: drv, 63 | Dialect: dialect.Postgres, 64 | SchemaName: schemaName, 65 | }, nil 66 | } 67 | --------------------------------------------------------------------------------