├── .gitattributes ├── .github └── workflows │ ├── client.yml │ ├── migrate.yml │ ├── postgresql.yml │ ├── release.yml │ ├── website.yml │ └── windows.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Makefile ├── README.md ├── README_zh.md ├── adapter ├── adapter.go ├── config.go ├── config_test.go ├── migration.go ├── migrator.go ├── mysql.go ├── postgresql.go └── sqlite.go ├── cmd └── queryx │ ├── action │ ├── db.go │ ├── format.go │ ├── generate.go │ ├── init.go │ ├── root.go │ └── version.go │ └── main.go ├── generator ├── client │ ├── golang │ │ ├── generator.go │ │ └── templates │ │ │ ├── [model].gotmpl │ │ │ ├── [model]_query.gotmpl │ │ │ ├── errors.gotmpl │ │ │ ├── queryx.gotmpl │ │ │ └── queryx │ │ │ ├── [model]_change.gotmpl │ │ │ ├── adapter.mysql.go │ │ │ ├── adapter.postgresql.go │ │ │ ├── adapter.sqlite.go │ │ │ ├── adapter_test.postgresql.go │ │ │ ├── bigint.go │ │ │ ├── bigint_column.go │ │ │ ├── bigint_test.go │ │ │ ├── bind.go │ │ │ ├── boolean.go │ │ │ ├── boolean_column.go │ │ │ ├── boolean_test.go │ │ │ ├── clause.go │ │ │ ├── clause_test.go │ │ │ ├── config.gotmpl │ │ │ ├── date.gotmpl │ │ │ ├── date_column.gotmpl │ │ │ ├── date_test.gotmpl │ │ │ ├── datetime.gotmpl │ │ │ ├── datetime_column.gotmpl │ │ │ ├── datetime_test.gotmpl │ │ │ ├── db.go │ │ │ ├── delete.go │ │ │ ├── delete_test.go │ │ │ ├── env.go │ │ │ ├── float.go │ │ │ ├── float_column.go │ │ │ ├── float_test.go │ │ │ ├── insert.gotmpl │ │ │ ├── insert_test.gotmpl │ │ │ ├── integer.go │ │ │ ├── integer_column.go │ │ │ ├── integer_test.go │ │ │ ├── json.go │ │ │ ├── json_column.go │ │ │ ├── json_test.go │ │ │ ├── logger.go │ │ │ ├── scan.go │ │ │ ├── schema.gotmpl │ │ │ ├── select.go │ │ │ ├── select_test.go │ │ │ ├── string.go │ │ │ ├── string_array.go │ │ │ ├── string_array_column.go │ │ │ ├── string_array_test.go │ │ │ ├── string_column.go │ │ │ ├── string_test.go │ │ │ ├── table.go │ │ │ ├── time.gotmpl │ │ │ ├── time_column.gotmpl │ │ │ ├── time_test.gotmpl │ │ │ ├── update.go │ │ │ ├── update_test.go │ │ │ ├── uuid.go │ │ │ ├── uuid_column.go │ │ │ └── uuid_test.go │ └── typescript │ │ ├── generator.go │ │ └── templates │ │ ├── [model] │ │ ├── [model].tstmpl │ │ ├── [model]_change.tstmpl │ │ ├── [model]_query.tstmpl │ │ └── index.tstmpl │ │ ├── index.tstmpl │ │ └── queryx │ │ ├── adapter.mysql.ts │ │ ├── adapter.postgresql.ts │ │ ├── adapter.sqlite.ts │ │ ├── clause.test.ts │ │ ├── clause.ts │ │ ├── client.tstmpl │ │ ├── config.tstmpl │ │ ├── delete.ts │ │ ├── index.ts │ │ ├── insert.tstmpl │ │ ├── select.test.ts │ │ ├── select.ts │ │ ├── table.ts │ │ └── update.ts └── generator.go ├── go.mod ├── go.sum ├── inflect ├── golang.go ├── golang_test.go ├── inflect.go ├── inflect_test.go └── typescript.go ├── install.sh ├── internal └── integration │ ├── client │ ├── client.test.ts │ ├── client_test.go │ ├── mysql.hcl │ ├── postgresql.hcl │ └── sqlite.hcl │ ├── migrate │ ├── mysql1.hcl │ ├── mysql2.hcl │ ├── postgresql1.hcl │ ├── postgresql2.hcl │ ├── sqlite1.hcl │ └── sqlite2.hcl │ ├── package.json │ ├── postgresql │ ├── client.test.ts │ ├── client_test.go │ └── schema.hcl │ ├── tsconfig.json │ └── yarn.lock ├── package.json ├── schema ├── config.go ├── database.go ├── dsl.go ├── dsl_test.go ├── hcl.go ├── model.go ├── mysql.go ├── postgresql.go ├── schema.go ├── schema_test.go └── sqlite.go ├── types └── string.go ├── website ├── .vitepress │ ├── config.mts │ └── theme │ │ └── index.ts ├── caitou.yml ├── docs │ ├── association.md │ ├── build-from-source.md │ ├── cli.md │ ├── custom-primary-key.md │ ├── custom-table-name.md │ ├── data-types.md │ ├── database-index.md │ ├── environment-variable.md │ ├── getting-started.md │ ├── has_many.png │ ├── has_many_through.png │ ├── has_one.png │ ├── primary-key.md │ ├── query-methods.md │ ├── raw-sql.md │ ├── time-zone.md │ ├── transaction.md │ └── what-is-queryx.md └── index.md └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | *.go linguist-language=Go linguist-generated=false 2 | *.gotmpl linguist-language=Go linguist-generated=false 3 | *.ts linguist-language=TypeScript linguist-generated=false 4 | *.tstmpl linguist-language=TypeScript linguist-generated=false 5 | -------------------------------------------------------------------------------- /.github/workflows/postgresql.yml: -------------------------------------------------------------------------------- 1 | name: postgresql 2 | 3 | env: 4 | QUERYX_ENV: test 5 | DATABASE_URL: postgresql://postgres:postgres@localhost:5432/queryx_test?sslmode=disable 6 | 7 | on: 8 | push: 9 | paths-ignore: 10 | - "website/**" 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-go@v4 17 | with: 18 | go-version: "1.20" 19 | - name: Test 20 | run: | 21 | go test $(go list ./... | grep -Ev "generator|internal") -race -coverprofile=coverage.txt -covermode=atomic 22 | - name: Build 23 | run: go build -o /usr/local/bin/queryx cmd/queryx/main.go 24 | - uses: actions/upload-artifact@v4 25 | with: 26 | name: bin 27 | path: /usr/local/bin/queryx 28 | golang: 29 | needs: [build] 30 | strategy: 31 | matrix: 32 | postgres: 33 | - "14.2" 34 | runs-on: ubuntu-latest 35 | services: 36 | postgres: 37 | image: postgres:${{matrix.postgres}} 38 | env: 39 | POSTGRES_PASSWORD: postgres 40 | POSTGRES_USER: postgres 41 | POSTGRES_DB: queryx_test 42 | ports: 43 | - 5432:5432 44 | options: >- 45 | --health-cmd pg_isready 46 | --health-interval 10s 47 | --health-timeout 5s 48 | --health-retries 5 49 | steps: 50 | - uses: actions/checkout@v3 51 | - uses: actions/download-artifact@v4 52 | with: 53 | name: bin 54 | path: /usr/local/bin 55 | - run: chmod a+x /usr/local/bin/queryx 56 | - name: generate 57 | run: | 58 | cd internal/integration/postgresql 59 | queryx db:migrate 60 | queryx generate 61 | - name: go test 62 | run: | 63 | cd internal/integration/postgresql 64 | go test -v ./... 65 | # typescript: 66 | # needs: [build] 67 | # runs-on: ubuntu-latest 68 | # services: 69 | # postgres: 70 | # image: postgres:14.2 71 | # env: 72 | # POSTGRES_PASSWORD: postgres 73 | # POSTGRES_USER: postgres 74 | # POSTGRES_DB: queryx_test 75 | # ports: 76 | # - 5432:5432 77 | # options: >- 78 | # --health-cmd pg_isready 79 | # --health-interval 10s 80 | # --health-timeout 5s 81 | # --health-retries 5 82 | # steps: 83 | # - uses: actions/checkout@v3 84 | # - uses: actions/download-artifact@v4 85 | # with: 86 | # name: bin 87 | # path: /usr/local/bin 88 | # - run: chmod a+x /usr/local/bin/queryx 89 | # - name: generate 90 | # run: | 91 | # cd internal/integration/postgresql 92 | # queryx db:migrate 93 | # queryx generate 94 | # - name: yarn install 95 | # run: | 96 | # cd internal/integration/postgresql 97 | # yarn install 98 | # - name: yarn test 99 | # run: | 100 | # cd internal/integration/postgresql 101 | # yarn test 102 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | jobs: 7 | release-windows: 8 | runs-on: windows-latest 9 | strategy: 10 | matrix: 11 | goarch: [amd64] 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-go@v5 15 | with: 16 | go-version: "stable" 17 | - name: Build 18 | shell: pwsh 19 | env: 20 | GOARCH: ${{ matrix.goarch }} 21 | CGO_ENABLED: 1 22 | run: | 23 | go build -o queryx.exe cmd/queryx/main.go 24 | tar zcvf queryx_${{ github.ref_name }}_windows_${{ matrix.goarch }}.tar.gz ./*.md LICENSE queryx.exe 25 | - name: Release 26 | uses: softprops/action-gh-release@v1 27 | with: 28 | draft: false 29 | prerelease: false 30 | files: | 31 | queryx_${{ github.ref_name }}_windows_${{ matrix.goarch }}.tar.gz 32 | - name: Checksums 33 | uses: wangzuo/action-release-checksums@v1 34 | release-linux: 35 | runs-on: ubuntu-latest 36 | steps: 37 | - name: Install GNU C compiler for the arm64 architecture 38 | run: sudo apt-get update && sudo apt-get -y install gcc-aarch64-linux-gnu 39 | - uses: actions/checkout@v4 40 | - uses: actions/setup-go@v5 41 | with: 42 | go-version: "stable" 43 | - name: Build 44 | env: 45 | CGO_ENABLED: 1 46 | run: | 47 | go build -o queryx cmd/queryx/main.go 48 | tar zcvf queryx_${{ github.ref_name }}_linux_amd64.tar.gz ./*.md LICENSE queryx 49 | GOARCH=arm64 CC=aarch64-linux-gnu-gcc go build -o queryx cmd/queryx/main.go 50 | tar zcvf queryx_${{ github.ref_name }}_linux_arm64.tar.gz ./*.md LICENSE queryx 51 | - name: Release 52 | uses: softprops/action-gh-release@v1 53 | with: 54 | draft: false 55 | prerelease: false 56 | files: | 57 | queryx_${{ github.ref_name }}_linux_amd64.tar.gz 58 | queryx_${{ github.ref_name }}_linux_arm64.tar.gz 59 | - name: Checksums 60 | uses: wangzuo/action-release-checksums@v1 61 | release-mac: 62 | runs-on: macos-latest 63 | strategy: 64 | matrix: 65 | goarch: [amd64, arm64] 66 | steps: 67 | - uses: actions/checkout@v4 68 | - uses: actions/setup-go@v5 69 | with: 70 | go-version: "stable" 71 | - name: Build 72 | env: 73 | GOARCH: ${{ matrix.goarch }} 74 | CGO_ENABLED: 1 75 | run: | 76 | go build -o queryx cmd/queryx/main.go 77 | tar zcvf queryx_${{ github.ref_name }}_darwin_${{ matrix.goarch }}.tar.gz ./*.md LICENSE queryx 78 | - name: Release 79 | uses: softprops/action-gh-release@v1 80 | with: 81 | draft: false 82 | prerelease: false 83 | files: | 84 | queryx_${{ github.ref_name }}_darwin_${{ matrix.goarch }}.tar.gz 85 | - name: Checksums 86 | uses: wangzuo/action-release-checksums@v1 87 | -------------------------------------------------------------------------------- /.github/workflows/website.yml: -------------------------------------------------------------------------------- 1 | name: website 2 | on: 3 | push: 4 | branches: [main] 5 | paths: 6 | - package.json 7 | - "website/**" 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - run: curl -sf https://cli.caitouyun.com/install.sh | sh 14 | - run: yarn 15 | - run: yarn website:build 16 | - run: | 17 | cd website && caitou deploy --git 18 | env: 19 | CAITOU_TOKEN: ${{ secrets.CAITOU_TOKEN }} 20 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | name: windows 2 | on: 3 | push: 4 | paths-ignore: 5 | - "website/**" 6 | jobs: 7 | sqlite-golang: 8 | runs-on: windows-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-go@v4 12 | with: 13 | go-version: "1.20" 14 | - name: Test 15 | run: | 16 | go test $(go list ./... | grep -Ev "generator|internal") -race -coverprofile=coverage.txt -covermode=atomic 17 | - name: Build 18 | run: go build -o queryx.exe cmd/queryx/main.go 19 | - name: generate 20 | env: 21 | QUERYX_ENV: test 22 | DATABASE_URL: sqlite:test.sqlite3 23 | run: | 24 | cd internal/integration/client 25 | ..\..\..\queryx.exe db:migrate --schema sqlite.hcl 26 | ..\..\..\queryx.exe db:migrate --schema sqlite.hcl 27 | ..\..\..\queryx.exe generate --schema sqlite.hcl 28 | - name: go test 29 | run: | 30 | cd internal/integration/client 31 | go test -v ./... 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | internal/integration/client/db 2 | internal/integration/postgresql/db 3 | internal/migrate/db 4 | node_modules 5 | /bin 6 | .idea 7 | *.sqlite3 8 | *.out 9 | node_modules 10 | website/.vitepress/dist 11 | website/.vitepress/cache 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## Unreleased 4 | 5 | ## 0.2.18 6 | 7 | - support string array in postgresql-golang 8 | 9 | ## 0.2.17 10 | 11 | - fix missing semicolon in migration sql files 12 | 13 | ## 0.2.16 14 | 15 | - add `db:schema:dump` to export database structure 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOFMT_FILES?=$$(find . -name '*.go') 2 | 3 | all: fmt build install 4 | 5 | fmt: 6 | gofmt -w $(GOFMT_FILES) 7 | 8 | build: fmt 9 | go build -ldflags "-X github.com/swiftcarrot/queryx/cmd/queryx/action.Version=`git rev-parse HEAD`" -o bin/queryx cmd/queryx/main.go 10 | 11 | install: build 12 | sudo install bin/queryx /usr/local/bin 13 | 14 | clean: 15 | rm bin/queryx 16 | 17 | test-postgresql: install 18 | rm -rf internal/integration/db 19 | cd internal/integration && queryx db:drop --schema postgresql.hcl 20 | cd internal/integration && queryx db:create --schema postgresql.hcl 21 | cd internal/integration && queryx db:migrate --schema postgresql.hcl 22 | cd internal/integration && queryx db:migrate --schema postgresql.hcl 23 | cd internal/integration && queryx generate --schema postgresql.hcl 24 | # cd internal/integration && yarn tsc 25 | cd internal/integration && yarn test 26 | cd internal/integration && go test ./... 27 | # cd internal/integration && queryx db:drop --schema postgresql.hcl 28 | 29 | test-mysql: install 30 | rm -rf internal/integration/db 31 | cd internal/integration && queryx db:drop --schema mysql.hcl 32 | cd internal/integration && queryx db:create --schema mysql.hcl 33 | cd internal/integration && queryx db:migrate --schema mysql.hcl 34 | cd internal/integration && queryx db:migrate --schema mysql.hcl 35 | cd internal/integration && queryx generate --schema mysql.hcl 36 | # cd internal/integration && yarn tsc 37 | cd internal/integration && yarn test 38 | # cd internal/integration && go test ./... 39 | 40 | test-sqlite: install 41 | rm -rf internal/integration/db 42 | cd internal/integration && queryx db:drop --schema sqlite.hcl 43 | cd internal/integration && queryx db:create --schema sqlite.hcl 44 | cd internal/integration && queryx db:migrate --schema sqlite.hcl 45 | cd internal/integration && queryx db:migrate --schema sqlite.hcl 46 | cd internal/integration && queryx generate --schema sqlite.hcl 47 | # cd internal/integration && yarn tsc 48 | cd internal/integration && yarn test 49 | cd internal/integration && go test ./... 50 | 51 | test: test-postgresql test-sqlite test-mysql 52 | 53 | test-migrate: install 54 | rm -rf internal/migrate/db internal/migrate/test.sqlite3 55 | cd internal/migrate && queryx db:migrate --schema sqlite1.hcl 56 | sleep 1 57 | cd internal/migrate && queryx db:migrate --schema sqlite2.hcl 58 | cd internal/migrate && sqlite3 test.sqlite3 "insert into users(name, email) values('test', 'test@example.com')" 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Queryx 2 | 3 | [English](README.md) | [中文](README_zh.md) 4 | 5 | Queryx is schema-first and type-safe ORM for Go and TypeScript. 6 | 7 | - **Schema First**: Queryx automatically migrates the database based on defined models in a queryx schema file. 8 | - **Type Safe**: Queryx generates friendly, type-safe ORM methods and come with autocomplete support and are free from type-related errors. 9 | - **Go and TypeScript**: Queryx supports generating both Go and TypeScript ORM methods. 10 | 11 | ## Quick Installation 12 | 13 | To easily install the latest version of queryx, open your terminal and run the following command: 14 | 15 | ```sh 16 | curl -sf https://raw.githubusercontent.com/swiftcarrot/queryx/main/install.sh | sh 17 | ``` 18 | 19 | You can also build queryx from the source following the instructions [here](https://queryx.caitouyun.com/docs/build-from-source). 20 | 21 | ## Documentation and Support 22 | 23 | Queryx documentation is available at: https://queryx.caitouyun.com 24 | 25 | Feel free to [open an issue](https://github.com/swiftcarrot/queryx/issues) or [start a discussion](https://github.com/swiftcarrot/queryx/discussions) if you have any questions. [Join our Discord community](https://discord.gg/QUTxjJBRfA) 26 | 27 | ## License 28 | 29 | Queryx is licensed under Apache 2.0 as found in the LICENSE file. 30 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # Queryx 2 | 3 | [English](README.md) | [中文](README_zh.md) 4 | 5 | Queryx 是针对 Go 和 TypeScript 的基于模式优先和类型安全的 ORM。 6 | 7 | - **模式优先**:Queryx 根据模式文件中定义的模型自动迁移数据库。 8 | - **类型安全**:Queryx 生成友好的、类型安全的 ORM 方法,并提供编辑器自动补全支持,免于类型相关错误。 9 | - **Go 和 TypeScript**:Queryx 支持生成 Go 和 TypeScript 的 ORM 方法。 10 | 11 | ## 快速安装 12 | 13 | 打开终端并运行以下命令来安装最新版本的 Queryx: 14 | 15 | ```sh 16 | curl -sf https://raw.githubusercontent.com/swiftcarrot/queryx/main/install.sh | sh 17 | ``` 18 | 19 | 也可以按照[这里](https://queryx.caitouyun.com/docs/build-from-source)的说明从源代码构建 Queryx. 20 | 21 | ## 文档和支持 22 | 23 | Queryx 的文档可在以下网址找到:https://queryx.caitouyun.com 24 | 25 | 如果您有任何问题,请随时[提交问题](https://github.com/swiftcarrot/queryx/issues)或[开始讨论](https://github.com/swiftcarrot/queryx/discussions)。[加入 Discord 聊天](https://discord.gg/QUTxjJBRfA) 26 | 27 | ## License 28 | 29 | Queryx is licensed under Apache 2.0 as found in the LICENSE file. 30 | -------------------------------------------------------------------------------- /adapter/adapter.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | 8 | "github.com/swiftcarrot/queryx/schema" 9 | ) 10 | 11 | type Adapter interface { 12 | Open() error 13 | Close() error 14 | CreateDatabase() error 15 | DropDatabase() error 16 | DumpSchema(string, []string) error 17 | LoadSchema(string, []string) error 18 | CreateMigrationsTable(ctx context.Context) error 19 | ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) 20 | QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) 21 | QueryVersion(ctx context.Context, version string) (*sql.Rows, error) 22 | } 23 | 24 | func NewAdapter(cfg *schema.Config) (Adapter, error) { 25 | config := NewConfig(cfg) 26 | if config.Adapter == "postgresql" { 27 | return NewPostgreSQLAdapter(config), nil 28 | } else if config.Adapter == "mysql" { 29 | return NewMySQLAdapter(config), nil 30 | } else if config.Adapter == "sqlite" { 31 | return NewSQLiteAdapter(config), nil 32 | } 33 | 34 | // TODO: list supported adapters 35 | return nil, fmt.Errorf("unsupported adapter: %q", config.Adapter) 36 | } 37 | -------------------------------------------------------------------------------- /adapter/config.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/url" 7 | "os" 8 | "strings" 9 | 10 | "github.com/swiftcarrot/queryx/schema" 11 | ) 12 | 13 | type Config struct { 14 | Adapter string 15 | // full connection url 16 | URL string 17 | // connection url without database 18 | URL2 string 19 | // database, for sqlite it is sqlite file path 20 | Database string 21 | Host string 22 | Port string 23 | Username string 24 | Password string 25 | Options url.Values 26 | } 27 | 28 | func NewConfigFromURL(rawURL string) *Config { 29 | // TODO: error handling in parsing rawURL 30 | c := &Config{} 31 | 32 | u, _ := url.Parse(rawURL) 33 | 34 | c.Adapter = u.Scheme 35 | c.Username = u.User.Username() 36 | c.Password, _ = u.User.Password() 37 | c.Host, c.Port, _ = net.SplitHostPort(u.Host) 38 | c.Database = strings.TrimPrefix(u.Path, "/") 39 | c.Options, _ = url.ParseQuery(u.RawQuery) 40 | 41 | if c.Adapter == "sqlite" { 42 | c.Database = strings.TrimPrefix(rawURL, "sqlite:") 43 | } 44 | 45 | return c 46 | } 47 | 48 | func NewConfig(cfg *schema.Config) *Config { 49 | rawURL := "" 50 | if cfg.URL.EnvKey != "" { 51 | rawURL = os.Getenv(cfg.URL.EnvKey) 52 | } else { 53 | rawURL = cfg.URL.Value 54 | } 55 | 56 | c := NewConfigFromURL(rawURL) 57 | c.URL = c.GoFormat() 58 | 59 | db := c.Database 60 | c.Database = "" 61 | c.URL2 = c.GoFormat() 62 | c.Database = db 63 | 64 | return c 65 | } 66 | 67 | func (c *Config) GoFormat() string { 68 | var u string 69 | 70 | switch c.Adapter { 71 | case "postgresql": 72 | u = fmt.Sprintf("postgres://%s:%s@%s:%s/%s", c.Username, c.Password, c.Host, c.Port, c.Database) 73 | case "mysql": 74 | c.Options.Set("parseTime", "true") 75 | u = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", c.Username, c.Password, c.Host, c.Port, c.Database) 76 | case "sqlite": 77 | return fmt.Sprintf("file:%s", c.Database) 78 | default: 79 | return "" 80 | } 81 | 82 | options := c.Options.Encode() 83 | if options != "" { 84 | u = u + "?" + options 85 | } 86 | 87 | return u 88 | } 89 | 90 | func (c *Config) TSFormat() string { 91 | var u string 92 | 93 | switch c.Adapter { 94 | case "postgresql": 95 | u = fmt.Sprintf("postgres://%s:%s@%s:%s/%s", c.Username, c.Password, c.Host, c.Port, c.Database) 96 | case "mysql": 97 | u = fmt.Sprintf("mysql://%s:%s@%s:%s/%s", c.Username, c.Password, c.Host, c.Port, c.Database) 98 | case "sqlite": 99 | return "" 100 | default: 101 | return "" 102 | } 103 | 104 | options := c.Options.Encode() 105 | if options != "" { 106 | u = u + "?" + options 107 | } 108 | 109 | return u 110 | } 111 | -------------------------------------------------------------------------------- /adapter/config_test.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/swiftcarrot/queryx/schema" 8 | "github.com/swiftcarrot/queryx/types" 9 | ) 10 | 11 | func TestNewPostgreSQLConfig(t *testing.T) { 12 | u := "postgresql://postgres:password@localhost:5432/queryx_test?sslmode=disable" 13 | c := NewConfig(&schema.Config{URL: types.StringOrEnv{Value: u}}) 14 | require.Equal(t, "postgresql", c.Adapter) 15 | require.Equal(t, "postgres", c.Username) 16 | require.Equal(t, "password", c.Password) 17 | require.Equal(t, "localhost", c.Host) 18 | require.Equal(t, "5432", c.Port) 19 | require.Equal(t, "queryx_test", c.Database) 20 | require.Equal(t, "sslmode=disable", c.Options.Encode()) 21 | require.Equal(t, "postgres://postgres:password@localhost:5432/queryx_test?sslmode=disable", c.URL) 22 | require.Equal(t, "postgres://postgres:password@localhost:5432/?sslmode=disable", c.URL2) 23 | require.Equal(t, "postgres://postgres:password@localhost:5432/queryx_test?sslmode=disable", c.TSFormat()) 24 | } 25 | 26 | func TestNewMySQLConfig(t *testing.T) { 27 | u := "mysql://root:@localhost:3306/queryx_test" 28 | // url := "root@tcp(localhost:3306)/queryx_test?parseTime=true&loc=Asia%2FShanghai" 29 | c := NewConfig(&schema.Config{URL: types.StringOrEnv{Value: u}}) 30 | require.Equal(t, "mysql", c.Adapter) 31 | require.Equal(t, "root", c.Username) 32 | require.Equal(t, "", c.Password) 33 | require.Equal(t, "localhost", c.Host) 34 | require.Equal(t, "3306", c.Port) 35 | require.Equal(t, "queryx_test", c.Database) 36 | require.Equal(t, "root:@tcp(localhost:3306)/queryx_test?parseTime=true", c.URL) 37 | require.Equal(t, "root:@tcp(localhost:3306)/?parseTime=true", c.URL2) 38 | require.Equal(t, "mysql://root:@localhost:3306/queryx_test?parseTime=true", c.TSFormat()) 39 | 40 | } 41 | 42 | func TestNewSQLiteConfig(t *testing.T) { 43 | u := "sqlite:test.sqlite3" 44 | c := NewConfig(&schema.Config{URL: types.StringOrEnv{Value: u}}) 45 | require.Equal(t, "sqlite", c.Adapter) 46 | require.Equal(t, "file:test.sqlite3", c.URL) 47 | require.Equal(t, "test.sqlite3", c.Database) 48 | } 49 | -------------------------------------------------------------------------------- /adapter/migration.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | type Migration struct { 8 | Path string 9 | Version string 10 | Name string 11 | Direction string 12 | } 13 | 14 | var mrx = regexp.MustCompile(`^(\d+)_([^.]+)\.(up|down)\.sql$`) 15 | 16 | func ParseMigrationFilename(filename string) (*Migration, error) { 17 | matches := mrx.FindAllStringSubmatch(filename, -1) 18 | if len(matches) == 0 { 19 | return nil, nil 20 | } 21 | 22 | m := matches[0] 23 | 24 | return &Migration{ 25 | Version: m[1], 26 | Name: m[2], 27 | Direction: m[3], 28 | }, nil 29 | } 30 | 31 | type Migrations []*Migration 32 | type UpMigrations Migrations 33 | type DownMigrations Migrations 34 | 35 | func (ms UpMigrations) Len() int { 36 | return len(ms) 37 | } 38 | 39 | func (ms UpMigrations) Less(i, j int) bool { 40 | return ms[i].Version < ms[j].Version 41 | } 42 | 43 | func (ms UpMigrations) Swap(i, j int) { 44 | ms[i], ms[j] = ms[j], ms[i] 45 | } 46 | 47 | func (ms DownMigrations) Len() int { 48 | return len(ms) 49 | } 50 | 51 | func (ms DownMigrations) Less(i, j int) bool { 52 | return ms[i].Version > ms[j].Version 53 | } 54 | 55 | func (ms DownMigrations) Swap(i, j int) { 56 | ms[i], ms[j] = ms[j], ms[i] 57 | } 58 | -------------------------------------------------------------------------------- /adapter/mysql.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | "os/exec" 8 | 9 | "ariga.io/atlas/sql/mysql" 10 | sqlschema "ariga.io/atlas/sql/schema" 11 | _ "github.com/go-sql-driver/mysql" 12 | ) 13 | 14 | type MySQLAdapter struct { 15 | *sql.DB 16 | Config *Config 17 | } 18 | 19 | func NewMySQLAdapter(config *Config) *MySQLAdapter { 20 | return &MySQLAdapter{ 21 | Config: config, 22 | } 23 | } 24 | 25 | func (a *MySQLAdapter) Open() error { 26 | db, err := sql.Open("mysql", a.Config.URL) 27 | if err != nil { 28 | return err 29 | } 30 | a.DB = db 31 | return nil 32 | } 33 | 34 | func (a *MySQLAdapter) CreateDatabase() error { 35 | db, err := sql.Open("mysql", a.Config.URL2) 36 | if err != nil { 37 | return err 38 | } 39 | defer db.Close() 40 | 41 | a.DB = db 42 | sql := fmt.Sprintf("CREATE DATABASE %s", a.Config.Database) 43 | _, err = a.ExecContext(context.Background(), sql) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | fmt.Println("Created database", a.Config.Database) 49 | 50 | return nil 51 | } 52 | 53 | func (a *MySQLAdapter) DropDatabase() error { 54 | db, err := sql.Open("mysql", a.Config.URL2) 55 | if err != nil { 56 | return err 57 | } 58 | defer db.Close() 59 | 60 | a.DB = db 61 | sql := fmt.Sprintf("DROP DATABASE %s", a.Config.Database) 62 | _, err = a.ExecContext(context.Background(), sql) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | fmt.Println("Dropped database", a.Config.Database) 68 | 69 | return nil 70 | } 71 | 72 | // create migrations table with atlas 73 | func (a *MySQLAdapter) CreateMigrationsTable(ctx context.Context) error { 74 | driver, err := mysql.Open(a) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | from, err := driver.InspectSchema(ctx, a.Config.Database, &sqlschema.InspectOptions{ 80 | Tables: []string{"schema_migrations"}, 81 | }) 82 | if err != nil { 83 | return err 84 | } 85 | version := sqlschema.NewStringColumn("version", "varchar(256)") 86 | to := sqlschema.New(a.Config.Database).AddTables( 87 | sqlschema.NewTable("schema_migrations").AddColumns( 88 | sqlschema.NewStringColumn("version", "varchar(256)"), 89 | ).SetPrimaryKey(sqlschema.NewPrimaryKey(version))) 90 | 91 | changes, err := driver.SchemaDiff(from, to) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | if err := driver.ApplyChanges(ctx, changes); err != nil { 97 | return err 98 | } 99 | 100 | return nil 101 | } 102 | 103 | func (a *MySQLAdapter) QueryVersion(ctx context.Context, version string) (*sql.Rows, error) { 104 | return a.DB.QueryContext(ctx, "select version from schema_migrations where version = ?", version) 105 | } 106 | 107 | func (a *MySQLAdapter) DumpSchema(filename string, extraFlags []string) error { 108 | cmd := exec.Command("mysqldump", "--result-file", filename, "--no-data", a.Config.Database) 109 | return cmd.Run() 110 | } 111 | 112 | func (a *MySQLAdapter) LoadSchema(filename string, extraFlags []string) error { 113 | return fmt.Errorf("not implemented") 114 | } 115 | -------------------------------------------------------------------------------- /adapter/postgresql.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | "os/exec" 8 | 9 | "ariga.io/atlas/sql/postgres" 10 | sqlschema "ariga.io/atlas/sql/schema" 11 | _ "github.com/lib/pq" 12 | ) 13 | 14 | type PostgreSQLAdapter struct { 15 | *sql.DB 16 | Config *Config 17 | } 18 | 19 | func NewPostgreSQLAdapter(config *Config) *PostgreSQLAdapter { 20 | return &PostgreSQLAdapter{ 21 | Config: config, 22 | } 23 | } 24 | 25 | func (a *PostgreSQLAdapter) Open() error { 26 | db, err := sql.Open("postgres", a.Config.URL) 27 | if err != nil { 28 | return err 29 | } 30 | a.DB = db 31 | return nil 32 | } 33 | 34 | func (a *PostgreSQLAdapter) CreateDatabase() error { 35 | db, err := sql.Open("postgres", a.Config.URL2) 36 | if err != nil { 37 | return err 38 | } 39 | defer db.Close() 40 | 41 | a.DB = db 42 | sql := fmt.Sprintf("CREATE DATABASE %s", a.Config.Database) 43 | _, err = a.ExecContext(context.Background(), sql) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | fmt.Println("Created database", a.Config.Database) 49 | 50 | return nil 51 | } 52 | 53 | func (a *PostgreSQLAdapter) DropDatabase() error { 54 | db, err := sql.Open("postgres", a.Config.URL2) 55 | if err != nil { 56 | return err 57 | } 58 | defer db.Close() 59 | 60 | a.DB = db 61 | sql := fmt.Sprintf("DROP DATABASE %s", a.Config.Database) 62 | _, err = a.ExecContext(context.Background(), sql) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | fmt.Println("Dropped database", a.Config.Database) 68 | 69 | return nil 70 | } 71 | 72 | // create migrations table with atlas 73 | func (a *PostgreSQLAdapter) CreateMigrationsTable(ctx context.Context) error { 74 | driver, err := postgres.Open(a) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | from, err := driver.InspectSchema(ctx, "public", &sqlschema.InspectOptions{ 80 | Tables: []string{"schema_migrations"}, 81 | }) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | version := sqlschema.NewStringColumn("version", "varchar") 87 | to := sqlschema.New("public").AddTables( 88 | sqlschema.NewTable("schema_migrations").AddColumns( 89 | sqlschema.NewStringColumn("version", "varchar"), 90 | ).SetPrimaryKey(sqlschema.NewPrimaryKey(version))) 91 | 92 | changes, err := driver.SchemaDiff(from, to) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | if err := driver.ApplyChanges(ctx, changes); err != nil { 98 | return err 99 | } 100 | 101 | return nil 102 | } 103 | 104 | func (a *PostgreSQLAdapter) QueryVersion(ctx context.Context, version string) (*sql.Rows, error) { 105 | return a.DB.QueryContext(ctx, "select version from schema_migrations where version = $1", version) 106 | } 107 | 108 | func (a *PostgreSQLAdapter) DumpSchema(filename string, extraFlags []string) error { 109 | cmd := exec.Command("pg_dump", "--schema-only", "--no-owner", "--file", filename, a.Config.Database) 110 | return cmd.Run() 111 | } 112 | 113 | func (a *PostgreSQLAdapter) LoadSchema(filename string, extraFlags []string) error { 114 | return fmt.Errorf("not implemented") 115 | } 116 | -------------------------------------------------------------------------------- /adapter/sqlite.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | 10 | sqlschema "ariga.io/atlas/sql/schema" 11 | "ariga.io/atlas/sql/sqlite" 12 | _ "github.com/mattn/go-sqlite3" 13 | ) 14 | 15 | type SQLiteAdapter struct { 16 | *sql.DB 17 | Config *Config 18 | } 19 | 20 | func NewSQLiteAdapter(config *Config) *SQLiteAdapter { 21 | return &SQLiteAdapter{ 22 | Config: config, 23 | } 24 | } 25 | 26 | func (a *SQLiteAdapter) Open() error { 27 | db, err := sql.Open("sqlite3", a.Config.URL) 28 | if err != nil { 29 | return err 30 | } 31 | a.DB = db 32 | return nil 33 | } 34 | 35 | func (a *SQLiteAdapter) CreateDatabase() error { 36 | _, err := os.Create(a.Config.Database) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | fmt.Println("Created database", a.Config.Database) 42 | 43 | return nil 44 | } 45 | 46 | func (a *SQLiteAdapter) DropDatabase() error { 47 | err := os.Remove(a.Config.Database) 48 | if err != nil { 49 | return err 50 | } 51 | fmt.Println("Dropped database", a.Config.Database) 52 | return nil 53 | } 54 | 55 | // create migrations table with atlas 56 | func (a *SQLiteAdapter) CreateMigrationsTable(ctx context.Context) error { 57 | driver, err := sqlite.Open(a) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | from, err := driver.InspectSchema(ctx, "main", &sqlschema.InspectOptions{ 63 | Tables: []string{"schema_migrations"}, 64 | }) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | version := sqlschema.NewStringColumn("version", "varchar") 70 | to := sqlschema.New("main").AddTables( 71 | sqlschema.NewTable("schema_migrations").AddColumns( 72 | sqlschema.NewStringColumn("version", "varchar"), 73 | ).SetPrimaryKey(sqlschema.NewPrimaryKey(version))) 74 | 75 | changes, err := driver.SchemaDiff(from, to) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | if err := driver.ApplyChanges(ctx, changes); err != nil { 81 | return err 82 | } 83 | 84 | return nil 85 | } 86 | 87 | func (a *SQLiteAdapter) QueryVersion(ctx context.Context, version string) (*sql.Rows, error) { 88 | return a.DB.QueryContext(ctx, "select version from schema_migrations where version = $1", version) 89 | } 90 | 91 | func (a *SQLiteAdapter) DumpSchema(filename string, extraFlags []string) error { 92 | file, err := os.Create(filename) 93 | if err != nil { 94 | return err 95 | } 96 | defer file.Close() 97 | cmd := exec.Command("sqlite3", a.Config.Database, ".schema") 98 | cmd.Stdout = file 99 | 100 | if err := cmd.Run(); err != nil { 101 | return err 102 | } 103 | 104 | return nil 105 | } 106 | 107 | func (a *SQLiteAdapter) LoadSchema(filename string, extraFlags []string) error { 108 | return fmt.Errorf("not implemented") 109 | } 110 | -------------------------------------------------------------------------------- /cmd/queryx/action/format.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | 8 | "github.com/hashicorp/hcl/v2/hclwrite" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var formatCmd = &cobra.Command{ 13 | Use: "format", 14 | Aliases: []string{"fmt"}, 15 | Short: "Format current schema file", 16 | RunE: func(cmd *cobra.Command, args []string) error { 17 | f, err := os.Open(schemaFile) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | inSrc, err := io.ReadAll(f) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | // TODO: check syntax 28 | outSrc := hclwrite.Format(inSrc) 29 | 30 | // TODO: add overwrite option 31 | if err := os.WriteFile(schemaFile, outSrc, 0644); err != nil { 32 | return err 33 | } 34 | 35 | fmt.Println("Format", schemaFile) 36 | 37 | return nil 38 | }, 39 | } 40 | -------------------------------------------------------------------------------- /cmd/queryx/action/generate.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/swiftcarrot/queryx/generator" 8 | "github.com/swiftcarrot/queryx/generator/client/golang" 9 | "github.com/swiftcarrot/queryx/generator/client/typescript" 10 | ) 11 | 12 | var generateCmd = &cobra.Command{ 13 | Use: "generate", 14 | Aliases: []string{"g"}, 15 | Short: "Run generator defined in schema", 16 | RunE: func(cmd *cobra.Command, args []string) error { 17 | sch, err := newSchema() 18 | if err != nil { 19 | return err 20 | } 21 | 22 | database := sch.Databases[0] 23 | 24 | g := generator.NewGenerator(sch) 25 | 26 | hasGenerator := false 27 | for _, generator := range database.Generators { 28 | hasGenerator = true 29 | switch generator.Name { 30 | case "client-golang": 31 | if err := golang.Run(g, generator, args); err != nil { 32 | return err 33 | } 34 | case "client-typescript": 35 | if err := typescript.Run(g, generator, args); err != nil { 36 | return err 37 | } 38 | default: 39 | return fmt.Errorf("only supports generator.Name: %s , %s", "client-golang", "client-typescript") 40 | } 41 | } 42 | 43 | if !hasGenerator { 44 | return fmt.Errorf("no generator is found in schema file") 45 | } 46 | 47 | if err := g.Clean(); err != nil { 48 | return err 49 | } 50 | 51 | return nil 52 | }, 53 | } 54 | -------------------------------------------------------------------------------- /cmd/queryx/action/init.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var sampleSchema = `database "db" { 11 | adapter = "sqlite" 12 | 13 | config "development" { 14 | url = "sqlite:blog_development.sqlite3" 15 | } 16 | 17 | model "Post" { 18 | column "title" { 19 | type = string 20 | } 21 | column "content" { 22 | type = text 23 | } 24 | } 25 | } 26 | ` 27 | 28 | var initCmd = &cobra.Command{ 29 | Use: "init", 30 | Short: "Creates a sample schema.hcl", 31 | RunE: func(cmd *cobra.Command, args []string) error { 32 | fmt.Println("Created schema.hcl") 33 | file, err := os.Create("schema.hcl") 34 | if err != nil { 35 | return err 36 | } 37 | defer file.Close() 38 | _, err = file.WriteString(sampleSchema) 39 | if err != nil { 40 | return err 41 | } 42 | return nil 43 | }, 44 | } 45 | -------------------------------------------------------------------------------- /cmd/queryx/action/root.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/hashicorp/hcl/v2" 7 | "github.com/hashicorp/hcl/v2/hclsyntax" 8 | "github.com/spf13/cobra" 9 | "github.com/swiftcarrot/queryx/schema" 10 | ) 11 | 12 | var schemaFile string 13 | 14 | var RootCmd = &cobra.Command{ 15 | Use: "queryx", 16 | RunE: func(cmd *cobra.Command, args []string) error { 17 | return cmd.Help() 18 | }, 19 | } 20 | 21 | func newSchema() (*schema.Schema, error) { 22 | content, err := os.ReadFile(schemaFile) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | file, diagnostics := hclsyntax.ParseConfig(content, schemaFile, hcl.Pos{Line: 1, Column: 1, Byte: 0}) 28 | if diagnostics != nil && diagnostics.HasErrors() { 29 | return nil, diagnostics.Errs()[0] 30 | } 31 | sch, err := schema.Parse(file.Body) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | return sch, nil 37 | } 38 | 39 | func init() { 40 | RootCmd.PersistentFlags().StringVar(&schemaFile, "schema", "schema.hcl", "queryx schema file") 41 | 42 | RootCmd.AddCommand(versionCmd) 43 | RootCmd.AddCommand(formatCmd) 44 | RootCmd.AddCommand(generateCmd) 45 | 46 | RootCmd.AddCommand(dbCreateCmd) 47 | RootCmd.AddCommand(dbDropCmd) 48 | RootCmd.AddCommand(dbMigrateCmd) 49 | RootCmd.AddCommand(dbMigrateGenerateCmd) 50 | RootCmd.AddCommand(dbRollbackCmd) 51 | RootCmd.AddCommand(dbMigrateStatusCmd) 52 | RootCmd.AddCommand(dbVersionCmd) 53 | RootCmd.AddCommand(dbSchemaDumpCmd) 54 | RootCmd.AddCommand(dbSchemaLoadCmd) 55 | 56 | RootCmd.AddCommand(initCmd) 57 | } 58 | -------------------------------------------------------------------------------- /cmd/queryx/action/version.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var Version = "0.2.18" 10 | 11 | var versionCmd = &cobra.Command{ 12 | Use: "version", 13 | Short: "Prints current version", 14 | Run: func(cmd *cobra.Command, args []string) { 15 | RootCmd.Println(fmt.Sprintf("queryx %s", Version)) 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /cmd/queryx/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/swiftcarrot/queryx/cmd/queryx/action" 8 | ) 9 | 10 | func main() { 11 | if err := action.RootCmd.Execute(); err != nil { 12 | fmt.Println(err) 13 | os.Exit(1) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /generator/client/golang/generator.go: -------------------------------------------------------------------------------- 1 | package golang 2 | 3 | import ( 4 | "embed" 5 | "go/format" 6 | "log" 7 | "os" 8 | "path" 9 | "path/filepath" 10 | "strings" 11 | "text/template" 12 | 13 | "github.com/swiftcarrot/queryx/generator" 14 | "github.com/swiftcarrot/queryx/schema" 15 | "golang.org/x/mod/modfile" 16 | ) 17 | 18 | //go:embed templates 19 | var templatesFS embed.FS 20 | 21 | func Run(g *generator.Generator, generatorConfig *schema.Generator, args []string) error { 22 | schema := g.Schema 23 | database := schema.Databases[0] 24 | 25 | if err := g.LoadTemplates(templatesFS, database.Adapter); err != nil { 26 | return err 27 | } 28 | 29 | templates := []*template.Template{} 30 | typs := typesFromSchema(schema) 31 | for _, tpl := range g.Templates { 32 | name := tpl.Name() 33 | 34 | isTest := strings.HasSuffix(name, "_test.go") 35 | if isTest && !generatorConfig.Test { 36 | continue 37 | } else { 38 | name = strings.TrimPrefix(name, "/queryx/") 39 | name = strings.TrimSuffix(name, ".go") 40 | name = strings.TrimSuffix(name, "_column") 41 | 42 | if isTest { 43 | name = strings.TrimSuffix(name, "_test") 44 | } 45 | 46 | b, ok := typs[name] 47 | if !ok || b { 48 | templates = append(templates, tpl) 49 | } 50 | } 51 | } 52 | g.Templates = templates 53 | 54 | // TODO: wrap this in a function 55 | cwd, err := os.Getwd() 56 | if err != nil { 57 | return err 58 | } 59 | roots := findModuleRoot(cwd) 60 | f := filepath.Join(roots, "go.mod") 61 | content, err := os.ReadFile(f) 62 | if err != nil { 63 | return err 64 | } 65 | mf, err := modfile.Parse("go.mod", content, nil) 66 | if err != nil { 67 | return err 68 | } 69 | rel, err := filepath.Rel(roots, cwd) 70 | if err != nil { 71 | return err 72 | } 73 | goModPath := path.Join(mf.Module.Mod.Path, rel) 74 | goModPath = strings.ReplaceAll(goModPath, "\\", "/") 75 | 76 | if err := g.Generate(transform, goModPath); err != nil { 77 | return err 78 | } 79 | 80 | return nil 81 | } 82 | 83 | func transform(b []byte) []byte { 84 | b, err := format.Source(b) 85 | if err != nil { 86 | log.Fatal(err) 87 | } 88 | return b 89 | } 90 | 91 | func typesFromSchema(sch *schema.Schema) map[string]bool { 92 | m := map[string]bool{} 93 | // TODO: move to schema package 94 | typs := []string{"string", "text", "boolean", 95 | "date", "time", "datetime", "float", 96 | "integer", "bigint", "json", "uuid"} 97 | for _, typ := range typs { 98 | m[typ] = false 99 | } 100 | 101 | for _, database := range sch.Databases { 102 | for _, model := range database.Models { 103 | for _, column := range model.Columns { 104 | typ := column.Type 105 | if typ == "jsonb" { 106 | typ = "json" 107 | } 108 | m[typ] = true 109 | } 110 | } 111 | } 112 | 113 | return m 114 | } 115 | 116 | // findModuleRoot finds the module root by looking for go.mod file in the current directory and its parents. 117 | // This function is copied from https://github.com/golang/go/blob/master/src/cmd/go/internal/modload/init.go 118 | func findModuleRoot(dir string) (roots string) { 119 | if dir == "" { 120 | // TODO: add go mod init in docs 121 | panic("dir not set") // TODO: improve this error message 122 | } 123 | dir = filepath.Clean(dir) 124 | 125 | for { 126 | if fi, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil && !fi.IsDir() { 127 | return dir 128 | } 129 | d := filepath.Dir(dir) 130 | if d == dir { 131 | break 132 | } 133 | dir = d 134 | } 135 | return "" 136 | } 137 | -------------------------------------------------------------------------------- /generator/client/golang/templates/errors.gotmpl: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package {{ $.packageName }} 4 | 5 | import "errors" 6 | 7 | var ErrInsertAllEmptyChanges = errors.New("queryx: insert all with empty changes") 8 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx.gotmpl: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package {{ $.packageName }} 4 | 5 | import ( 6 | "context" 7 | "database/sql" 8 | "fmt" 9 | "log" 10 | "os" 11 | 12 | "{{ $.goModPath }}/{{ $.packageName }}/queryx" 13 | ) 14 | 15 | type Queries interface { 16 | {{- range $m := $.client.Models }} 17 | Query{{ $m.Name }}() *{{ $m.Name }}Query 18 | {{- end }} 19 | } 20 | 21 | type QXClient struct { 22 | DB *sql.DB 23 | config *queryx.Config 24 | logger queryx.Logger 25 | *queryx.Adapter 26 | *queryx.Schema 27 | } 28 | 29 | func NewClient() (*QXClient, error) { 30 | env := os.Getenv("QUERYX_ENV") 31 | if env == "" { 32 | env = "development" 33 | } 34 | return NewClientWithEnv(env) 35 | } 36 | 37 | func NewClientWithEnv(env string) (*QXClient, error) { 38 | config := queryx.NewConfig(env) 39 | if config == nil { 40 | return nil, fmt.Errorf("client config is missing for %s", env) 41 | } 42 | 43 | {{- if eq $.client.Adapter "postgresql" }} 44 | db, err := sql.Open("postgres", config.URL) 45 | {{- else if eq $.client.Adapter "mysql" }} 46 | db, err := sql.Open("mysql", config.URL) 47 | {{- else if eq $.client.Adapter "sqlite" }} 48 | db, err := sql.Open("sqlite3", config.URL) 49 | {{- end }} 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | client := &QXClient{ 55 | DB: db, 56 | config: config, 57 | Adapter: queryx.NewAdapter(db), 58 | Schema : queryx.NewSchema(), 59 | } 60 | client.setDefaultLogger() 61 | 62 | return client, nil 63 | } 64 | 65 | func (c *QXClient) SetLogger(logger queryx.Logger) { 66 | c.logger = logger 67 | } 68 | 69 | func (c *QXClient) setDefaultLogger() { 70 | c.logger = log.New(os.Stderr, "sql ", log.Llongfile|log.LstdFlags) 71 | } 72 | 73 | {{- range $m := $.client.Models }} 74 | 75 | func (c *QXClient) Query{{ $m.Name }}() *{{ $m.Name }}Query { 76 | return New{{ $m.Name }}Query(c.Adapter, c.Schema, c) 77 | } 78 | {{- end }} 79 | 80 | func (c *QXClient) Transaction(f func(t *Tx) error) error { 81 | tx, err := c.Tx() 82 | if err != nil { 83 | return err 84 | } 85 | defer tx.Rollback() 86 | if err = f(tx);err != nil { 87 | return err 88 | } 89 | return tx.Commit() 90 | } 91 | 92 | func (c *QXClient) Raw(fragment string, args ...interface{}) *queryx.Clause { 93 | return queryx.NewClause(fragment, args) 94 | } 95 | 96 | type Tx struct { 97 | *queryx.Schema 98 | *queryx.Adapter 99 | tx *sql.Tx 100 | client *QXClient 101 | } 102 | 103 | func (c *QXClient) Tx() (*Tx, error) { 104 | ctx := context.Background() 105 | tx, err := c.DB.BeginTx(ctx, nil) 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | return &Tx{ 111 | tx: tx, 112 | Schema: c.Schema, 113 | Adapter: queryx.NewAdapter(tx), 114 | client: c, 115 | }, nil 116 | } 117 | 118 | func (tx *Tx) Commit() error { 119 | return tx.tx.Commit() 120 | } 121 | 122 | func (tx *Tx) Rollback() error { 123 | return tx.tx.Rollback() 124 | } 125 | 126 | {{- range $m := $.client.Models }} 127 | 128 | func (tx *Tx) Query{{ $m.Name }}() *{{ $m.Name }}Query { 129 | return New{{ $m.Name }}Query(tx.Adapter, tx.Schema, tx) 130 | } 131 | {{- end }} 132 | 133 | func (tx *Tx) Raw(fragment string, args ...interface{}) *queryx.Clause { 134 | return queryx.NewClause(fragment, args) 135 | } 136 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/[model]_change.gotmpl: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | type {{ $.model.Name }}Change struct { 6 | {{- range $c := .model.Columns }} 7 | {{ $c.Name | pascal }} {{ goType $c.Type $c.Null $c.Array }} 8 | {{- end }} 9 | } 10 | 11 | func (c *{{.model.Name}}Change) Changes() (columns []string, values []interface{}) { 12 | if c == nil { 13 | return columns, values 14 | } 15 | 16 | {{- range $c := .model.Columns }} 17 | {{- $f := $c.Name | pascal }} 18 | if c.{{ $f }}.Set { 19 | columns = append(columns, "{{ $c.Name }}") 20 | values = append(values, c.{{ $f }}) 21 | } 22 | {{- end }} 23 | return columns, values 24 | } 25 | 26 | {{- range $c := $.model.Columns }} 27 | {{- $f := $c.Name | pascal }} 28 | {{- $t := goChangeSetType $c.Type $c.Null $c.Array }} 29 | {{- $a := $c.Name | camel | goKeywordFix }} 30 | 31 | func (c *{{ $.model.Name }}Change) Set{{ pascal $c.Name }}({{ $a }} {{ $t }}) *{{ $.model.Name }}Change { 32 | c.{{ $f }} = New{{ goType $c.Type $c.Null $c.Array }}({{ $a }}) 33 | c.{{ $f }}.Set = true 34 | return c 35 | } 36 | {{- if $c.Null }} 37 | 38 | func (c *{{ $.model.Name }}Change) SetNullable{{ pascal $c.Name }}({{ $a }} *{{ $t }}) *{{ $.model.Name }}Change { 39 | c.{{ $f }} = NewNullable{{ goType $c.Type $c.Null $c.Array }}({{ $a }}) 40 | c.{{ $f }}.Set = true 41 | return c 42 | } 43 | {{- end }} 44 | {{- end -}} 45 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/adapter.mysql.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "database/sql" 7 | "regexp" 8 | 9 | _ "github.com/go-sql-driver/mysql" 10 | ) 11 | 12 | func execUtils(query string, args ...interface{}) (string, []interface{}, error) { 13 | matched1, err := regexp.MatchString(`.* IN (.*?)`, query) 14 | if err != nil { 15 | return "", nil, err 16 | } 17 | matched2, err := regexp.MatchString(`.* in (.*?)`, query) 18 | if err != nil { 19 | return "", nil, err 20 | } 21 | if matched1 || matched2 { 22 | query, args, err = In(query, args...) 23 | if err != nil { 24 | return "", nil, err 25 | } 26 | } 27 | return query, args, err 28 | } 29 | 30 | func (a *Adapter) Exec(query string, args ...interface{}) (int64, error) { 31 | query, args, err := execUtils(query, args...) 32 | if err != nil { 33 | return 0, err 34 | } 35 | 36 | result, err := a.db.Exec(query, args...) 37 | if err != nil { 38 | return 0, err 39 | } 40 | return result.RowsAffected() 41 | } 42 | 43 | func (a *Adapter) ExecInternal(query string, args ...interface{}) (sql.Result, error) { 44 | query, args, err := execUtils(query, args...) 45 | if err != nil { 46 | return nil, err 47 | } 48 | result, err := a.db.Exec(query, args...) 49 | if err != nil { 50 | return nil, err 51 | } 52 | return result, err 53 | } 54 | 55 | func rebind(query string, args []interface{}) (string, []interface{}) { 56 | return query, args 57 | } 58 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/adapter.postgresql.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | 10 | _ "github.com/lib/pq" 11 | ) 12 | 13 | func (a *Adapter) Exec(query string, args ...interface{}) (int64, error) { 14 | matched1, err := regexp.MatchString(`.* IN (.*?)`, query) 15 | if err != nil { 16 | return 0, err 17 | } 18 | matched2, err := regexp.MatchString(`.* in (.*?)`, query) 19 | if err != nil { 20 | return 0, err 21 | } 22 | if matched1 || matched2 { 23 | query, args, err = In(query, args...) 24 | if err != nil { 25 | return 0, err 26 | } 27 | } 28 | query, args = rebind(query, args) 29 | result, err := a.db.Exec(query, args...) 30 | if err != nil { 31 | return 0, err 32 | } 33 | return result.RowsAffected() 34 | } 35 | 36 | func rebind(query string, args []interface{}) (string, []interface{}) { 37 | rqb := make([]byte, 0, len(query)+10) 38 | var i, j int 39 | for i = strings.Index(query, "?"); i != -1; i = strings.Index(query, "?") { 40 | rqb = append(rqb, query[:i]...) 41 | rqb = append(rqb, '$') 42 | j++ 43 | rqb = strconv.AppendInt(rqb, int64(j), 10) 44 | query = query[i+1:] 45 | } 46 | query = string(append(rqb, query...)) 47 | return query, args 48 | } 49 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/adapter.sqlite.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "regexp" 7 | 8 | _ "github.com/mattn/go-sqlite3" 9 | ) 10 | 11 | func (a *Adapter) Exec(query string, args ...interface{}) (int64, error) { 12 | matched1, err := regexp.MatchString(`.* IN (.*?)`, query) 13 | if err != nil { 14 | return 0, err 15 | } 16 | matched2, err := regexp.MatchString(`.* in (.*?)`, query) 17 | if err != nil { 18 | return 0, err 19 | } 20 | if matched1 || matched2 { 21 | query, args, err = In(query, args...) 22 | if err != nil { 23 | return 0, err 24 | } 25 | } 26 | query, args = rebind(query, args) 27 | result, err := a.db.Exec(query, args...) 28 | if err != nil { 29 | return 0, err 30 | } 31 | return result.RowsAffected() 32 | } 33 | 34 | func rebind(query string, args []interface{}) (string, []interface{}) { 35 | return query, args 36 | } 37 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/adapter_test.postgresql.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestRebind(t *testing.T) { 12 | sql, args := rebind("a ? b ?", []interface{}{1, 2}) 13 | require.Equal(t, "a $1 b $2", sql) 14 | require.Equal(t, []interface{}{1, 2}, args) 15 | } 16 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/bigint.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "database/sql" 7 | "database/sql/driver" 8 | "encoding/json" 9 | "strconv" 10 | ) 11 | 12 | type BigInt struct { 13 | Val int64 14 | Valid bool 15 | Set bool 16 | } 17 | 18 | func NewBigInt(v int64) BigInt { 19 | return BigInt{Val: v, Valid: true, Set: true} 20 | } 21 | 22 | func NewNullableBigInt(v *int64) BigInt { 23 | if v != nil { 24 | return NewBigInt(*v) 25 | } 26 | return BigInt{Set: true} 27 | } 28 | 29 | // Scan implements the Scanner interface. 30 | func (b *BigInt) Scan(value interface{}) error { 31 | n := sql.NullInt64{} 32 | err := n.Scan(value) 33 | if err != nil { 34 | return err 35 | } 36 | b.Val, b.Valid = n.Int64, n.Valid 37 | return nil 38 | } 39 | 40 | // Value implements the driver Valuer interface. 41 | func (b BigInt) Value() (driver.Value, error) { 42 | if !b.Valid { 43 | return nil, nil 44 | } 45 | return b.Val, nil 46 | } 47 | 48 | // MarshalJSON implements the json.Marshaler interface. 49 | func (b BigInt) MarshalJSON() ([]byte, error) { 50 | if !b.Valid { 51 | return json.Marshal(nil) 52 | } 53 | return json.Marshal(b.Val) 54 | } 55 | 56 | // UnmarshalJSON implements the json.Unmarshaler interface. 57 | func (b *BigInt) UnmarshalJSON(data []byte) error { 58 | b.Set = true 59 | if string(data) == "null" { 60 | return nil 61 | } 62 | b.Valid = true 63 | if err := json.Unmarshal(data, &b.Val); err != nil { 64 | return err 65 | } 66 | return nil 67 | } 68 | 69 | // String implements the stringer interface. 70 | func (b BigInt) String() string { 71 | if !b.Valid { 72 | return "null" 73 | } 74 | return strconv.FormatInt(b.Val, 10) 75 | } 76 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/bigint_column.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import "fmt" 6 | 7 | type BigIntColumn struct { 8 | Name string 9 | Table *Table 10 | } 11 | 12 | func (t *Table) NewBigIntColumn(name string) *BigIntColumn { 13 | return &BigIntColumn{ 14 | Table: t, 15 | Name: name, 16 | } 17 | } 18 | 19 | func (c *BigIntColumn) EQ(v int64) *Clause { 20 | return &Clause{ 21 | fragment: fmt.Sprintf("%s.%s = ?", c.Table.Name, c.Name), 22 | args: []interface{}{v}, 23 | } 24 | } 25 | 26 | func (c *BigIntColumn) NE(v int64) *Clause { 27 | return &Clause{ 28 | fragment: fmt.Sprintf("%s.%s <> ?", c.Table.Name, c.Name), 29 | args: []interface{}{v}, 30 | } 31 | } 32 | 33 | func (c *BigIntColumn) LT(v int64) *Clause { 34 | return &Clause{ 35 | fragment: fmt.Sprintf("%s.%s < ?", c.Table.Name, c.Name), 36 | args: []interface{}{v}, 37 | } 38 | } 39 | 40 | func (c *BigIntColumn) GT(v int64) *Clause { 41 | return &Clause{ 42 | fragment: fmt.Sprintf("%s.%s > ?", c.Table.Name, c.Name), 43 | args: []interface{}{v}, 44 | } 45 | } 46 | 47 | func (c *BigIntColumn) LE(v int64) *Clause { 48 | return &Clause{ 49 | fragment: fmt.Sprintf("%s.%s <= ?", c.Table.Name, c.Name), 50 | args: []interface{}{v}, 51 | } 52 | } 53 | 54 | func (c *BigIntColumn) GE(v int64) *Clause { 55 | return &Clause{ 56 | fragment: fmt.Sprintf("%s.%s >= ?", c.Table.Name, c.Name), 57 | args: []interface{}{v}, 58 | } 59 | } 60 | 61 | func (c *BigIntColumn) In(v []int64) *Clause { 62 | if len(v) == 0 { 63 | return &Clause{ 64 | fragment: "1=0", 65 | } 66 | } 67 | return &Clause{ 68 | fragment: fmt.Sprintf("%s.%s IN (?)", c.Table.Name, c.Name), 69 | args: []interface{}{v}, 70 | } 71 | } 72 | 73 | func (c *BigIntColumn) NIn(v []int64) *Clause { 74 | if len(v) == 0 { 75 | return &Clause{ 76 | fragment: "1!=0", 77 | } 78 | } 79 | return &Clause{ 80 | fragment: fmt.Sprintf("%s.%s NOT IN (?)", c.Table.Name, c.Name), 81 | args: []interface{}{v}, 82 | } 83 | } 84 | 85 | func (c *BigIntColumn) InRange(start int64, end int64) *Clause { 86 | return &Clause{ 87 | fragment: fmt.Sprintf("%s.%s >= ? and %s.%s< ?", c.Table.Name, c.Name, c.Table.Name, c.Name), 88 | args: []interface{}{start, end}, 89 | } 90 | } 91 | 92 | func (c *BigIntColumn) Between(start int64, end int64) *Clause { 93 | return &Clause{ 94 | fragment: fmt.Sprintf("%s.%s >= ? and %s.%s<= ?", c.Table.Name, c.Name, c.Table.Name, c.Name), 95 | args: []interface{}{start, end}, 96 | } 97 | } 98 | 99 | func (c *BigIntColumn) Asc() string { 100 | return fmt.Sprintf("%s.%s ASC", c.Table.Name, c.Name) 101 | } 102 | 103 | func (c *BigIntColumn) Desc() string { 104 | return fmt.Sprintf("%s.%s DESC", c.Table.Name, c.Name) 105 | } 106 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/bigint_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "encoding/json" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestNewBigInt(t *testing.T) { 13 | b1 := NewBigInt(2) 14 | require.Equal(t, int64(2), b1.Val) 15 | require.True(t, b1.Valid) 16 | 17 | b2 := NewNullableBigInt(nil) 18 | require.False(t, b2.Valid) 19 | } 20 | 21 | func TestBigIntJSON(t *testing.T) { 22 | type Foo struct { 23 | X BigInt `json:"x"` 24 | Y BigInt `json:"y"` 25 | } 26 | x := NewBigInt(2) 27 | y := NewNullableBigInt(nil) 28 | s := `{"x":2,"y":null}` 29 | 30 | f1 := Foo{X: x, Y: y} 31 | b, err := json.Marshal(f1) 32 | require.NoError(t, err) 33 | require.Equal(t, s, string(b)) 34 | 35 | var f2 Foo 36 | err = json.Unmarshal([]byte(s), &f2) 37 | require.NoError(t, err) 38 | require.Equal(t, x, f2.X) 39 | require.Equal(t, y, f2.Y) 40 | } 41 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/boolean.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "database/sql" 7 | "database/sql/driver" 8 | "encoding/json" 9 | ) 10 | 11 | type Boolean struct { 12 | Val bool 13 | Valid bool 14 | Set bool 15 | } 16 | 17 | func NewBoolean(v bool) Boolean { 18 | return Boolean{Val: v, Valid: true, Set: true} 19 | } 20 | 21 | func NewNullableBoolean(v *bool) Boolean { 22 | if v != nil { 23 | return NewBoolean(*v) 24 | } 25 | return Boolean{Set: true} 26 | } 27 | 28 | // Scan implements the Scanner interface. 29 | func (b *Boolean) Scan(value interface{}) error { 30 | n := sql.NullBool{} 31 | err := n.Scan(value) 32 | if err != nil { 33 | return err 34 | } 35 | b.Val, b.Valid = n.Bool, n.Valid 36 | return nil 37 | } 38 | 39 | // Value implements the driver Valuer interface. 40 | func (b Boolean) Value() (driver.Value, error) { 41 | if !b.Valid { 42 | return nil, nil 43 | } 44 | return b.Val, nil 45 | } 46 | 47 | // MarshalJSON implements the json.Marshaler interface. 48 | func (b Boolean) MarshalJSON() ([]byte, error) { 49 | if !b.Valid { 50 | return json.Marshal(nil) 51 | } 52 | return json.Marshal(b.Val) 53 | } 54 | 55 | // UnmarshalJSON implements the json.Unmarshaler interface. 56 | func (b *Boolean) UnmarshalJSON(data []byte) error { 57 | b.Set = true 58 | if string(data) == "null" { 59 | return nil 60 | } 61 | b.Valid = true 62 | if err := json.Unmarshal(data, &b.Val); err != nil { 63 | return err 64 | } 65 | return nil 66 | } 67 | 68 | // String implements the stringer interface. 69 | func (b Boolean) String() string { 70 | if !b.Valid { 71 | return "null" 72 | } 73 | if b.Val { 74 | return "true" 75 | } 76 | return "false" 77 | } 78 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/boolean_column.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import "fmt" 6 | 7 | type BooleanColumn struct { 8 | Name string 9 | Table *Table 10 | } 11 | 12 | func (t *Table) NewBooleanColumn(name string) *BooleanColumn { 13 | return &BooleanColumn{ 14 | Table: t, 15 | Name: name, 16 | } 17 | } 18 | 19 | func (c *BooleanColumn) EQ(v bool) *Clause { 20 | return &Clause{ 21 | fragment: fmt.Sprintf("%s.%s = ?", c.Table.Name, c.Name), 22 | args: []interface{}{v}, 23 | } 24 | } 25 | 26 | func (c *BooleanColumn) NE(v bool) *Clause { 27 | return &Clause{ 28 | fragment: fmt.Sprintf("%s.%s <> ?", c.Table.Name, c.Name), 29 | args: []interface{}{v}, 30 | } 31 | } 32 | 33 | func (b *BooleanColumn) Asc() string { 34 | return fmt.Sprintf("%s.%s ASC", b.Table.Name, b.Name) 35 | } 36 | 37 | func (b *BooleanColumn) Desc() string { 38 | return fmt.Sprintf("%s.%s DESC", b.Table.Name, b.Name) 39 | } 40 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/boolean_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "encoding/json" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestNewBoolean(t *testing.T) { 13 | b1 := NewBoolean(true) 14 | require.Equal(t, true, b1.Val) 15 | require.True(t, b1.Valid) 16 | 17 | b2 := NewNullableBoolean(nil) 18 | require.False(t, b2.Valid) 19 | } 20 | 21 | func TestBooleanJSON(t *testing.T) { 22 | type Foo struct { 23 | X Boolean `json:"x"` 24 | Y Boolean `json:"y"` 25 | } 26 | x := NewBoolean(true) 27 | y := NewNullableBoolean(nil) 28 | s := `{"x":true,"y":null}` 29 | 30 | f1 := Foo{X: x, Y: y} 31 | b, err := json.Marshal(f1) 32 | require.NoError(t, err) 33 | require.Equal(t, s, string(b)) 34 | 35 | var f2 Foo 36 | err = json.Unmarshal([]byte(s), &f2) 37 | require.NoError(t, err) 38 | require.Equal(t, x, f2.X) 39 | require.Equal(t, y, f2.Y) 40 | } 41 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/clause.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "fmt" 7 | "strings" 8 | ) 9 | 10 | type Clause struct { 11 | fragment string 12 | args []interface{} 13 | err error 14 | } 15 | 16 | func NewClause(fragment string, args []interface{}) *Clause { 17 | return &Clause{ 18 | fragment: fragment, 19 | args: args, 20 | } 21 | } 22 | 23 | func (c *Clause) Err() error { 24 | return c.err 25 | } 26 | 27 | func (c *Clause) And(clauses ...*Clause) *Clause { 28 | if len(clauses) == 0 { 29 | return c 30 | } 31 | 32 | var fragments []string 33 | var args []interface{} 34 | clauses = append([]*Clause{c}, clauses...) 35 | for _, clause := range clauses { 36 | fragments = append(fragments, fmt.Sprintf("(%s)", clause.fragment)) 37 | args = append(args, clause.args...) 38 | } 39 | 40 | return NewClause(strings.Join(fragments, " AND "), args) 41 | } 42 | 43 | func (c *Clause) Or(clauses ...*Clause) *Clause { 44 | if len(clauses) == 0 { 45 | return c 46 | } 47 | 48 | var fragments []string 49 | var args []interface{} 50 | clauses = append([]*Clause{c}, clauses...) 51 | for _, clause := range clauses { 52 | fragments = append(fragments, fmt.Sprintf("(%s)", clause.fragment)) 53 | args = append(args, clause.args...) 54 | } 55 | 56 | return NewClause(strings.Join(fragments, " OR "), args) 57 | } 58 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/clause_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestClauseAddOr(t *testing.T) { 12 | c1 := &Clause{ 13 | fragment: "a = ?", 14 | args: []interface{}{1}, 15 | } 16 | c2 := &Clause{ 17 | fragment: "b = ?", 18 | args: []interface{}{"x"}, 19 | } 20 | 21 | c3 := c1.And(c2) 22 | require.Equal(t, "(a = ?) AND (b = ?)", c3.fragment) 23 | require.Equal(t, []interface{}{1, "x"}, c3.args) 24 | 25 | c4 := c1.Or(c2) 26 | require.Equal(t, "(a = ?) OR (b = ?)", c4.fragment) 27 | require.Equal(t, []interface{}{1, "x"}, c4.args) 28 | 29 | c5 := c1.And(c1.Or(c2)) 30 | require.Equal(t, "(a = ?) AND ((a = ?) OR (b = ?))", c5.fragment) 31 | require.Equal(t, []interface{}{1, 1, "x"}, c5.args) 32 | 33 | require.Equal(t, "a = ?", c1.fragment) 34 | require.Equal(t, []interface{}{1}, c1.args) 35 | require.Equal(t, "b = ?", c2.fragment) 36 | require.Equal(t, []interface{}{"x"}, c2.args) 37 | } 38 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/config.gotmpl: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | {{- if eq $.client.Adapter "mysql" }} 7 | "net/url" 8 | "fmt" 9 | {{- end }} 10 | {{- if eq $.client.Adapter "sqlite" }}"strings"{{- end }} 11 | ) 12 | 13 | type Config struct { 14 | URL string 15 | } 16 | 17 | func NewConfig(env string) *Config { 18 | switch env { 19 | {{- range $c := $.client.Configs }} 20 | case "{{ $c.Environment }}": 21 | return &Config{ 22 | URL: fixURL({{ if $c.URL.EnvKey }}getenv("{{ $c.URL.EnvKey }}"){{ else }}"{{ $c.URL.Value }}"{{ end }}), 23 | } 24 | {{- end }} 25 | } 26 | return nil 27 | } 28 | 29 | {{- if eq $.client.Adapter "postgresql" }} 30 | func fixURL(rawURL string) string { 31 | return rawURL 32 | } 33 | {{- else if eq $.client.Adapter "mysql" }} 34 | func fixURL(rawURL string) string { 35 | u, _ := url.Parse(rawURL) 36 | password, _ := u.User.Password() 37 | options, _ := url.ParseQuery(u.RawQuery) 38 | options.Set("parseTime", "true") 39 | options.Set("loc", "Asia/Shanghai") 40 | return fmt.Sprintf("%s:%s@tcp(%s)%s?%s", u.User.Username(), password, u.Host, u.Path, options.Encode()) 41 | } 42 | {{- else if eq $.client.Adapter "sqlite" }} 43 | func fixURL(rawURL string) string { 44 | return "file:" + strings.TrimPrefix(rawURL, "sqlite:") 45 | } 46 | {{- end }} 47 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/date.gotmpl: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "database/sql" 7 | "database/sql/driver" 8 | "encoding/json" 9 | "time" 10 | ) 11 | 12 | type Date struct { 13 | Val time.Time 14 | Valid bool 15 | Set bool 16 | } 17 | 18 | func parseDate(s string) (*time.Time, error) { 19 | loc, err := time.LoadLocation("{{ $.client.TimeZone}}") 20 | if err != nil { 21 | return nil, err 22 | } 23 | t, err := time.ParseInLocation("2006-01-02", s, loc) 24 | if err != nil { 25 | return nil, err 26 | } 27 | return &t, nil 28 | } 29 | 30 | func NewDate(v string) Date { 31 | t, err := parseDate(v) 32 | if err != nil { 33 | return Date{Set: true} 34 | } 35 | 36 | return Date{Val: *t, Valid: true, Set: true} 37 | } 38 | 39 | func NewNullableDate(v *string) Date { 40 | if v != nil { 41 | return NewDate(*v) 42 | } 43 | return Date{Set: true} 44 | } 45 | 46 | // Scan implements the Scanner interface. 47 | func (d *Date) Scan(value interface{}) error { 48 | n := sql.NullTime{} 49 | err := n.Scan(value) 50 | if err != nil { 51 | return err 52 | } 53 | d.Val, d.Valid = n.Time, n.Valid 54 | return nil 55 | } 56 | 57 | // Value implements the driver Valuer interface. 58 | func (d Date) Value() (driver.Value, error) { 59 | if !d.Valid { 60 | return nil, nil 61 | } 62 | return d.Val, nil 63 | } 64 | 65 | // MarshalJSON implements the json.Marshaler interface. 66 | func (d Date) MarshalJSON() ([]byte, error) { 67 | if !d.Valid { 68 | return json.Marshal(nil) 69 | } 70 | return json.Marshal(d.Val.Format("2006-01-02")) 71 | } 72 | 73 | // UnmarshalJSON implements the json.Unmarshaler interface. 74 | func (d *Date) UnmarshalJSON(data []byte) error { 75 | d.Set = true 76 | s := string(data) 77 | if s == "null" || s == "" { 78 | return nil 79 | } 80 | d.Valid = true 81 | s = s[len(`"`) : len(s)-len(`"`)] 82 | t, err := parseDate(s) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | d.Val = *t 88 | return nil 89 | } 90 | 91 | // String implements the stringer interface. 92 | func (d Date) String() string { 93 | if !d.Valid { 94 | return "null" 95 | } 96 | return d.Val.Format("2006-01-02") 97 | } 98 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/date_column.gotmpl: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | type DateColumn struct { 10 | Name string 11 | Table *Table 12 | } 13 | 14 | func (t *Table) NewDateColumn(name string) *DateColumn { 15 | return &DateColumn{ 16 | Table: t, 17 | Name: name, 18 | } 19 | } 20 | 21 | func (c *DateColumn) EQ(v string) *Clause { 22 | d, err := parseDate(v) 23 | if err != nil { 24 | return &Clause{ 25 | err: err, 26 | } 27 | } 28 | return &Clause{ 29 | fragment: fmt.Sprintf("%s.%s = ?", c.Table.Name, c.Name), 30 | args: []interface{}{d}, 31 | err: err, 32 | } 33 | } 34 | 35 | func (c *DateColumn) LE(v string) *Clause { 36 | d, err := parseDate(v) 37 | if err != nil { 38 | return &Clause{ 39 | err: err, 40 | } 41 | } 42 | return &Clause{ 43 | fragment: fmt.Sprintf("%s.%s <= ?", c.Table.Name, c.Name), 44 | args: []interface{}{d}, 45 | err: err, 46 | } 47 | } 48 | 49 | func (c *DateColumn) LT(v string) *Clause { 50 | d, err := parseDate(v) 51 | if err != nil { 52 | return &Clause{ 53 | err: err, 54 | } 55 | } 56 | return &Clause{ 57 | fragment: fmt.Sprintf("%s.%s <=?", c.Table.Name, c.Name), 58 | args: []interface{}{d}, 59 | err: err, 60 | } 61 | } 62 | 63 | func (c *DateColumn) GE(v string) *Clause { 64 | d, err := parseDate(v) 65 | if err != nil { 66 | return &Clause{ 67 | err: err, 68 | } 69 | } 70 | return &Clause{ 71 | fragment: fmt.Sprintf("%s.%s >= ?", c.Table.Name, c.Name), 72 | args: []interface{}{d}, 73 | err: err, 74 | } 75 | } 76 | 77 | func (c *DateColumn) GT(v string) *Clause { 78 | d, err := parseDate(v) 79 | if err != nil { 80 | return &Clause{ 81 | err: err, 82 | } 83 | } 84 | return &Clause{ 85 | fragment: fmt.Sprintf("%s.%s > ?", c.Table.Name, c.Name), 86 | args: []interface{}{d}, 87 | err: err, 88 | } 89 | } 90 | 91 | func (c *DateColumn) Asc() string { 92 | return fmt.Sprintf("%s.%s ASC", c.Table.Name, c.Name) 93 | } 94 | 95 | func (c *DateColumn) Desc() string { 96 | return fmt.Sprintf("%s.%s DESC", c.Table.Name, c.Name) 97 | } 98 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/date_test.gotmpl: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "encoding/json" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestNewDate(t *testing.T) { 13 | d1 := NewDate("2012-11-10") 14 | require.Equal(t, "2012-11-10", d1.Val.Format("2006-01-02")) 15 | require.True(t, d1.Valid) 16 | 17 | d2 := NewNullableDate(nil) 18 | require.False(t, d2.Valid) 19 | } 20 | 21 | func TestDateJSON(t *testing.T) { 22 | type Foo struct { 23 | X Date `json:"x"` 24 | Y Date `json:"y"` 25 | } 26 | x := NewDate("2012-11-10") 27 | y := NewNullableDate(nil) 28 | s := `{"x":"2012-11-10","y":null}` 29 | 30 | f1 := Foo{X: x, Y: y} 31 | b, err := json.Marshal(f1) 32 | require.NoError(t, err) 33 | require.Equal(t, s, string(b)) 34 | 35 | var f2 Foo 36 | err = json.Unmarshal([]byte(s), &f2) 37 | require.NoError(t, err) 38 | require.Equal(t, x, f2.X) 39 | require.Equal(t, y, f2.Y) 40 | } 41 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/datetime.gotmpl: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "database/sql" 7 | "database/sql/driver" 8 | "encoding/json" 9 | "time" 10 | ) 11 | 12 | type Datetime struct { 13 | Val time.Time 14 | Valid bool 15 | Set bool 16 | } 17 | 18 | func loadLocation() (*time.Location, error) { 19 | return time.LoadLocation("{{ $.client.TimeZone }}") 20 | } 21 | 22 | func parseDatetime(s string) (*time.Time, error) { 23 | loc, err := loadLocation() 24 | if err != nil { 25 | return nil, err 26 | } 27 | t, err := time.ParseInLocation("2006-01-02 15:04:05", s, loc) 28 | if err != nil { 29 | return nil, err 30 | } 31 | return &t, nil 32 | } 33 | 34 | func Now(layout string) string { 35 | loc, _ := loadLocation() 36 | return time.Now().In(loc).Format(layout) 37 | } 38 | 39 | func NewDatetime(v string) Datetime { 40 | t, err := parseDatetime(v) 41 | if err != nil { 42 | return Datetime{Set: true} 43 | } 44 | return Datetime{Val: *t, Valid: true, Set: true} 45 | } 46 | 47 | func NewNullableDatetime(v *string) Datetime { 48 | if v != nil { 49 | return NewDatetime(*v) 50 | } 51 | return Datetime{Set: true} 52 | } 53 | 54 | // Scan implements the Scanner interface. 55 | func (d *Datetime) Scan(value interface{}) error { 56 | n := sql.NullTime{} 57 | err := n.Scan(value) 58 | if err != nil { 59 | return err 60 | } 61 | d.Val, d.Valid = n.Time, n.Valid 62 | loc, err := loadLocation() 63 | if err != nil { 64 | return err 65 | } 66 | d.Val = d.Val.In(loc) 67 | return nil 68 | } 69 | 70 | // Value implements the driver Valuer interface. 71 | func (d Datetime) Value() (driver.Value, error) { 72 | if !d.Valid { 73 | return nil, nil 74 | } 75 | return d.Val.UTC(), nil 76 | } 77 | 78 | // MarshalJSON implements the json.Marshaler interface. 79 | func (d Datetime) MarshalJSON() ([]byte, error) { 80 | if !d.Valid { 81 | return json.Marshal(nil) 82 | } 83 | return json.Marshal(d.Val.UTC()) 84 | } 85 | 86 | // UnmarshalJSON implements the json.Unmarshaler interface. 87 | func (d *Datetime) UnmarshalJSON(data []byte) error { 88 | d.Set = true 89 | s := string(data) 90 | if s == "null" || s == "" { 91 | return nil 92 | } 93 | d.Valid = true 94 | t := time.Time{} 95 | err := t.UnmarshalJSON(data) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | location, err := loadLocation() 101 | if err != nil { 102 | return err 103 | } 104 | d.Val = t.In(location) 105 | 106 | return nil 107 | } 108 | 109 | // String implements the stringer interface. 110 | func (d Datetime) String() string { 111 | if !d.Valid { 112 | return "null" 113 | } 114 | return d.Val.Format("2006-01-02 15:04:05") 115 | } 116 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/datetime_column.gotmpl: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | type DatetimeColumn struct { 10 | Name string 11 | Table *Table 12 | } 13 | 14 | func (t *Table) NewDatetimeColumn(name string) *DatetimeColumn { 15 | return &DatetimeColumn{ 16 | Table: t, 17 | Name: name, 18 | } 19 | } 20 | 21 | func (c *DatetimeColumn) EQ(v string) *Clause { 22 | d, err := parseDatetime(v) 23 | if err != nil { 24 | return &Clause{ 25 | err: err, 26 | } 27 | } 28 | return &Clause{ 29 | fragment: fmt.Sprintf("%s.%s = ?", c.Table.Name, c.Name), 30 | args: []interface{}{d.UTC()}, 31 | err: err, 32 | } 33 | } 34 | 35 | func (c *DatetimeColumn) LE(v string) *Clause { 36 | d, err := parseDatetime(v) 37 | if err != nil { 38 | return &Clause{ 39 | err: err, 40 | } 41 | } 42 | return &Clause{ 43 | fragment: fmt.Sprintf("%s.%s <= ?", c.Table.Name, c.Name), 44 | args: []interface{}{d.UTC()}, 45 | err: err, 46 | } 47 | } 48 | 49 | func (c *DatetimeColumn) LT(v string) *Clause { 50 | d, err := parseDatetime(v) 51 | if err != nil { 52 | return &Clause{ 53 | err: err, 54 | } 55 | } 56 | return &Clause{ 57 | fragment: fmt.Sprintf("%s.%s <=?", c.Table.Name, c.Name), 58 | args: []interface{}{d.UTC()}, 59 | err: err, 60 | } 61 | } 62 | 63 | func (c *DatetimeColumn) GE(v string) *Clause { 64 | d, err := parseDatetime(v) 65 | if err != nil { 66 | return &Clause{ 67 | err: err, 68 | } 69 | } 70 | return &Clause{ 71 | fragment: fmt.Sprintf("%s.%s >= ?", c.Table.Name, c.Name), 72 | args: []interface{}{d.UTC()}, 73 | err: err, 74 | } 75 | } 76 | 77 | func (c *DatetimeColumn) GT(v string) *Clause { 78 | d, err := parseDatetime(v) 79 | if err != nil { 80 | return &Clause{ 81 | err: err, 82 | } 83 | } 84 | return &Clause{ 85 | fragment: fmt.Sprintf("%s.%s > ?", c.Table.Name, c.Name), 86 | args: []interface{}{d.UTC()}, 87 | err: err, 88 | } 89 | } 90 | 91 | func (c *DatetimeColumn) Asc() string { 92 | return fmt.Sprintf("%s.%s ASC", c.Table.Name, c.Name) 93 | } 94 | 95 | func (c *DatetimeColumn) Desc() string { 96 | return fmt.Sprintf("%s.%s DESC", c.Table.Name, c.Name) 97 | } 98 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/datetime_test.gotmpl: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "encoding/json" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestNewDatetime(t *testing.T) { 13 | d1 := NewDatetime("2012-12-12 11:10:09") 14 | require.Equal(t, "2012-12-12 11:10:09", d1.Val.Format("2006-01-02 15:04:05")) 15 | require.True(t, d1.Valid) 16 | 17 | d2 := NewNullableDatetime(nil) 18 | require.False(t, d2.Valid) 19 | } 20 | 21 | func TestDatetimeJSON(t *testing.T) { 22 | type Foo struct { 23 | X Datetime `json:"x"` 24 | Y Datetime `json:"y"` 25 | } 26 | x := NewDatetime("2012-12-12 11:10:09") 27 | y := NewNullableDatetime(nil) 28 | s := `{"x":"2012-12-12T03:10:09Z","y":null}` 29 | 30 | f1 := Foo{X: x, Y: y} 31 | b, err := json.Marshal(f1) 32 | require.NoError(t, err) 33 | require.Equal(t, s, string(b)) 34 | 35 | var f2 Foo 36 | err = json.Unmarshal([]byte(s), &f2) 37 | require.NoError(t, err) 38 | require.Equal(t, x, f2.X) 39 | require.Equal(t, y, f2.Y) 40 | } 41 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/db.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "database/sql" 7 | "regexp" 8 | ) 9 | 10 | type DB interface { 11 | Exec(query string, args ...interface{}) (sql.Result, error) 12 | Query(query string, args ...interface{}) (*sql.Rows, error) 13 | } 14 | 15 | type Adapter struct { 16 | db DB 17 | } 18 | 19 | func NewAdapter(db DB) *Adapter { 20 | return &Adapter{db: db} 21 | } 22 | 23 | func (a *Adapter) Query(query string, args ...interface{}) *Rows { 24 | return &Rows{ 25 | adapter: a, 26 | query: query, 27 | args: args, 28 | } 29 | } 30 | 31 | type Rows struct { 32 | adapter *Adapter 33 | query string 34 | args []interface{} 35 | err error 36 | } 37 | 38 | func (r *Rows) Scan(v interface{}) error { 39 | if r.err != nil { 40 | return r.err 41 | } 42 | var err error 43 | query, args := r.query, r.args 44 | matched1, err := regexp.MatchString(`.* IN (.*?)`, query) 45 | if err != nil { 46 | return err 47 | } 48 | matched2, err := regexp.MatchString(`.* in (.*?)`, query) 49 | if err != nil { 50 | return err 51 | } 52 | if matched1 || matched2 { 53 | query, args, err = In(query, args...) 54 | if err != nil { 55 | return err 56 | } 57 | } 58 | query, args = rebind(query, args) 59 | rows, err := r.adapter.db.Query(query, args...) 60 | if err != nil { 61 | return err 62 | } 63 | defer rows.Close() 64 | 65 | err = ScanSlice(rows, v) 66 | if err != nil { 67 | return err 68 | } 69 | return err 70 | } 71 | 72 | type Row struct { 73 | adapter *Adapter 74 | query string 75 | args []interface{} 76 | } 77 | 78 | func (r *Row) Scan(v interface{}) error { 79 | query, args := r.query, r.args 80 | matched1, err := regexp.MatchString(`.* IN (.*?)`, query) 81 | if err != nil { 82 | return err 83 | } 84 | matched2, err := regexp.MatchString(`.* in (.*?)`, query) 85 | if err != nil { 86 | return err 87 | } 88 | if matched1 || matched2 { 89 | query, args, err = In(query, args...) 90 | if err != nil { 91 | return err 92 | } 93 | } 94 | query, args = rebind(query, args) 95 | rows, err := r.adapter.db.Query(query, args...) 96 | if err != nil { 97 | return err 98 | } 99 | defer rows.Close() 100 | 101 | err = ScanOne(rows, v) 102 | if err != nil { 103 | return err 104 | } 105 | return err 106 | } 107 | 108 | func (a *Adapter) QueryOne(query string, args ...interface{}) *Row { 109 | return &Row{ 110 | adapter: a, 111 | query: query, 112 | args: args, 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/delete.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import "fmt" 6 | 7 | type DeleteStatemnet struct { 8 | from string 9 | where *Clause 10 | } 11 | 12 | func NewDelete() *DeleteStatemnet { 13 | return &DeleteStatemnet{} 14 | } 15 | 16 | func (s *DeleteStatemnet) From(from string) *DeleteStatemnet { 17 | s.from = from 18 | return s 19 | } 20 | 21 | func (s *DeleteStatemnet) Where(expr *Clause) *DeleteStatemnet { 22 | s.where = expr 23 | return s 24 | } 25 | 26 | func (s *DeleteStatemnet) ToSQL() (string, []interface{}) { 27 | sql, args := "", []interface{}{} 28 | sql = fmt.Sprintf("DELETE FROM %s", s.from) 29 | 30 | if s.where != nil { 31 | sql = fmt.Sprintf("%s WHERE %s", sql, s.where.fragment) 32 | args = append(args, s.where.args...) 33 | } 34 | 35 | return sql, args 36 | } 37 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/delete_test.go: -------------------------------------------------------------------------------- 1 | package queryx 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestNewDelete(t *testing.T) { 10 | s := NewDelete().From("users") 11 | 12 | sql, args := s.ToSQL() 13 | require.Equal(t, "DELETE FROM users", sql) 14 | require.Equal(t, []interface{}{}, args) 15 | } 16 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/env.go: -------------------------------------------------------------------------------- 1 | package queryx 2 | 3 | import "os" 4 | 5 | func getenv(k string) string { 6 | return os.Getenv(k) 7 | } 8 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/float.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "database/sql" 7 | "database/sql/driver" 8 | "encoding/json" 9 | "strconv" 10 | ) 11 | 12 | type Float struct { 13 | Val float64 14 | Valid bool 15 | Set bool 16 | } 17 | 18 | func NewFloat(v float64) Float { 19 | return Float{Val: v, Valid: true, Set: true} 20 | } 21 | 22 | func NewNullableFloat(v *float64) Float { 23 | if v != nil { 24 | return NewFloat(*v) 25 | } 26 | return Float{Set: true} 27 | } 28 | 29 | // Scan implements the Scanner interface. 30 | func (f *Float) Scan(value interface{}) error { 31 | ns := sql.NullFloat64{Float64: f.Val} 32 | err := ns.Scan(value) 33 | f.Val, f.Valid = ns.Float64, ns.Valid 34 | return err 35 | } 36 | 37 | // Value implements the driver Valuer interface. 38 | func (f Float) Value() (driver.Value, error) { 39 | if !f.Valid { 40 | return nil, nil 41 | } 42 | return float64(f.Val), nil 43 | } 44 | 45 | // MarshalJSON implements the json.Marshaler interface. 46 | func (f Float) MarshalJSON() ([]byte, error) { 47 | if !f.Valid { 48 | return json.Marshal(nil) 49 | } 50 | return json.Marshal(f.Val) 51 | } 52 | 53 | // UnmarshalJSON implements the json.Unmarshaler interface. 54 | func (f *Float) UnmarshalJSON(data []byte) error { 55 | f.Set = true 56 | if string(data) == "null" { 57 | return nil 58 | } 59 | f.Valid = true 60 | if err := json.Unmarshal(data, &f.Val); err != nil { 61 | return err 62 | } 63 | return nil 64 | } 65 | 66 | // String implements the stringer interface. 67 | func (f Float) String() string { 68 | if !f.Valid { 69 | return "null" 70 | } 71 | return strconv.FormatFloat(f.Val, 'f', -1, 64) 72 | } 73 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/float_column.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import "fmt" 6 | 7 | type FloatColumn struct { 8 | Name string 9 | Table *Table 10 | } 11 | 12 | func (t *Table) NewFloatColumn(name string) *FloatColumn { 13 | return &FloatColumn{ 14 | Table: t, 15 | Name: name, 16 | } 17 | } 18 | 19 | func (c *FloatColumn) EQ(v float64) *Clause { 20 | return &Clause{ 21 | fragment: fmt.Sprintf("%s.%s = ?", c.Table.Name, c.Name), 22 | args: []interface{}{v}, 23 | } 24 | } 25 | 26 | func (c *FloatColumn) GT(v float64) *Clause { 27 | return &Clause{ 28 | fragment: fmt.Sprintf("%s.%s > ?", c.Table.Name, c.Name), 29 | args: []interface{}{v}, 30 | } 31 | } 32 | 33 | func (c *FloatColumn) LT(v float64) *Clause { 34 | return &Clause{ 35 | fragment: fmt.Sprintf("%s.%s < ?", c.Table.Name, c.Name), 36 | args: []interface{}{v}, 37 | } 38 | } 39 | 40 | func (c *FloatColumn) GE(v float64) *Clause { 41 | return &Clause{ 42 | fragment: fmt.Sprintf("%s.%s >= ?", c.Table.Name, c.Name), 43 | args: []interface{}{v}, 44 | } 45 | } 46 | 47 | func (c *FloatColumn) LE(v float64) *Clause { 48 | return &Clause{ 49 | fragment: fmt.Sprintf("%s.%s <= ?", c.Table.Name, c.Name), 50 | args: []interface{}{v}, 51 | } 52 | } 53 | 54 | func (c *FloatColumn) In(v []float64) *Clause { 55 | return &Clause{ 56 | fragment: fmt.Sprintf("%s.%s IN (?)", c.Table.Name, c.Name), 57 | args: []interface{}{v}, 58 | } 59 | } 60 | 61 | func (c *FloatColumn) Asc() string { 62 | return fmt.Sprintf("%s.%s ASC", c.Table.Name, c.Name) 63 | } 64 | 65 | func (c *FloatColumn) Desc() string { 66 | return fmt.Sprintf("%s.%s DESC", c.Table.Name, c.Name) 67 | } 68 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/float_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "encoding/json" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestNewFloat(t *testing.T) { 13 | f1 := NewFloat(2.1) 14 | require.Equal(t, 2.1, f1.Val) 15 | require.True(t, f1.Valid) 16 | 17 | f2 := NewNullableFloat(nil) 18 | require.False(t, f2.Valid) 19 | } 20 | 21 | func TestFloatJSON(t *testing.T) { 22 | type Foo struct { 23 | X Float `json:"x"` 24 | Y Float `json:"y"` 25 | } 26 | x := NewFloat(2.1) 27 | y := NewNullableFloat(nil) 28 | s := `{"x":2.1,"y":null}` 29 | 30 | f1 := Foo{X: x, Y: y} 31 | b, err := json.Marshal(f1) 32 | require.NoError(t, err) 33 | require.Equal(t, s, string(b)) 34 | 35 | var f2 Foo 36 | err = json.Unmarshal([]byte(s), &f2) 37 | require.NoError(t, err) 38 | require.Equal(t, x, f2.X) 39 | require.Equal(t, y, f2.Y) 40 | } 41 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/insert.gotmpl: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "fmt" 7 | "strings" 8 | ) 9 | 10 | type InsertStatement struct { 11 | into string 12 | columns []string 13 | values [][]interface{} 14 | returning []string 15 | onConflict string 16 | } 17 | 18 | func NewInsert() *InsertStatement { 19 | return &InsertStatement{} 20 | } 21 | 22 | func (s *InsertStatement) Into(into string) *InsertStatement { 23 | s.into = into 24 | return s 25 | } 26 | 27 | func (s *InsertStatement) Columns(columns ...string) *InsertStatement { 28 | s.columns = columns 29 | return s 30 | } 31 | 32 | func (s *InsertStatement) Values(values ...interface{}) *InsertStatement { 33 | if len(values) > 0 { 34 | s.values = append(s.values, values) 35 | } 36 | return s 37 | } 38 | 39 | {{- if or (eq $.client.Adapter "postgresql") (eq $.client.Adapter "sqlite") }} 40 | func (s *InsertStatement) Returning(returning ...string) *InsertStatement { 41 | s.returning = returning 42 | return s 43 | } 44 | {{- end }} 45 | 46 | func (s *InsertStatement) OnConflict(onConflict string) *InsertStatement { 47 | s.onConflict = onConflict 48 | return s 49 | } 50 | 51 | func (s *InsertStatement) ToSQL() (string, []interface{}) { 52 | sql := fmt.Sprintf("INSERT INTO %s", s.into) 53 | 54 | if len(s.columns) > 0 { 55 | {{- if eq $.client.Adapter "mysql" }} 56 | sql = fmt.Sprintf("%s(`%s`)", sql, strings.Join(s.columns, "`, `")) 57 | {{- else }} 58 | sql = fmt.Sprintf("%s(%s)", sql, strings.Join(s.columns, ", ")) 59 | {{- end }} 60 | } else { 61 | {{- if eq $.client.Adapter "mysql" }} 62 | sql = fmt.Sprintf("%s VALUES ()", sql) 63 | {{- else }} 64 | sql = fmt.Sprintf("%s DEFAULT VALUES", sql) 65 | {{- end }} 66 | } 67 | values := []string{} 68 | for _, v := range s.values { 69 | ss := []string{} 70 | for range v { 71 | ss = append(ss, "?") 72 | } 73 | values = append(values, fmt.Sprintf("(%s)", strings.Join(ss, ", "))) 74 | } 75 | if len(values) > 0 { 76 | sql = fmt.Sprintf("%s VALUES %s", sql, strings.Join(values, ", ")) 77 | } 78 | 79 | if len(s.returning) > 0 { 80 | sql = fmt.Sprintf("%s RETURNING %s", sql, strings.Join(s.returning, ", ")) 81 | } 82 | 83 | if s.onConflict != "" { 84 | sql = fmt.Sprintf("%s %s", sql, s.onConflict) 85 | } 86 | 87 | args := []interface{}{} 88 | for _, v := range s.values { 89 | args = append(args, v...) 90 | } 91 | 92 | return sql, args 93 | } 94 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/insert_test.gotmpl: -------------------------------------------------------------------------------- 1 | package queryx 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | {{- if eq .client.Adapter "mysql" }} 10 | func TestNewInsert(t *testing.T) { 11 | s1 := NewInsert().Into("users") 12 | sql, args := s1.ToSQL() 13 | require.Equal(t, "INSERT INTO users VALUES ()", sql) 14 | require.Equal(t, []interface{}{}, args) 15 | 16 | var columns []string 17 | var values []interface{} 18 | s2 := NewInsert().Into("users").Columns(columns...).Values(values...) 19 | sql, args = s2.ToSQL() 20 | require.Equal(t, "INSERT INTO users VALUES ()", sql) 21 | require.Equal(t, []interface{}{}, args) 22 | 23 | s3 := NewInsert().Into("users").Columns("name", "email").Values("test", "test@example.com") 24 | sql, args = s3.ToSQL() 25 | require.Equal(t, "INSERT INTO users(`name`, `email`) VALUES (?, ?)", sql) 26 | require.Equal(t, []interface{}{"test", "test@example.com"}, args) 27 | 28 | s4 := NewInsert().Into("users").Columns("name", "email"). 29 | Values("test1", "test1@example.com"). 30 | Values("test2", "test2@example.com") 31 | sql, args = s4.ToSQL() 32 | require.Equal(t, "INSERT INTO users(`name`, `email`) VALUES (?, ?), (?, ?)", sql) 33 | require.Equal(t, []interface{}{"test1", "test1@example.com", "test2", "test2@example.com"}, args) 34 | } 35 | {{- else }} 36 | func TestNewInsert(t *testing.T) { 37 | s1 := NewInsert().Into("users") 38 | sql, args := s1.ToSQL() 39 | require.Equal(t, "INSERT INTO users DEFAULT VALUES", sql) 40 | require.Equal(t, []interface{}{}, args) 41 | 42 | var columns []string 43 | var values []interface{} 44 | s2 := NewInsert().Into("users").Columns(columns...).Values(values...).Returning("id", "name") 45 | sql, args = s2.ToSQL() 46 | require.Equal(t, "INSERT INTO users DEFAULT VALUES RETURNING id, name", sql) 47 | require.Equal(t, []interface{}{}, args) 48 | 49 | s3 := NewInsert().Into("users").Columns("name", "email").Values("test", "test@example.com") 50 | sql, args = s3.ToSQL() 51 | require.Equal(t, "INSERT INTO users(name, email) VALUES (?, ?)", sql) 52 | require.Equal(t, []interface{}{"test", "test@example.com"}, args) 53 | 54 | s4 := NewInsert().Into("users").Columns("name", "email"). 55 | Values("test1", "test1@example.com"). 56 | Values("test2", "test2@example.com") 57 | sql, args = s4.ToSQL() 58 | require.Equal(t, "INSERT INTO users(name, email) VALUES (?, ?), (?, ?)", sql) 59 | require.Equal(t, []interface{}{"test1", "test1@example.com", "test2", "test2@example.com"}, args) 60 | } 61 | {{- end }} 62 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/integer.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "database/sql" 7 | "database/sql/driver" 8 | "encoding/json" 9 | "strconv" 10 | ) 11 | 12 | type Integer struct { 13 | Val int32 14 | Valid bool 15 | Set bool 16 | } 17 | 18 | func NewInteger(v int32) Integer { 19 | return Integer{Val: v, Valid: true, Set: true} 20 | } 21 | 22 | func NewNullableInteger(v *int32) Integer { 23 | if v != nil { 24 | return NewInteger(*v) 25 | } 26 | return Integer{Set: true} 27 | } 28 | 29 | // Scan implements the Scanner interface. 30 | func (i *Integer) Scan(value interface{}) error { 31 | n := sql.NullInt64{} 32 | err := n.Scan(value) 33 | if err != nil { 34 | return err 35 | } 36 | i.Val, i.Valid = int32(n.Int64), n.Valid 37 | return nil 38 | } 39 | 40 | // Value implements the driver Valuer interface. 41 | func (i Integer) Value() (driver.Value, error) { 42 | if !i.Valid { 43 | return nil, nil 44 | } 45 | return int64(i.Val), nil 46 | } 47 | 48 | // MarshalJSON implements the json.Marshaler interface. 49 | func (i Integer) MarshalJSON() ([]byte, error) { 50 | if !i.Valid { 51 | return json.Marshal(nil) 52 | } 53 | return json.Marshal(i.Val) 54 | } 55 | 56 | // UnmarshalJSON implements the json.Unmarshaler interface. 57 | func (i *Integer) UnmarshalJSON(data []byte) error { 58 | i.Set = true 59 | s := string(data) 60 | if s == "null" { 61 | return nil 62 | } 63 | i.Valid = true 64 | p, err := strconv.ParseInt(s, 10, 32) 65 | if err != nil { 66 | return err 67 | } 68 | i.Val = int32(p) 69 | return nil 70 | } 71 | 72 | // String implements the stringer interface. 73 | func (i Integer) String() string { 74 | if !i.Valid { 75 | return "null" 76 | } 77 | return strconv.Itoa(int(i.Val)) 78 | } 79 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/integer_column.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import "fmt" 6 | 7 | type IntegerColumn struct { 8 | Name string 9 | Table *Table 10 | } 11 | 12 | func (t *Table) NewIntegerColumn(name string) *IntegerColumn { 13 | return &IntegerColumn{ 14 | Table: t, 15 | Name: name, 16 | } 17 | } 18 | 19 | func (c *IntegerColumn) EQ(v int32) *Clause { 20 | return &Clause{ 21 | fragment: fmt.Sprintf("%s.%s = ?", c.Table.Name, c.Name), 22 | args: []interface{}{v}, 23 | } 24 | } 25 | 26 | func (c *IntegerColumn) NE(v int32) *Clause { 27 | return &Clause{ 28 | fragment: fmt.Sprintf("%s.%s <> ?", c.Table.Name, c.Name), 29 | args: []interface{}{v}, 30 | } 31 | } 32 | 33 | func (c *IntegerColumn) LT(v int32) *Clause { 34 | return &Clause{ 35 | fragment: fmt.Sprintf("%s.%s < ?", c.Table.Name, c.Name), 36 | args: []interface{}{v}, 37 | } 38 | } 39 | 40 | func (c *IntegerColumn) GT(v int32) *Clause { 41 | return &Clause{ 42 | fragment: fmt.Sprintf("%s.%s > ?", c.Table.Name, c.Name), 43 | args: []interface{}{v}, 44 | } 45 | } 46 | 47 | func (c *IntegerColumn) LE(v int32) *Clause { 48 | return &Clause{ 49 | fragment: fmt.Sprintf("%s.%s <= ?", c.Table.Name, c.Name), 50 | args: []interface{}{v}, 51 | } 52 | } 53 | 54 | func (c *IntegerColumn) GE(v int32) *Clause { 55 | return &Clause{ 56 | fragment: fmt.Sprintf("%s.%s >= ?", c.Table.Name, c.Name), 57 | args: []interface{}{v}, 58 | } 59 | } 60 | 61 | func (c *IntegerColumn) In(v []int32) *Clause { 62 | return &Clause{ 63 | fragment: fmt.Sprintf("%s.%s IN (?)", c.Table.Name, c.Name), 64 | args: []interface{}{v}, 65 | } 66 | } 67 | 68 | func (c *IntegerColumn) InRange(start int32, end int32) *Clause { 69 | return &Clause{ 70 | fragment: fmt.Sprintf("%s.%s >= ? and %s.%s< ?", c.Table.Name, c.Name, c.Table.Name, c.Name), 71 | args: []interface{}{start, end}, 72 | } 73 | } 74 | 75 | func (c *IntegerColumn) Between(start int32, end int32) *Clause { 76 | return &Clause{ 77 | fragment: fmt.Sprintf("%s.%s >= ? and %s.%s<= ?", c.Table.Name, c.Name, c.Table.Name, c.Name), 78 | args: []interface{}{start, end}, 79 | } 80 | } 81 | 82 | func (c *IntegerColumn) Asc() string { 83 | return fmt.Sprintf("%s.%s ASC", c.Table.Name, c.Name) 84 | } 85 | 86 | func (c *IntegerColumn) Desc() string { 87 | return fmt.Sprintf("%s.%s DESC", c.Table.Name, c.Name) 88 | } 89 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/integer_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "encoding/json" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestNewInteger(t *testing.T) { 13 | i1 := NewInteger(2) 14 | require.Equal(t, int32(2), i1.Val) 15 | require.True(t, i1.Valid) 16 | 17 | i2 := NewNullableInteger(nil) 18 | require.False(t, i2.Valid) 19 | } 20 | 21 | func TestIntegerJSON(t *testing.T) { 22 | type Foo struct { 23 | X Integer `json:"x"` 24 | Y Integer `json:"y"` 25 | } 26 | x := NewInteger(2) 27 | y := NewNullableInteger(nil) 28 | s := `{"x":2,"y":null}` 29 | 30 | f1 := Foo{X: x, Y: y} 31 | b, err := json.Marshal(f1) 32 | require.NoError(t, err) 33 | require.Equal(t, s, string(b)) 34 | 35 | var f2 Foo 36 | err = json.Unmarshal([]byte(s), &f2) 37 | require.NoError(t, err) 38 | require.Equal(t, x, f2.X) 39 | require.Equal(t, y, f2.Y) 40 | } 41 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/json.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "database/sql/driver" 7 | "encoding/json" 8 | "fmt" 9 | ) 10 | 11 | type JSON struct { 12 | Val map[string]interface{} 13 | Valid bool 14 | Set bool 15 | } 16 | 17 | func NewJSON(v map[string]interface{}) JSON { 18 | return JSON{Val: v, Valid: true, Set: true} 19 | } 20 | 21 | func NewNullableJSON(v interface{}) JSON { 22 | if v != nil { 23 | return NewJSON(v.(map[string]interface{})) 24 | } 25 | return JSON{Set: true} 26 | } 27 | 28 | // Scan implements the Scanner interface. 29 | func (j *JSON) Scan(value interface{}) error { 30 | if value == nil { 31 | j.Val, j.Valid = nil, false 32 | return nil 33 | } 34 | b, ok := value.([]byte) 35 | if !ok { 36 | return fmt.Errorf("JSON scan source was not []byte") 37 | } 38 | return json.Unmarshal(b, &j.Val) 39 | } 40 | 41 | // Value implements the driver Valuer interface. 42 | func (j JSON) Value() (driver.Value, error) { 43 | return json.Marshal(j.Val) 44 | } 45 | 46 | // MarshalJSON implements the json.Marshaler interface. 47 | func (j JSON) MarshalJSON() ([]byte, error) { 48 | if !j.Valid { 49 | return json.Marshal(nil) 50 | } 51 | return json.Marshal(j.Val) 52 | } 53 | 54 | // UnmarshalJSON implements the json.Unmarshaler interface. 55 | func (j *JSON) UnmarshalJSON(data []byte) error { 56 | j.Set = true 57 | s := string(data) 58 | if s == "{}" || s == "null" { 59 | return nil 60 | } 61 | j.Valid = true 62 | m := map[string]interface{}{} 63 | err := json.Unmarshal(data, &m) 64 | if err != nil { 65 | return err 66 | } 67 | j.Val = m 68 | return nil 69 | } 70 | 71 | // String implements the stringer interface. 72 | func (j JSON) String() string { 73 | if !j.Valid { 74 | return "null" 75 | } 76 | return fmt.Sprintf("%+v", j.Val) 77 | } 78 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/json_column.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | type JSONColumn struct { 10 | Name string 11 | Table *Table 12 | } 13 | 14 | func (t *Table) NewJSONColumn(name string) *JSONColumn { 15 | return &JSONColumn{ 16 | Table: t, 17 | Name: name, 18 | } 19 | } 20 | 21 | func (c *JSONColumn) IsNull() *Clause { 22 | return &Clause{ 23 | fragment: fmt.Sprintf("%s.%s is null", c.Table.Name, c.Name), 24 | } 25 | } 26 | 27 | func (c *JSONColumn) NotNull() *Clause { 28 | return &Clause{ 29 | fragment: fmt.Sprintf("%s.%s is not null", c.Table.Name, c.Name), 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/json_test.go: -------------------------------------------------------------------------------- 1 | package queryx 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestNewJSON(t *testing.T) { 11 | m := map[string]interface{}{"a": 1} 12 | j1 := NewJSON(m) 13 | require.Equal(t, m, j1.Val) 14 | require.True(t, j1.Valid) 15 | 16 | j2 := NewNullableJSON(nil) 17 | require.False(t, j2.Valid) 18 | 19 | j3 := NewNullableJSON(m) 20 | require.True(t, j3.Valid) 21 | } 22 | 23 | func TestJSONJSON(t *testing.T) { 24 | type Foo struct { 25 | X JSON `json:"x"` 26 | Y JSON `json:"y"` 27 | } 28 | x := NewJSON(map[string]interface{}{"a": "b"}) 29 | y := NewNullableJSON(nil) 30 | s := `{"x":{"a":"b"},"y":null}` 31 | 32 | f1 := Foo{X: x, Y: y} 33 | b, err := json.Marshal(f1) 34 | require.NoError(t, err) 35 | require.Equal(t, s, string(b)) 36 | 37 | var f2 Foo 38 | err = json.Unmarshal([]byte(s), &f2) 39 | require.NoError(t, err) 40 | require.Equal(t, x, f2.X) 41 | require.Equal(t, y, f2.Y) 42 | } 43 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/logger.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | type Logger interface { 6 | Print(v ...interface{}) 7 | Printf(format string, v ...interface{}) 8 | Println(v ...interface{}) 9 | Panic(v ...interface{}) 10 | Panicf(format string, v ...interface{}) 11 | Panicln(v ...interface{}) 12 | Fatal(v ...interface{}) 13 | Fatalf(format string, v ...interface{}) 14 | Fatalln(v ...interface{}) 15 | } 16 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/scan.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "database/sql" 7 | "fmt" 8 | "reflect" 9 | "strings" 10 | ) 11 | 12 | func ScanOne(rows *sql.Rows, v interface{}) error { 13 | rv := reflect.ValueOf(v) 14 | if rv.Kind() != reflect.Ptr { 15 | return fmt.Errorf("must pass pointer") 16 | } 17 | 18 | columns, err := rows.Columns() 19 | if err != nil { 20 | return err 21 | } 22 | 23 | typ := rv.Type().Elem() 24 | 25 | // column -> index 26 | names := make(map[string]int, typ.NumField()) 27 | for i := 0; i < typ.NumField(); i++ { 28 | f := typ.Field(i) 29 | names[columnName(f)] = i 30 | } 31 | 32 | if !rows.Next() { 33 | return sql.ErrNoRows 34 | } 35 | 36 | indexes := make(map[int]int, typ.NumField()) 37 | for i, c := range columns { 38 | name := strings.ToLower(strings.Split(c, "(")[0]) 39 | index, ok := names[name] 40 | if !ok { 41 | return fmt.Errorf("name not found") 42 | } 43 | indexes[i] = index 44 | } 45 | 46 | values := make([]interface{}, len(columns)) 47 | for i := range columns { 48 | t := typ.Field(indexes[i]).Type 49 | values[i] = reflect.New(t).Interface() 50 | 51 | } 52 | 53 | // scan into interfaces 54 | if err := rows.Scan(values...); err != nil { 55 | return err 56 | } 57 | 58 | for i, v := range values { 59 | reflect.Indirect(rv).Field(indexes[i]).Set(reflect.Indirect(reflect.ValueOf(v))) 60 | } 61 | 62 | if rows.Next() { 63 | return fmt.Errorf("more than one row") 64 | } 65 | 66 | return nil 67 | } 68 | 69 | func ScanSlice(rows *sql.Rows, v interface{}) error { 70 | rv := reflect.ValueOf(v) 71 | if rv.Kind() != reflect.Ptr { 72 | return fmt.Errorf("must pass pointer") 73 | } 74 | 75 | rv = reflect.Indirect(rv) 76 | if k := rv.Kind(); k != reflect.Slice { 77 | return fmt.Errorf("must pass slice") 78 | } 79 | 80 | columns, err := rows.Columns() 81 | if err != nil { 82 | return err 83 | } 84 | 85 | typ := rv.Type().Elem() 86 | 87 | names := make(map[string]int, typ.NumField()) 88 | for i := 0; i < typ.NumField(); i++ { 89 | f := typ.Field(i) 90 | names[columnName(f)] = i 91 | } 92 | 93 | indexes := make(map[int]int, typ.NumField()) 94 | for i, c := range columns { 95 | name := strings.ToLower(strings.Split(c, "(")[0]) 96 | index, ok := names[name] 97 | if !ok { 98 | return fmt.Errorf("name %+v not found", name) 99 | } 100 | indexes[i] = index 101 | } 102 | 103 | for rows.Next() { 104 | values := make([]interface{}, len(columns)) 105 | for i := range columns { 106 | t := typ.Field(indexes[i]).Type 107 | values[i] = reflect.New(t).Interface() 108 | } 109 | 110 | // scan into interfaces 111 | if err := rows.Scan(values...); err != nil { 112 | return err 113 | } 114 | 115 | // convert to reflect.Value 116 | e := reflect.New(typ).Elem() 117 | for i, v := range values { 118 | fv := e.Field(indexes[i]) 119 | fv.Set(reflect.Indirect(reflect.ValueOf(v))) 120 | } 121 | 122 | vv := reflect.Append(rv, e) 123 | rv.Set(vv) 124 | } 125 | 126 | return nil 127 | } 128 | 129 | func columnName(f reflect.StructField) string { 130 | name := strings.ToLower(f.Name) 131 | if tag, ok := f.Tag.Lookup("db"); ok { 132 | name = tag 133 | } 134 | return name 135 | } 136 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/schema.gotmpl: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | type Schema struct { 6 | {{- range $m := $.client.Models }} 7 | {{ $m.Name }} *Table 8 | {{- range .Columns }} 9 | {{ $m.Name }}{{ firstWordUpperCamel .Name }} *{{goType .Type .Null .Array}}Column 10 | {{- end}} 11 | {{- end}} 12 | } 13 | 14 | func NewSchema() *Schema { 15 | {{- range $m := $.client.Models }} 16 | {{ camel $m.Name }} := NewTable("{{ $m.TableName }}") 17 | {{- end}} 18 | 19 | return &Schema{ 20 | {{- range $m := $.client.Models }} 21 | {{ $m.Name }}: {{ camel $m.Name }}, 22 | {{- range $c := $m.Columns }} 23 | {{ $m.Name }}{{ firstWordUpperCamel $c.Name }}: {{ camel $m.Name }}.New{{ goType $c.Type $c.Null $c.Array }}Column("{{ $c.Name }}"), 24 | {{- end }} 25 | {{- end }} 26 | } 27 | } 28 | 29 | func (s *Schema) And(clauses ...*Clause) *Clause { 30 | return clauses[0].And(clauses[1:]...) 31 | } 32 | 33 | func (s *Schema) Or(clauses ...*Clause) *Clause { 34 | return clauses[0].Or(clauses[1:]...) 35 | } 36 | 37 | {{- range $m := $.client.Models }} 38 | 39 | func (s *Schema) Change{{ $m.Name }}() *{{ $m.Name }}Change { 40 | return &{{ $m.Name }}Change{ 41 | {{- range $c := $m.Columns }} 42 | {{- $f := $c.Name | pascal }} 43 | {{ $f }}: {{ goType $c.Type $c.Null $c.Array }}{}, 44 | {{- end }} 45 | } 46 | } 47 | {{- end }} 48 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/select.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "fmt" 7 | "strings" 8 | ) 9 | 10 | type SelectStatement struct { 11 | selection []string 12 | from string 13 | where *Clause 14 | limit *int 15 | offset *int 16 | order []string 17 | group string 18 | having string 19 | distinct []string 20 | joins []string 21 | } 22 | 23 | func NewSelect() *SelectStatement { 24 | return &SelectStatement{} 25 | } 26 | 27 | func (s *SelectStatement) Select(selection ...string) *SelectStatement { 28 | s.selection = selection 29 | return s 30 | } 31 | 32 | func (s *SelectStatement) From(from string) *SelectStatement { 33 | s.from = from 34 | return s 35 | } 36 | 37 | func (s *SelectStatement) Where(clauses ...*Clause) *SelectStatement { 38 | if len(clauses) == 0 { 39 | return s 40 | } 41 | 42 | if s.where == nil { 43 | s.where = clauses[0].And(clauses[1:]...) 44 | } else { 45 | s.where = s.where.And(clauses...) 46 | } 47 | 48 | return s 49 | } 50 | 51 | func (s *SelectStatement) Limit(limit int) *SelectStatement { 52 | s.limit = &limit 53 | return s 54 | } 55 | 56 | func (s *SelectStatement) Offset(offset int) *SelectStatement { 57 | s.offset = &offset 58 | return s 59 | } 60 | 61 | func (s *SelectStatement) Order(order ...string) *SelectStatement { 62 | s.order = append(s.order, order...) 63 | return s 64 | } 65 | 66 | func (s *SelectStatement) Distinct(distinct ...string) *SelectStatement { 67 | s.distinct = distinct 68 | return s 69 | } 70 | 71 | func (s *SelectStatement) GroupBy(group string) *SelectStatement { 72 | s.group = group 73 | return s 74 | } 75 | 76 | func (s *SelectStatement) Having(having string) *SelectStatement { 77 | s.having = having 78 | return s 79 | } 80 | 81 | func (s *SelectStatement) Join(join ...string) *SelectStatement { 82 | s.joins = append(s.joins, join...) 83 | return s 84 | } 85 | 86 | // convert select statement to update statement 87 | func (s *SelectStatement) Update() *UpdateStatement { 88 | if s.limit != nil { 89 | return NewUpdate().Table(s.from).Where(s.where) // TODO: convert into subquery 90 | } 91 | return NewUpdate().Table(s.from).Where(s.where) 92 | } 93 | 94 | // convert select statement to delete statement 95 | func (s *SelectStatement) Delete() *DeleteStatemnet { 96 | return NewDelete().From(s.from).Where(s.where) 97 | } 98 | 99 | func (s *SelectStatement) ToSQL() (string, []interface{}) { 100 | var query string 101 | if len(s.distinct) > 0 { 102 | query = fmt.Sprintf("SELECT DISTINCT %s FROM %s", strings.Join(s.distinct, ", "), s.from) 103 | } else { 104 | query = fmt.Sprintf("SELECT %s FROM %s", strings.Join(s.selection, ", "), s.from) 105 | } 106 | args := []interface{}{} 107 | 108 | if len(s.joins) > 0 { 109 | for i := 0; i < len(s.joins); i++ { 110 | query = fmt.Sprintf("%s %s", query, s.joins[i]) 111 | } 112 | } 113 | 114 | if s.where != nil { 115 | query = fmt.Sprintf("%s WHERE %s", query, s.where.fragment) 116 | args = append(args, s.where.args...) 117 | } 118 | 119 | if s.group != "" { 120 | query = fmt.Sprintf("%s GROUP BY %s", query, s.group) 121 | } 122 | 123 | if s.having != "" { 124 | query = fmt.Sprintf("%s HAVING %s", query, s.having) 125 | } 126 | 127 | if s.order != nil { 128 | query = fmt.Sprintf("%s ORDER BY %s", query, strings.Join(s.order, ", ")) 129 | } 130 | 131 | if s.limit != nil { 132 | query = fmt.Sprintf("%s LIMIT ?", query) 133 | args = append(args, *s.limit) 134 | } 135 | 136 | if s.offset != nil { 137 | query = fmt.Sprintf("%s OFFSET ?", query) 138 | args = append(args, *s.offset) 139 | } 140 | 141 | return query, args 142 | } 143 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/select_test.go: -------------------------------------------------------------------------------- 1 | package queryx 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestSelect(t *testing.T) { 10 | s := NewSelect().Select("users.*").From("users") 11 | sql, args := s.ToSQL() 12 | require.Equal(t, `SELECT users.* FROM users`, sql) 13 | require.Equal(t, []interface{}{}, args) 14 | 15 | sql, args = s.Update().Columns("name", "email").Values("test", "test@example.com").ToSQL() 16 | require.Equal(t, `UPDATE users SET name = ?, email = ?`, sql) 17 | require.Equal(t, []interface{}{"test", "test@example.com"}, args) 18 | 19 | s1 := s.Limit(1) 20 | sql, args = s1.ToSQL() 21 | require.Equal(t, `SELECT users.* FROM users LIMIT ?`, sql) 22 | require.Equal(t, []interface{}{1}, args) 23 | } 24 | 25 | func TestSelectWhere(t *testing.T) { 26 | s1 := NewSelect().Select("users.*").From("users").Where(NewClause("id = ?", []interface{}{1})) 27 | sql, args := s1.ToSQL() 28 | require.Equal(t, `SELECT users.* FROM users WHERE id = ?`, sql) 29 | require.Equal(t, []interface{}{1}, args) 30 | 31 | s1.Where(NewClause("name = ?", []interface{}{"test"})) 32 | sql, args = s1.ToSQL() 33 | require.Equal(t, `SELECT users.* FROM users WHERE (id = ?) AND (name = ?)`, sql) 34 | require.Equal(t, []interface{}{1, "test"}, args) 35 | } 36 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/string.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "database/sql" 7 | "database/sql/driver" 8 | "encoding/json" 9 | "fmt" 10 | ) 11 | 12 | type String struct { 13 | Val string 14 | Valid bool 15 | Set bool 16 | } 17 | 18 | func NewString(v string) String { 19 | return String{Val: v, Valid: true, Set: true} 20 | } 21 | 22 | func NewNullableString(v *string) String { 23 | if v != nil { 24 | return NewString(*v) 25 | } 26 | return String{Set: true} 27 | } 28 | 29 | // Scan implements the Scanner interface. 30 | func (s *String) Scan(value interface{}) error { 31 | ns := sql.NullString{String: s.Val} 32 | err := ns.Scan(value) 33 | s.Val, s.Valid = ns.String, ns.Valid 34 | return err 35 | } 36 | 37 | // Value implements the driver Valuer interface. 38 | func (s String) Value() (driver.Value, error) { 39 | if !s.Valid { 40 | return nil, nil 41 | } 42 | return s.Val, nil 43 | } 44 | 45 | // MarshalJSON implements the json.Marshaler interface. 46 | func (s String) MarshalJSON() ([]byte, error) { 47 | if !s.Valid { 48 | return json.Marshal(nil) 49 | } 50 | return json.Marshal(s.Val) 51 | } 52 | 53 | // UnmarshalJSON implements the json.Unmarshaler interface. 54 | func (s *String) UnmarshalJSON(data []byte) error { 55 | s.Set = true 56 | if string(data) == "null" { 57 | return nil 58 | } 59 | s.Valid = true 60 | if err := json.Unmarshal(data, &s.Val); err != nil { 61 | return err 62 | } 63 | return nil 64 | } 65 | 66 | // String implements the stringer interface. 67 | func (s String) String() string { 68 | if !s.Valid { 69 | return "null" 70 | } 71 | return fmt.Sprintf(`"%s"`, s.Val) 72 | } 73 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/string_array.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "database/sql/driver" 7 | "encoding/json" 8 | "fmt" 9 | "strings" 10 | ) 11 | 12 | type StringArray struct { 13 | Val []string 14 | Valid bool 15 | Set bool 16 | } 17 | 18 | func NewStringArray(v []string) StringArray { 19 | return StringArray{Val: v, Valid: true, Set: true} 20 | } 21 | 22 | func NewNullableStringArray(v *[]string) StringArray { 23 | if v != nil { 24 | return NewStringArray(*v) 25 | } 26 | return StringArray{Set: true} 27 | } 28 | 29 | // Scan implements the Scanner interface. 30 | func (s *StringArray) Scan(value interface{}) error { 31 | if value == nil { 32 | s.Val, s.Valid = nil, false 33 | return nil 34 | } 35 | 36 | switch v := value.(type) { 37 | case []byte: 38 | s.Valid = true 39 | return s.parseArray(string(v)) 40 | case string: 41 | s.Valid = true 42 | return s.parseArray(v) 43 | case []string: 44 | s.Val, s.Valid = v, true 45 | return nil 46 | default: 47 | return fmt.Errorf("unsupported Scan type for StringArray: %T", value) 48 | } 49 | } 50 | 51 | // parseArray parses a PostgreSQL array string into a Go string slice 52 | func (s *StringArray) parseArray(str string) error { 53 | if str == "{}" { 54 | s.Val = []string{} 55 | return nil 56 | } 57 | 58 | // Simple parsing for basic arrays 59 | // This is a simplified implementation and might not handle all edge cases 60 | str = strings.TrimPrefix(str, "{") 61 | str = strings.TrimSuffix(str, "}") 62 | 63 | // Split by comma, but respect quotes 64 | parts := []string{} 65 | inQuote := false 66 | current := "" 67 | 68 | for _, r := range str { 69 | switch r { 70 | case '"': 71 | inQuote = !inQuote 72 | current += string(r) 73 | case ',': 74 | if inQuote { 75 | current += string(r) 76 | } else { 77 | parts = append(parts, current) 78 | current = "" 79 | } 80 | default: 81 | current += string(r) 82 | } 83 | } 84 | 85 | if current != "" { 86 | parts = append(parts, current) 87 | } 88 | 89 | // Strip quotes and unescape 90 | result := make([]string, len(parts)) 91 | for i, p := range parts { 92 | p = strings.TrimSpace(p) 93 | if strings.HasPrefix(p, "\"") && strings.HasSuffix(p, "\"") { 94 | // Unquote the string 95 | p = p[1 : len(p)-1] 96 | // Unescape escaped quotes 97 | p = strings.ReplaceAll(p, "\"\"", "\"") 98 | } 99 | result[i] = p 100 | } 101 | 102 | s.Val = result 103 | return nil 104 | } 105 | 106 | // Value implements the driver Valuer interface. 107 | func (s StringArray) Value() (driver.Value, error) { 108 | if !s.Valid { 109 | return nil, nil 110 | } 111 | 112 | if s.Val == nil { 113 | return "{}", nil 114 | } 115 | 116 | // Format slice as PostgreSQL array 117 | escaped := make([]string, len(s.Val)) 118 | for i, v := range s.Val { 119 | // Escape special characters and quote 120 | v = strings.ReplaceAll(v, "\"", "\"\"") 121 | escaped[i] = fmt.Sprintf("\"%s\"", v) 122 | } 123 | 124 | return fmt.Sprintf("{%s}", strings.Join(escaped, ",")), nil 125 | } 126 | 127 | // MarshalJSON implements the json.Marshaler interface. 128 | func (s StringArray) MarshalJSON() ([]byte, error) { 129 | if !s.Valid { 130 | return json.Marshal(nil) 131 | } 132 | return json.Marshal(s.Val) 133 | } 134 | 135 | // UnmarshalJSON implements the json.Unmarshaler interface. 136 | func (s *StringArray) UnmarshalJSON(data []byte) error { 137 | s.Set = true 138 | if string(data) == "null" { 139 | return nil 140 | } 141 | s.Valid = true 142 | if err := json.Unmarshal(data, &s.Val); err != nil { 143 | return err 144 | } 145 | return nil 146 | } 147 | 148 | // String implements the stringer interface. 149 | func (s StringArray) String() string { 150 | if !s.Valid { 151 | return "null" 152 | } 153 | bytes, _ := json.Marshal(s.Val) 154 | return string(bytes) 155 | } 156 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/string_array_column.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | type StringArrayColumn struct { 6 | Name string 7 | Table *Table 8 | } 9 | 10 | func (t *Table) NewStringArrayColumn(name string) *StringArrayColumn { 11 | return &StringArrayColumn{ 12 | Table: t, 13 | Name: name, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/string_array_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "encoding/json" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestNewStringArray(t *testing.T) { 13 | // Test with a non-empty slice 14 | s1 := NewStringArray([]string{"a", "b", "c"}) 15 | require.Equal(t, []string{"a", "b", "c"}, s1.Val) 16 | require.True(t, s1.Valid) 17 | require.True(t, s1.Set) 18 | 19 | // Test with an empty slice 20 | s2 := NewStringArray([]string{}) 21 | require.Equal(t, []string{}, s2.Val) 22 | require.True(t, s2.Valid) 23 | require.True(t, s2.Set) 24 | 25 | // Test with nullable, non-nil value 26 | v := []string{"x", "y"} 27 | s3 := NewNullableStringArray(&v) 28 | require.Equal(t, []string{"x", "y"}, s3.Val) 29 | require.True(t, s3.Valid) 30 | require.True(t, s3.Set) 31 | 32 | // Test with nullable, nil value 33 | s4 := NewNullableStringArray(nil) 34 | require.False(t, s4.Valid) 35 | require.True(t, s4.Set) 36 | } 37 | 38 | func TestStringArrayJSON(t *testing.T) { 39 | type Foo struct { 40 | X StringArray `json:"x"` 41 | Y StringArray `json:"y"` 42 | } 43 | 44 | x := NewStringArray([]string{"a", "b", "c"}) 45 | y := NewNullableStringArray(nil) 46 | s := `{"x":["a","b","c"],"y":null}` 47 | 48 | f1 := Foo{X: x, Y: y} 49 | b, err := json.Marshal(f1) 50 | require.NoError(t, err) 51 | require.Equal(t, s, string(b)) 52 | 53 | var f2 Foo 54 | err = json.Unmarshal([]byte(s), &f2) 55 | require.NoError(t, err) 56 | require.Equal(t, x, f2.X) 57 | require.Equal(t, y, f2.Y) 58 | } 59 | 60 | func TestStringArrayScan(t *testing.T) { 61 | // Test scanning from nil 62 | var s1 StringArray 63 | err := s1.Scan(nil) 64 | require.NoError(t, err) 65 | require.False(t, s1.Valid) 66 | require.Nil(t, s1.Val) 67 | 68 | // Test scanning from PostgreSQL array string format 69 | var s2 StringArray 70 | err = s2.Scan(`{"a","b","c"}`) 71 | require.NoError(t, err) 72 | require.True(t, s2.Valid) 73 | require.Equal(t, []string{"a", "b", "c"}, s2.Val) 74 | 75 | // Test scanning from byte slice 76 | var s3 StringArray 77 | err = s3.Scan([]byte(`{"x","y"}`)) 78 | require.NoError(t, err) 79 | require.True(t, s3.Valid) 80 | require.Equal(t, []string{"x", "y"}, s3.Val) 81 | 82 | // Test scanning from string slice 83 | var s4 StringArray 84 | err = s4.Scan([]string{"p", "q"}) 85 | require.NoError(t, err) 86 | require.True(t, s4.Valid) 87 | require.Equal(t, []string{"p", "q"}, s4.Val) 88 | 89 | // Test scanning from empty array 90 | var s5 StringArray 91 | err = s5.Scan(`{}`) 92 | require.NoError(t, err) 93 | require.True(t, s5.Valid) 94 | require.Equal(t, []string{}, s5.Val) 95 | 96 | // Test scanning from unsupported type 97 | var s6 StringArray 98 | err = s6.Scan(123) 99 | require.Error(t, err) 100 | } 101 | 102 | func TestStringArrayValue(t *testing.T) { 103 | // Test with valid array 104 | s1 := NewStringArray([]string{"a", "b", "c"}) 105 | v1, err := s1.Value() 106 | require.NoError(t, err) 107 | require.Equal(t, `{"a","b","c"}`, v1) 108 | 109 | // Test with invalid array 110 | s2 := StringArray{Valid: false} 111 | v2, err := s2.Value() 112 | require.NoError(t, err) 113 | require.Nil(t, v2) 114 | 115 | // Test with nil slice but valid flag 116 | s3 := StringArray{Valid: true, Val: nil} 117 | v3, err := s3.Value() 118 | require.NoError(t, err) 119 | require.Equal(t, "{}", v3) 120 | 121 | // Test with values that need escaping 122 | s4 := NewStringArray([]string{`a"quote`}) 123 | v4, err := s4.Value() 124 | require.NoError(t, err) 125 | require.Equal(t, `{"a""quote"}`, v4) 126 | } 127 | 128 | func TestStringArrayString(t *testing.T) { 129 | // Test with valid array 130 | s1 := NewStringArray([]string{"a", "b", "c"}) 131 | require.Equal(t, `["a","b","c"]`, s1.String()) 132 | 133 | // Test with invalid array 134 | s2 := StringArray{Valid: false} 135 | require.Equal(t, "null", s2.String()) 136 | } 137 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/string_column.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import "fmt" 6 | 7 | type StringColumn struct { 8 | Name string 9 | Table *Table 10 | } 11 | 12 | func (t *Table) NewStringColumn(name string) *StringColumn { 13 | return &StringColumn{ 14 | Table: t, 15 | Name: name, 16 | } 17 | } 18 | 19 | func (c *StringColumn) NE(v string) *Clause { 20 | return &Clause{ 21 | fragment: fmt.Sprintf("%s.%s <> ?", c.Table.Name, c.Name), 22 | args: []interface{}{v}, 23 | } 24 | } 25 | func (c *StringColumn) EQ(v string) *Clause { 26 | return &Clause{ 27 | fragment: fmt.Sprintf("lower(%s.%s) = lower(?)", c.Table.Name, c.Name), 28 | args: []interface{}{v}, 29 | } 30 | } 31 | 32 | func (c *StringColumn) IEQ(v string) *Clause { 33 | return &Clause{ 34 | fragment: fmt.Sprintf("%s.%s = ?", c.Table.Name, c.Name), 35 | args: []interface{}{v}, 36 | } 37 | } 38 | 39 | func (c *StringColumn) In(v []string) *Clause { 40 | if len(v) == 0 { 41 | return &Clause{ 42 | fragment: "1=0", 43 | } 44 | } 45 | return &Clause{ 46 | fragment: fmt.Sprintf("%s.%s IN (?)", c.Table.Name, c.Name), 47 | args: []interface{}{v}, 48 | } 49 | } 50 | 51 | func (c *StringColumn) NIn(v []string) *Clause { 52 | if len(v) == 0 { 53 | return &Clause{ 54 | fragment: "1!=0", 55 | } 56 | } 57 | return &Clause{ 58 | fragment: fmt.Sprintf("%s.%s NOT IN (?)", c.Table.Name, c.Name), 59 | args: []interface{}{v}, 60 | } 61 | } 62 | 63 | func (c *StringColumn) Like(v string) *Clause { 64 | return &Clause{ 65 | fragment: fmt.Sprintf("%s.%s ilike ?", c.Table.Name, c.Name), 66 | args: []interface{}{fmt.Sprintf("%s%s%s", "%", v, "%")}, 67 | } 68 | } 69 | 70 | func (c *StringColumn) ILike(v string) *Clause { 71 | return &Clause{ 72 | fragment: fmt.Sprintf("%s.%s like ?", c.Table.Name, c.Name), 73 | args: []interface{}{fmt.Sprintf("%s%s%s", "%", v, "%")}, 74 | } 75 | } 76 | func (c *StringColumn) IStartsWith(v string) *Clause { 77 | return &Clause{ 78 | fragment: fmt.Sprintf("%s.%s like ?", c.Table.Name, c.Name), 79 | args: []interface{}{fmt.Sprintf("%s%s", v, "%")}, 80 | } 81 | } 82 | 83 | func (c *StringColumn) StartsWith(v string) *Clause { 84 | return &Clause{ 85 | fragment: fmt.Sprintf("%s.%s ilike ?", c.Table.Name, c.Name), 86 | args: []interface{}{fmt.Sprintf("%s%s", v, "%")}, 87 | } 88 | } 89 | 90 | func (c *StringColumn) EndsWith(v string) *Clause { 91 | return &Clause{ 92 | fragment: fmt.Sprintf("%s.%s ilike ?", c.Table.Name, c.Name), 93 | args: []interface{}{fmt.Sprintf("%s%s", "%", v)}, 94 | } 95 | } 96 | 97 | func (c *StringColumn) IEndsWith(v string) *Clause { 98 | return &Clause{ 99 | fragment: fmt.Sprintf("%s.%s like ?", c.Table.Name, c.Name), 100 | args: []interface{}{fmt.Sprintf("%s%s", "%", v)}, 101 | } 102 | } 103 | 104 | func (c *StringColumn) Asc() string { 105 | return fmt.Sprintf("%s.%s ASC", c.Table.Name, c.Name) 106 | } 107 | 108 | func (c *StringColumn) Desc() string { 109 | return fmt.Sprintf("%s.%s DESC", c.Table.Name, c.Name) 110 | } 111 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/string_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "encoding/json" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestNewString(t *testing.T) { 13 | s1 := NewString("ss") 14 | require.Equal(t, "ss", s1.Val) 15 | require.True(t, s1.Valid) 16 | 17 | s2 := NewNullableString(nil) 18 | require.False(t, s2.Valid) 19 | } 20 | 21 | func TestStringJSON(t *testing.T) { 22 | type Foo struct { 23 | X String `json:"x"` 24 | Y String `json:"y"` 25 | } 26 | x := NewString("ss") 27 | y := NewNullableString(nil) 28 | s := `{"x":"ss","y":null}` 29 | 30 | f1 := Foo{X: x, Y: y} 31 | b, err := json.Marshal(f1) 32 | require.NoError(t, err) 33 | require.Equal(t, s, string(b)) 34 | 35 | var f2 Foo 36 | err = json.Unmarshal([]byte(s), &f2) 37 | require.NoError(t, err) 38 | require.Equal(t, x, f2.X) 39 | require.Equal(t, y, f2.Y) 40 | } 41 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/table.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | type Table struct { 6 | Name string 7 | } 8 | 9 | func NewTable(name string) *Table { 10 | return &Table{ 11 | Name: name, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/time.gotmpl: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "database/sql" 7 | "database/sql/driver" 8 | "encoding/json" 9 | "time" 10 | {{ if eq $.client.Adapter "mysql" }}"fmt"{{ end }} 11 | ) 12 | 13 | type Time struct { 14 | Val time.Time 15 | Valid bool 16 | Set bool 17 | } 18 | 19 | func parseTime(s string) (*time.Time, error) { 20 | {{- if eq $.client.Adapter "mysql" }} 21 | s = fmt.Sprintf("2006-01-02 %+v", s) 22 | layout := "2006-01-02 15:04:05" 23 | {{- else if or (eq .client.Adapter "postgresql") (eq $.client.Adapter "sqlite") }} 24 | layout := "15:04:05" 25 | {{- end }} 26 | loc, err := time.LoadLocation("{{ $.client.TimeZone }}") 27 | if err != nil { 28 | return nil, err 29 | } 30 | t, err := time.ParseInLocation(layout, s, loc) 31 | if err != nil { 32 | return nil, err 33 | } 34 | return &t, nil 35 | } 36 | 37 | func NewTime(v string) Time { 38 | t, err := parseTime(v) 39 | if err != nil { 40 | return Time{Set: true} 41 | } 42 | 43 | return Time{Val: *t, Valid: true, Set: true} 44 | } 45 | 46 | func NewNullableTime(v *string) Time { 47 | if v != nil { 48 | return NewTime(*v) 49 | } 50 | return Time{Set: true} 51 | } 52 | 53 | // Scan implements the Scanner interface. 54 | func (t *Time) Scan(value interface{}) error { 55 | n := sql.NullTime{} 56 | err := n.Scan(value) 57 | if err != nil { 58 | return err 59 | } 60 | t.Val, t.Valid = n.Time, n.Valid 61 | {{- if eq .client.Adapter "mysql" }} 62 | loc, err := loadLocation() 63 | if err != nil { 64 | return err 65 | } 66 | t.Val = t.Val.In(loc) 67 | {{- end }} 68 | return nil 69 | } 70 | 71 | // Value implements the driver Valuer interface. 72 | func (t Time) Value() (driver.Value, error) { 73 | if !t.Valid { 74 | return nil, nil 75 | } 76 | {{- if eq .client.Adapter "mysql"}} 77 | return t.Val.UTC(), nil 78 | {{- else}} 79 | return t.Val, nil 80 | {{- end}} 81 | } 82 | 83 | // MarshalJSON implements the json.Marshaler interface. 84 | func (t Time) MarshalJSON() ([]byte, error) { 85 | if !t.Valid { 86 | return json.Marshal(nil) 87 | } 88 | return json.Marshal(t.Val.Format("15:04:05")) 89 | } 90 | 91 | // UnmarshalJSON implements the json.Unmarshaler interface. 92 | func (t *Time) UnmarshalJSON(data []byte) error { 93 | t.Set = true 94 | s := string(data) 95 | if s == "null" || s == "" { 96 | return nil 97 | } 98 | t.Valid = true 99 | s = s[len(`"`) : len(s)-len(`"`)] 100 | tt, err := parseTime(s) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | t.Val = *tt 106 | return nil 107 | } 108 | 109 | // String implements the stringer interface. 110 | func (t Time) String() string { 111 | if !t.Valid { 112 | return "null" 113 | } 114 | return t.Val.Format("15:04:05") 115 | } 116 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/time_column.gotmpl: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | 10 | type TimeColumn struct { 11 | Name string 12 | Table *Table 13 | } 14 | 15 | func (t *Table) NewTimeColumn(name string) *TimeColumn { 16 | return &TimeColumn{ 17 | Table: t, 18 | Name: name, 19 | } 20 | } 21 | 22 | func (c *TimeColumn) LE(v string) *Clause { 23 | t, err := parseTime(v) 24 | if err != nil { 25 | return &Clause{ 26 | err: err, 27 | } 28 | } 29 | return &Clause{ 30 | fragment: fmt.Sprintf("%s.%s <= ?", c.Table.Name, c.Name), 31 | args: []interface{}{t.UTC()}, 32 | err: err, 33 | } 34 | } 35 | 36 | func (c *TimeColumn) LT(v string) *Clause { 37 | t, err := parseTime(v) 38 | if err != nil { 39 | return &Clause{ 40 | err: err, 41 | } 42 | } 43 | return &Clause{ 44 | fragment: fmt.Sprintf("%s.%s <=?", c.Table.Name, c.Name), 45 | args: []interface{}{t.UTC()}, 46 | err: err, 47 | } 48 | } 49 | 50 | func (c *TimeColumn) GE(v string) *Clause { 51 | t, err := parseTime(v) 52 | if err != nil { 53 | return &Clause{ 54 | err: err, 55 | } 56 | } 57 | return &Clause{ 58 | fragment: fmt.Sprintf("%s.%s >= ?", c.Table.Name, c.Name), 59 | args: []interface{}{t.UTC()}, 60 | err: err, 61 | } 62 | } 63 | 64 | func (c *TimeColumn) GT(v string) *Clause { 65 | t, err := parseTime(v) 66 | if err != nil { 67 | return &Clause{ 68 | err: err, 69 | } 70 | } 71 | return &Clause{ 72 | fragment: fmt.Sprintf("%s.%s > ?", c.Table.Name, c.Name), 73 | args: []interface{}{t.UTC()}, 74 | err: err, 75 | } 76 | } 77 | 78 | func (c *TimeColumn) EQ(v string) *Clause { 79 | t, err := parseTime(v) 80 | if err != nil { 81 | return &Clause{ 82 | err: err, 83 | } 84 | } 85 | return &Clause{ 86 | fragment: fmt.Sprintf("%s.%s = ?", c.Table.Name, c.Name), 87 | args: []interface{}{t.UTC()}, 88 | err: err, 89 | } 90 | } 91 | 92 | func (c *TimeColumn) Asc() string { 93 | return fmt.Sprintf("%s.%s ASC", c.Table.Name, c.Name) 94 | } 95 | 96 | func (c *TimeColumn) Desc() string { 97 | return fmt.Sprintf("%s.%s DESC", c.Table.Name, c.Name) 98 | } 99 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/time_test.gotmpl: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "encoding/json" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestNewTime(t *testing.T) { 13 | t1 := NewTime("12:11:10") 14 | require.Equal(t, "12:11:10", t1.Val.Format("15:04:05")) 15 | require.True(t, t1.Valid) 16 | 17 | t2 := NewNullableTime(nil) 18 | require.False(t, t2.Valid) 19 | } 20 | 21 | func TestTimeJSON(t *testing.T) { 22 | type Foo struct { 23 | X Time `json:"x"` 24 | Y Time `json:"y"` 25 | } 26 | x := NewTime("12:11:10") 27 | y := NewNullableTime(nil) 28 | s := `{"x":"12:11:10","y":null}` 29 | 30 | f1 := Foo{X: x, Y: y} 31 | b, err := json.Marshal(f1) 32 | require.NoError(t, err) 33 | require.Equal(t, s, string(b)) 34 | 35 | var f2 Foo 36 | err = json.Unmarshal([]byte(s), &f2) 37 | require.NoError(t, err) 38 | require.Equal(t, x, f2.X) 39 | require.Equal(t, y, f2.Y) 40 | } 41 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/update.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "fmt" 7 | "strings" 8 | ) 9 | 10 | type UpdateStatement struct { 11 | table string 12 | columns []string 13 | values []interface{} 14 | where *Clause 15 | returning string 16 | } 17 | 18 | func NewUpdate() *UpdateStatement { 19 | return &UpdateStatement{} 20 | } 21 | 22 | func (s *UpdateStatement) Table(table string) *UpdateStatement { 23 | s.table = table 24 | return s 25 | } 26 | 27 | func (s *UpdateStatement) Columns(columns ...string) *UpdateStatement { 28 | s.columns = columns 29 | return s 30 | } 31 | 32 | func (s *UpdateStatement) Values(values ...interface{}) *UpdateStatement { 33 | s.values = values 34 | return s 35 | } 36 | 37 | func (s *UpdateStatement) Where(expr *Clause) *UpdateStatement { 38 | s.where = expr 39 | return s 40 | } 41 | 42 | func (s *UpdateStatement) Returning(returning string) *UpdateStatement { 43 | s.returning = returning 44 | return s 45 | } 46 | 47 | func (s *UpdateStatement) ToSQL() (string, []interface{}) { 48 | sql, args := fmt.Sprintf("UPDATE %s SET", s.table), s.values 49 | 50 | sets := []string{} 51 | for _, col := range s.columns { 52 | sets = append(sets, fmt.Sprintf("%s = ?", col)) 53 | } 54 | 55 | sql = fmt.Sprintf("%s %s", sql, strings.Join(sets, ", ")) 56 | 57 | if s.where != nil { 58 | sql = fmt.Sprintf("%s WHERE %s", sql, s.where.fragment) 59 | args = append(args, s.where.args...) 60 | } 61 | 62 | if s.returning != "" { 63 | sql = fmt.Sprintf("%s RETURNING %s", sql, s.returning) 64 | } 65 | 66 | return sql, args 67 | } 68 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/update_test.go: -------------------------------------------------------------------------------- 1 | package queryx 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestNewUpdate(t *testing.T) { 10 | s := NewUpdate().Table("users").Columns("name", "email").Values("test", "test@example.com") 11 | sql, args := s.ToSQL() 12 | require.Equal(t, "UPDATE users SET name = ?, email = ?", sql) 13 | require.Equal(t, []interface{}{"test", "test@example.com"}, args) 14 | } 15 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/uuid.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "database/sql" 7 | "database/sql/driver" 8 | "encoding/json" 9 | ) 10 | 11 | type UUID struct { 12 | Val string 13 | Valid bool 14 | Set bool 15 | } 16 | 17 | func NewUUID(v string) UUID { 18 | return UUID{Val: v, Valid: true, Set: true} 19 | } 20 | 21 | func NewNullableUUID(v *string) UUID { 22 | if v != nil { 23 | return NewUUID(*v) 24 | } 25 | return UUID{Set: true} 26 | } 27 | 28 | // Scan implements the Scanner interface. 29 | func (u *UUID) Scan(value interface{}) error { 30 | ns := sql.NullString{String: u.Val} 31 | err := ns.Scan(value) 32 | u.Val, u.Valid = ns.String, ns.Valid 33 | return err 34 | } 35 | 36 | // Value implements the driver Valuer interface. 37 | func (u UUID) Value() (driver.Value, error) { 38 | if !u.Valid { 39 | return nil, nil 40 | } 41 | return u.Val, nil 42 | } 43 | 44 | // MarshalJSON implements the json.Marshaler interface. 45 | func (u UUID) MarshalJSON() ([]byte, error) { 46 | if !u.Valid { 47 | return json.Marshal(nil) 48 | } 49 | return json.Marshal(u.Val) 50 | } 51 | 52 | // UnmarshalJSON implements the json.Unmarshaler interface. 53 | func (u *UUID) UnmarshalJSON(data []byte) error { 54 | u.Set = true 55 | if string(data) == "null" { 56 | return nil 57 | } 58 | u.Valid = true 59 | if err := json.Unmarshal(data, &u.Val); err != nil { 60 | return err 61 | } 62 | return nil 63 | } 64 | 65 | // String implements the stringer interface. 66 | func (u UUID) String() string { 67 | if !u.Valid { 68 | return "null" 69 | } 70 | return u.Val 71 | } 72 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/uuid_column.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import "fmt" 6 | 7 | type UUIDColumn struct { 8 | Name string 9 | Table *Table 10 | } 11 | 12 | func (t *Table) NewUUIDColumn(name string) *UUIDColumn { 13 | return &UUIDColumn{ 14 | Table: t, 15 | Name: name, 16 | } 17 | } 18 | 19 | func (c *UUIDColumn) NE(v string) *Clause { 20 | return &Clause{ 21 | fragment: fmt.Sprintf("%s.%s <> ?", c.Table.Name, c.Name), 22 | args: []interface{}{v}, 23 | } 24 | } 25 | func (c *UUIDColumn) EQ(v string) *Clause { 26 | return &Clause{ 27 | fragment: fmt.Sprintf("%s.%s = ?", c.Table.Name, c.Name), 28 | args: []interface{}{v}, 29 | } 30 | } 31 | 32 | func (c *UUIDColumn) Asc() string { 33 | return fmt.Sprintf("%s.%s ASC", c.Table.Name, c.Name) 34 | } 35 | 36 | func (c *UUIDColumn) Desc() string { 37 | return fmt.Sprintf("%s.%s DESC", c.Table.Name, c.Name) 38 | } 39 | -------------------------------------------------------------------------------- /generator/client/golang/templates/queryx/uuid_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | package queryx 4 | 5 | import ( 6 | "encoding/json" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestNewUUID(t *testing.T) { 13 | u1 := NewUUID("a81e44c5-7e18-4dfe-b9b3-d9280629d2ef") 14 | require.Equal(t, "a81e44c5-7e18-4dfe-b9b3-d9280629d2ef", u1.Val) 15 | require.True(t, u1.Valid) 16 | 17 | u2 := NewNullableUUID(nil) 18 | require.False(t, u2.Valid) 19 | } 20 | 21 | func TestUUIDJSON(t *testing.T) { 22 | type Foo struct { 23 | X UUID `json:"x"` 24 | Y UUID `json:"y"` 25 | } 26 | x := NewUUID("a81e44c5-7e18-4dfe-b9b3-d9280629d2ef") 27 | y := NewNullableUUID(nil) 28 | s := `{"x":"a81e44c5-7e18-4dfe-b9b3-d9280629d2ef","y":null}` 29 | 30 | f1 := Foo{X: x, Y: y} 31 | b, err := json.Marshal(f1) 32 | require.NoError(t, err) 33 | require.Equal(t, s, string(b)) 34 | 35 | var f2 Foo 36 | err = json.Unmarshal([]byte(s), &f2) 37 | require.NoError(t, err) 38 | require.Equal(t, x, f2.X) 39 | require.Equal(t, y, f2.Y) 40 | } 41 | -------------------------------------------------------------------------------- /generator/client/typescript/generator.go: -------------------------------------------------------------------------------- 1 | package typescript 2 | 3 | import ( 4 | "embed" 5 | 6 | "github.com/swiftcarrot/queryx/generator" 7 | "github.com/swiftcarrot/queryx/schema" 8 | ) 9 | 10 | //go:embed templates 11 | var templates embed.FS 12 | 13 | func Run(g *generator.Generator, generatorConfig *schema.Generator, args []string) error { 14 | database := g.Schema.Databases[0] 15 | 16 | if err := g.LoadTemplates(templates, database.Adapter); err != nil { 17 | return err 18 | } 19 | 20 | if err := g.Generate(transform, ""); err != nil { 21 | return err 22 | } 23 | 24 | return nil 25 | } 26 | 27 | func transform(b []byte) []byte { 28 | return b 29 | } 30 | -------------------------------------------------------------------------------- /generator/client/typescript/templates/[model]/[model]_change.tstmpl: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | import { adapterValue } from "../queryx"; 4 | 5 | export interface {{ $.model.Name }}ChangeInput { 6 | {{- range $c := .model.Columns }} 7 | {{ $c.Name | camel }}?: {{ tsChangeSetType $c.Type $c.Null $c.Array }}{{ if $c.Null}} | null{{ end }}; 8 | {{- end }} 9 | } 10 | 11 | export class {{ $.model.Name }}Change { 12 | {{- range $c := .model.Columns }} 13 | public {{ $c.Name | camel }}?: {{ tsType $c.Type $c.Null $c.Array }}{{ if $c.Null}} | null{{ end }}; 14 | {{- end }} 15 | 16 | constructor(input?: {{ $.model.Name }}ChangeInput) { 17 | {{- range $c := .model.Columns }} 18 | if (input?.{{ $c.Name | camel }} !== undefined) { 19 | {{- if eq $c.Type "date" }} 20 | this.{{ $c.Name | camel }} = new Date(input.{{ $c.Name | camel }}); 21 | {{- else if eq $c.Type "datetime" }} 22 | this.{{ $c.Name | camel }} = new Date(input.{{ $c.Name | camel }}); 23 | {{- else }} 24 | this.{{ $c.Name | camel }} = input.{{ $c.Name | camel }}; 25 | {{- end }} 26 | } 27 | {{- end }} 28 | } 29 | 30 | {{- range $c := .model.Columns }} 31 | 32 | set{{ $c.Name | pascal }}({{ $c.Name | camel}}: {{ tsChangeSetType $c.Type $c.Null $c.Array}}) { 33 | {{- if eq $c.Type "date" }} 34 | this.{{ $c.Name | camel }} = new Date({{ $c.Name | camel}}); 35 | {{- else if eq $c.Type "datetime" }} 36 | this.{{ $c.Name | camel }} = new Date({{ $c.Name | camel}}); 37 | {{- else }} 38 | this.{{ $c.Name | camel }} = {{ $c.Name | camel}}; 39 | {{- end }} 40 | 41 | return this; 42 | } 43 | {{- end }} 44 | 45 | changes() { 46 | let columns: string[] = []; 47 | let values: any[] = []; 48 | 49 | {{- range $c := .model.Columns }} 50 | if (this.{{ $c.Name | camel }} !== undefined) { 51 | columns.push("{{ $c.Name }}"); 52 | values.push(adapterValue("{{ $c.Type }}", this.{{ $c.Name | camel }})); 53 | } 54 | {{- end }} 55 | 56 | return [columns, values]; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /generator/client/typescript/templates/[model]/index.tstmpl: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | export * from "./{{$.model.Name | snake}}"; 4 | export * from "./{{$.model.Name | snake}}_query"; 5 | export * from "./{{$.model.Name | snake}}_change"; 6 | -------------------------------------------------------------------------------- /generator/client/typescript/templates/index.tstmpl: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | export * from "./queryx"; 4 | {{- range $m := $.client.Models }} 5 | export * from "./{{$m.Name | snake}}"; 6 | {{- end }} 7 | -------------------------------------------------------------------------------- /generator/client/typescript/templates/queryx/adapter.mysql.ts: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | import mysql, { ResultSetHeader, RowDataPacket } from "mysql2/promise"; 4 | import { parse } from "date-fns"; 5 | import { Config } from "./config"; 6 | 7 | export class Adapter { 8 | public config: Config; 9 | public db: mysql.Pool; 10 | 11 | constructor(config: Config) { 12 | this.config = config; 13 | } 14 | 15 | connect() { 16 | const pool = mysql.createPool({ 17 | uri: this.config.url, 18 | }); 19 | this.db = pool; 20 | } 21 | 22 | newConnection() { 23 | return this.db.getConnection(); 24 | } 25 | 26 | release() { 27 | this.db.release(); 28 | } 29 | 30 | async query(query: string, ...args: any[]) { 31 | let [rows] = await this.db.query(query, args); 32 | return rows; 33 | } 34 | 35 | async queryOne(query: string, ...args: any[]) { 36 | let [rows] = await this.db.query(query, args); 37 | return rows[0] || null; 38 | } 39 | 40 | async exec(query: string, ...args: any[]) { 41 | let [res] = await this.db.execute(query, args); 42 | return res.affectedRows; 43 | } 44 | 45 | async _exec(query: string, ...args: any[]) { 46 | let [res] = await this.db.execute(query, args); 47 | return res; 48 | } 49 | 50 | async beginTx() { 51 | await this.db.query("START TRANSACTION"); 52 | } 53 | 54 | async commit() { 55 | await this.db.query("COMMIT"); 56 | } 57 | 58 | async rollback() { 59 | await this.db.query("ROLLBACK"); 60 | } 61 | } 62 | 63 | export function adapterValue(type: string, value: any) { 64 | switch (type) { 65 | case "time": 66 | return parse(value, "HH:mm:ss", new Date()); 67 | default: 68 | return value; 69 | } 70 | } 71 | 72 | export function adapterScan(type: string, value: any) { 73 | switch (type) { 74 | case "boolean": 75 | return value === 1; 76 | default: 77 | return value; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /generator/client/typescript/templates/queryx/adapter.postgresql.ts: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | import pg from "pg"; 4 | import { parse } from "date-fns"; 5 | import { Config } from "./config"; 6 | 7 | pg.types.setTypeParser(pg.types.builtins.INT8, (val) => parseInt(val, 10)); 8 | 9 | export class Adapter { 10 | public config: Config; 11 | public db?: pg.Pool; 12 | 13 | constructor(config: Config) { 14 | this.config = config; 15 | } 16 | 17 | connect() { 18 | const pool = new pg.Pool({ 19 | connectionString: this.config.url, 20 | }); 21 | this.db = pool; 22 | } 23 | 24 | newConnection() { 25 | return this.db.connect(); 26 | } 27 | 28 | release() { 29 | this.db.release(); 30 | } 31 | 32 | private _query( 33 | query: string, 34 | args?: I, 35 | ) { 36 | let [query1, args1] = rebind(query, args); 37 | return this.db.query(query1, args1); 38 | } 39 | 40 | async query( 41 | query: string, 42 | ...args: any[] 43 | ) { 44 | const res = await this._query(query, args); 45 | return res.rows; 46 | } 47 | 48 | async queryOne( 49 | query: string, 50 | ...args: any[] 51 | ) { 52 | const res = await this._query(query, args); 53 | return res.rows[0]; 54 | } 55 | 56 | async exec(query: string, ...args: any[]) { 57 | const res = await this._query(query, args); 58 | return res.rowCount; 59 | } 60 | 61 | async beginTx() { 62 | await this.exec("BEGIN"); 63 | } 64 | 65 | async commit() { 66 | await this.exec("COMMIT"); 67 | } 68 | 69 | async rollback() { 70 | await this.exec("ROLLBACK"); 71 | } 72 | } 73 | 74 | export function rebind(query: string, args?: T) { 75 | let str = ""; 76 | let i = 0; 77 | let j = 1; 78 | let k = 0; 79 | let args1: any[] = []; 80 | 81 | while (i !== -1) { 82 | i = query.indexOf("?"); 83 | str += query.substring(0, i); 84 | 85 | if (args.length > k) { 86 | const arg = args[k]; 87 | if (Array.isArray(arg)) { 88 | args1 = args1.concat(arg); 89 | str += arg.map((_, i) => "$" + (j + i)).join(", "); 90 | j += arg.length; 91 | } else { 92 | args1.push(arg); 93 | str += "$" + j; 94 | j++; 95 | } 96 | k++; 97 | } 98 | 99 | query = query.substring(i + 1); 100 | } 101 | 102 | return [str + query, args1]; 103 | } 104 | 105 | export function adapterValue(type: string, value: any) { 106 | return value; 107 | } 108 | 109 | export function adapterScan(type: string, value: any) { 110 | switch (type) { 111 | case "time": 112 | return parse(value, "HH:mm:ss", new Date()); 113 | default: 114 | return value; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /generator/client/typescript/templates/queryx/adapter.sqlite.ts: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | import Database from "better-sqlite3"; 4 | import { format, parse } from "date-fns"; 5 | import { Config } from "./config"; 6 | 7 | export class Adapter { 8 | public config: Config; 9 | public db: Database; 10 | 11 | constructor(config: Config) { 12 | this.config = config; 13 | } 14 | 15 | connect() { 16 | this.db = new Database(this.config.url); 17 | } 18 | 19 | newConnection() { 20 | return new Database(this.config.url); 21 | } 22 | 23 | release() {} 24 | 25 | query(query: string, ...args: any[]): R[] { 26 | let [query1, args1] = rebind(query, args); 27 | let stmt = this.db.prepare(query1); 28 | return stmt.all(...args1) as R[]; 29 | } 30 | 31 | queryOne(query: string, ...args: any[]): R { 32 | let [query1, args1] = rebind(query, args); 33 | let stmt = this.db.prepare(query1); 34 | return stmt.get(...args1) as R; 35 | } 36 | 37 | async exec(query: string, ...args: any[]) { 38 | let [query1, args1] = rebind(query, args); 39 | let stmt = this.db.prepare(query1); 40 | let res = stmt.run(...args1); 41 | return res.changes; 42 | } 43 | 44 | async beginTx() { 45 | await this.exec("BEGIN"); 46 | } 47 | 48 | async commit() { 49 | await this.exec("COMMIT"); 50 | } 51 | 52 | async rollback() { 53 | await this.exec("ROLLBACK"); 54 | } 55 | } 56 | 57 | export function rebind(query: string, args?: T) { 58 | let str = ""; 59 | let i = 0; 60 | let j = 1; 61 | let k = 0; 62 | let args1: any[] = []; 63 | 64 | while (i !== -1) { 65 | i = query.indexOf("?"); 66 | str += query.substring(0, i); 67 | 68 | if (args.length > k) { 69 | const arg = args[k]; 70 | if (Array.isArray(arg)) { 71 | args1 = args1.concat(arg); 72 | str += arg.map((_, i) => "?").join(", "); 73 | j += arg.length; 74 | } else { 75 | args1.push(arg); 76 | str += "?"; 77 | j++; 78 | } 79 | k++; 80 | } 81 | 82 | query = query.substring(i + 1); 83 | } 84 | 85 | return [str + query, args1]; 86 | } 87 | 88 | // convert into sqlite type 89 | export function adapterValue(type: string, value: any) { 90 | if (typeof value === "object") { 91 | switch (type) { 92 | case "time": 93 | return format(value, "HH:mm:ss"); 94 | case "date": 95 | return format(value, "yyyy-MM-dd"); 96 | case "datetime": 97 | return format(value, "yyyy-MM-dd HH:mm:ss"); 98 | case "json": 99 | case "jsonb": 100 | return JSON.stringify(value); 101 | default: 102 | return value; 103 | } 104 | } 105 | 106 | switch (type) { 107 | case "boolean": 108 | return value ? 1 : 0; 109 | } 110 | 111 | return value; 112 | } 113 | 114 | // convert into queryx type 115 | export function adapterScan(type: string, value: any) { 116 | switch (type) { 117 | case "time": 118 | return parse(value, "HH:mm:ss", new Date()); 119 | case "date": 120 | return parse(value, "yyyy-MM-dd", new Date()); 121 | case "datetime": 122 | return parse(value, "yyyy-MM-dd HH:mm:ss", new Date()); 123 | case "json": 124 | case "jsonb": 125 | return JSON.parse(value); 126 | case "boolean": 127 | return value === 1; 128 | default: 129 | return value; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /generator/client/typescript/templates/queryx/clause.test.ts: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | import { test, expect } from "vitest"; 4 | import { Clause } from "./clause"; 5 | 6 | test("clauseAndOr", () => { 7 | let c1 = new Clause("a = ?", [1]); 8 | let c2 = new Clause("b = ?", ["x"]); 9 | 10 | let c3 = c1.and(c2); 11 | expect(c3.fragment).toEqual("(a = ?) AND (b = ?)"); 12 | expect(c3.args).toEqual([1, "x"]); 13 | 14 | let c4 = c1.or(c2); 15 | expect(c4.fragment).toEqual("(a = ?) OR (b = ?)"); 16 | expect(c4.args).toEqual([1, "x"]); 17 | 18 | let c5 = c1.and(c1.or(c2)); 19 | expect(c5.fragment).toEqual("(a = ?) AND ((a = ?) OR (b = ?))"); 20 | expect(c5.args).toEqual([1, 1, "x"]); 21 | 22 | expect(c1.fragment).toEqual("a = ?"); 23 | expect(c1.args).toEqual([1]); 24 | expect(c2.fragment).toEqual("b = ?"); 25 | expect(c2.args).toEqual(["x"]); 26 | }); 27 | -------------------------------------------------------------------------------- /generator/client/typescript/templates/queryx/clause.ts: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | export class Clause { 4 | fragment: string; 5 | args: any[]; 6 | 7 | constructor(fragment: string, args: any[]) { 8 | this.fragment = fragment; 9 | this.args = args; 10 | } 11 | 12 | and(...clauses: Clause[]) { 13 | if (clauses.length === 0) { 14 | return this; 15 | } 16 | 17 | let fragment = this.fragment; 18 | let args = this.args; 19 | 20 | for (let clause of clauses) { 21 | fragment = `(${this.fragment}) AND (${clause.fragment})`; 22 | args = this.args.concat(clause.args); 23 | } 24 | return new Clause(fragment, args); 25 | } 26 | 27 | or(...clauses: Clause[]) { 28 | if (clauses.length === 0) { 29 | return this; 30 | } 31 | 32 | let fragment = this.fragment; 33 | let args = this.args; 34 | 35 | for (let clause of clauses) { 36 | fragment = `(${this.fragment}) OR (${clause.fragment})`; 37 | args = this.args.concat(clause.args); 38 | } 39 | return new Clause(fragment, args); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /generator/client/typescript/templates/queryx/client.tstmpl: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | import { Env, Config, newConfig, getenv } from "./config"; 4 | {{- range $m := $.client.Models }} 5 | import { {{ $m.Name }}Query } from "../{{ $m.Name | snake }}"; 6 | {{- end }} 7 | import { 8 | Table, 9 | BigIntColumn, 10 | IntegerColumn, 11 | FloatColumn, 12 | BooleanColumn, 13 | StringColumn, 14 | TextColumn, 15 | DateColumn, 16 | TimeColumn, 17 | DatetimeColumn, 18 | UUIDColumn, 19 | JSONColumn, 20 | } from "./table"; 21 | import { Adapter } from "./adapter"; 22 | import { Clause } from "./clause"; 23 | 24 | export class QXClient { 25 | public config: Config; 26 | public adapter: Adapter; 27 | {{- range $m := $.client.Models }} 28 | public {{ $m.Name | camel }}: Table; 29 | {{- range $c := .Columns }} 30 | public {{ $m.Name | camel }}{{ $c.Name | pascal }}: {{ goType .Type .Null .Array }}Column; 31 | {{- end}} 32 | {{- end}} 33 | 34 | constructor(config: Config) { 35 | this.config = config; 36 | this.adapter = new Adapter(config); 37 | {{- range $m := $.client.Models }} 38 | this.{{ $m.Name | camel }} = new Table("{{ $m.TableName }}"); 39 | {{- range $c := $m.Columns }} 40 | this.{{ $m.Name | camel }}{{ $c.Name | pascal }} = this.{{ $m.Name | camel }}.new{{ goType .Type .Null .Array }}Column("{{ $c.Name }}"); 41 | {{- end}} 42 | {{- end}} 43 | } 44 | 45 | query(query: string, ...args: any[]) { 46 | return this.adapter.query(query, ...args); 47 | } 48 | 49 | queryOne(query: string, ...args: any[]) { 50 | return this.adapter.queryOne(query, ...args); 51 | } 52 | 53 | exec(query: string, ...args: any[]) { 54 | return this.adapter.exec(query, ...args); 55 | } 56 | 57 | {{- range $m := $.client.Models }} 58 | 59 | query{{ $m.Name }}() { 60 | return new {{ $m.Name }}Query(this); 61 | } 62 | {{- end }} 63 | 64 | raw(fragment: string, ...args: any[]) { 65 | return new Clause(fragment, args); 66 | } 67 | 68 | async tx() { 69 | const tx = new Tx(this.config); 70 | tx.adapter.db = await this.adapter.newConnection(); 71 | await tx.adapter.beginTx(); 72 | return tx; 73 | } 74 | 75 | async transaction(fn: (t: Tx) => Promise) { 76 | const tx = await this.tx(); 77 | try { 78 | await fn(tx); 79 | await tx.commit(); 80 | } catch (err) { 81 | await tx.rollback(); 82 | throw err; 83 | } 84 | } 85 | 86 | and(...clauses: Clause[]) { 87 | return clauses[0].and(...clauses.slice(1)); 88 | } 89 | 90 | or(...clauses: Clause[]) { 91 | return clauses[0].or(...clauses.slice(1)); 92 | } 93 | } 94 | 95 | export class Tx extends QXClient { 96 | constructor(config) { 97 | super(config); 98 | } 99 | 100 | async commit() { 101 | const res = this.adapter.commit(); 102 | this.adapter.release(); 103 | return res; 104 | } 105 | 106 | async rollback() { 107 | const res = await this.adapter.rollback(); 108 | this.adapter.release(); 109 | return res; 110 | } 111 | } 112 | 113 | export const newClient = () => { 114 | let env = getenv("QUERYX_ENV") || "development"; 115 | return newClientWithEnv(env); 116 | }; 117 | 118 | export const newClientWithEnv = (env: Env) => { 119 | let config = newConfig(env); 120 | let client = new QXClient(config); 121 | client.adapter.connect(); 122 | return client; 123 | }; 124 | -------------------------------------------------------------------------------- /generator/client/typescript/templates/queryx/config.tstmpl: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | export type Env = {{ range $i, $c := $.client.Configs }}{{ if $i }} | {{ end }}"{{ $c.Environment }}"{{ end }}; 4 | 5 | export class Config { 6 | url: string; 7 | 8 | constructor(url: string) { 9 | this.url = fixURL(url); 10 | } 11 | } 12 | 13 | export const getenv = (key: string) => { 14 | return process.env[key]; 15 | } 16 | 17 | export const newConfig = (env: Env) => { 18 | switch (env) { 19 | {{- range $c := $.client.Configs }} 20 | case "{{ $c.Environment }}": 21 | return new Config({{ if $c.URL.EnvKey }}getenv("{{ $c.URL.EnvKey }}"){{ else }}"{{ $c.URL.Value }}"{{ end }}); 22 | {{- end }} 23 | } 24 | }; 25 | 26 | {{- if eq $.client.Adapter "postgresql" }} 27 | function fixURL(rawURL: string) { 28 | return rawURL; 29 | } 30 | {{- else if eq $.client.Adapter "mysql" }} 31 | function fixURL(rawURL: string) { 32 | return rawURL; 33 | } 34 | {{- else if eq $.client.Adapter "sqlite" }} 35 | function fixURL(rawURL: string) { 36 | return rawURL.replace(/^sqlite:/, ""); 37 | } 38 | {{- end }} 39 | -------------------------------------------------------------------------------- /generator/client/typescript/templates/queryx/delete.ts: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | import { Clause } from "./clause"; 4 | 5 | export function newDelete(from: string) { 6 | return new DeleteStatemnet(from); 7 | } 8 | 9 | export class DeleteStatemnet { 10 | private _from: string; 11 | private _where?: Clause; 12 | 13 | constructor(from: string) { 14 | this._from = from; 15 | } 16 | 17 | where(expr: Clause) { 18 | this._where = expr; 19 | return this; 20 | } 21 | 22 | toSQL(): [string, any[]] { 23 | let sql = ""; 24 | let args: any[] = []; 25 | 26 | sql = `DELETE FROM ${this._from}`; 27 | 28 | if (this._where !== undefined) { 29 | sql = `${sql} WHERE ${this._where.fragment}`; 30 | args = args.concat(this._where.args); 31 | } 32 | 33 | return [sql, args]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /generator/client/typescript/templates/queryx/index.ts: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | export * from "./client"; 4 | export * from "./select"; 5 | export * from "./insert"; 6 | export * from "./update"; 7 | export * from "./delete"; 8 | export * from "./table"; 9 | export * from "./clause"; 10 | export * from "./adapter"; 11 | -------------------------------------------------------------------------------- /generator/client/typescript/templates/queryx/insert.tstmpl: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | export const newInsert = () => { 4 | return new InsertStatement(); 5 | }; 6 | 7 | export class InsertStatement { 8 | private _into: string; 9 | private _columns: string[]; 10 | private _values: any[][]; 11 | private _returning: string[]; 12 | private _onConflict: string; 13 | 14 | constructor() { 15 | // TODO: fix this 16 | this._into = ""; 17 | this._columns = []; 18 | this._values = []; 19 | this._returning = []; 20 | this._onConflict = ""; 21 | } 22 | 23 | into(into: string) { 24 | this._into = into; 25 | return this; 26 | } 27 | 28 | columns(...columns: string[]) { 29 | this._columns = columns; 30 | return this; 31 | } 32 | 33 | values(...values: any[]) { 34 | if (values.length > 0) { 35 | this._values.push(values); 36 | } 37 | return this; 38 | } 39 | {{- if or (eq $.client.Adapter "postgresql") (eq $.client.Adapter "sqlite") }} 40 | returning(...returning: string[]) { 41 | this._returning = returning; 42 | return this; 43 | } 44 | {{- end }} 45 | 46 | onConflict(onConflict: string) { 47 | this._onConflict = onConflict; 48 | return this; 49 | } 50 | 51 | toSQL(): [string, any[]] { 52 | let sql: string = `INSERT INTO ${this._into}`; 53 | 54 | if (this._columns.length > 0) { 55 | {{- if eq $.client.Adapter "mysql" }} 56 | sql = `${sql} (\`${this._columns.join("`, `")}\`)`; 57 | {{- else }} 58 | sql = `${sql} (${this._columns.join(", ")})`; 59 | {{- end }} 60 | } else { 61 | {{- if eq $.client.Adapter "mysql" }} 62 | sql = `${sql} VALUES ()`; 63 | {{- else }} 64 | sql = `${sql} DEFAULT VALUES`; 65 | {{- end }} 66 | } 67 | 68 | const values: string[] = []; 69 | for (const v of this._values) { 70 | const ss: string[] = []; 71 | for (let i = 0; i < v.length; i++) { 72 | ss.push("?"); 73 | } 74 | values.push(`(${ss.join(", ")})`); 75 | } 76 | if (values.length > 0) { 77 | sql = `${sql} VALUES ${values.join(", ")}`; 78 | } 79 | 80 | if (this._returning.length > 0) { 81 | sql = `${sql} RETURNING ${this._returning.join(", ")}`; 82 | } 83 | 84 | if (this._onConflict !== "") { 85 | sql = `${sql} ${this._onConflict}`; 86 | } 87 | 88 | const args: any[] = []; 89 | for (const v of this._values) { 90 | args.push(...v); 91 | } 92 | 93 | return [sql, args]; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /generator/client/typescript/templates/queryx/select.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "vitest"; 2 | import { newSelect } from "./select"; 3 | import { Clause } from "./clause"; 4 | 5 | test("select", () => { 6 | let s = newSelect().select("users.*").from("users"); 7 | expect(s.toSQL()).toEqual(["SELECT users.* FROM users", []]); 8 | }); 9 | 10 | test("select where", () => { 11 | let s1 = newSelect() 12 | .select("users.*") 13 | .from("users") 14 | .where(new Clause("id = ?", [1])); 15 | expect(s1.toSQL()).toEqual(["SELECT users.* FROM users WHERE id = ?", [1]]); 16 | 17 | s1.where(new Clause("name = ?", ["test"])); 18 | expect(s1.toSQL()).toEqual([ 19 | "SELECT users.* FROM users WHERE (id = ?) AND (name = ?)", 20 | [1, "test"], 21 | ]); 22 | }); 23 | -------------------------------------------------------------------------------- /generator/client/typescript/templates/queryx/select.ts: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | import type { DeleteStatemnet } from "./delete"; 4 | import { Clause } from "./clause"; 5 | import { newDelete } from "./delete"; 6 | import { newUpdate } from "./update"; 7 | 8 | export const newSelect = () => { 9 | return new SelectStatement(); 10 | }; 11 | 12 | export class SelectStatement { 13 | private _selection?: string[]; 14 | private _from?: string; 15 | private _where?: Clause; 16 | private _limit?: number; 17 | private _offset?: number; 18 | private _order: string[] = []; 19 | private _group?: string; 20 | private _having?: string; 21 | private _joins: string[] = []; 22 | 23 | select(...selection: string[]) { 24 | this._selection = selection; 25 | return this; 26 | } 27 | 28 | from(from: string) { 29 | this._from = from; 30 | return this; 31 | } 32 | 33 | where(...clauses: Clause[]) { 34 | if (clauses.length === 0) { 35 | return this; 36 | } 37 | 38 | if (this._where === undefined) { 39 | this._where = clauses[0].and(...clauses.slice(1)); 40 | } else { 41 | this._where = this._where.and(...clauses); 42 | } 43 | 44 | return this; 45 | } 46 | 47 | limit(limit: number) { 48 | this._limit = limit; 49 | return this; 50 | } 51 | 52 | offset(offset: number) { 53 | this._offset = offset; 54 | return this; 55 | } 56 | 57 | order(...order: string[]) { 58 | this._order = [...this._order, ...order]; 59 | return this; 60 | } 61 | 62 | groupBy(group: string) { 63 | this._group = group; 64 | return this; 65 | } 66 | 67 | having(having: string) { 68 | this._having = having; 69 | return this; 70 | } 71 | 72 | join(...join: string[]) { 73 | this._joins = [...this._joins, ...join]; 74 | } 75 | 76 | // convert select statement to update statement 77 | update() { 78 | let s; 79 | if (this._limit != undefined) { 80 | // TODO: convert into subquery if limit 81 | s = newUpdate(this._from); 82 | } else { 83 | s = newUpdate(this._from); 84 | } 85 | 86 | if (this._where) { 87 | s.where(this._where); 88 | } 89 | 90 | return s; 91 | } 92 | 93 | // convert select statement to delete statement 94 | delete() { 95 | let s: DeleteStatemnet = null; 96 | 97 | if (this._from !== undefined) { 98 | s = newDelete(this._from); 99 | } 100 | 101 | if (this._where !== undefined) { 102 | s.where(this._where); 103 | } 104 | 105 | return s; 106 | } 107 | 108 | toSQL(): [string, any[]] { 109 | let query = "SELECT"; 110 | let args = []; 111 | 112 | if (this._selection != undefined && this._selection.length > 0) { 113 | query += ` ${this._selection.join(", ")}`; 114 | } 115 | 116 | if (this._from !== undefined) { 117 | query += ` FROM ${this._from}`; 118 | } 119 | 120 | if (this._joins.length > 0) { 121 | for (let i = 0; i < this._joins.length; i++) { 122 | query += ` ${this._joins[i]}`; 123 | } 124 | } 125 | 126 | if (this._where !== undefined) { 127 | query += ` WHERE ${this._where.fragment}`; 128 | args.push(...this._where.args); 129 | } 130 | 131 | if (this._order.length > 0) { 132 | query += ` ORDER BY ${this._order.join(", ")}`; 133 | } 134 | 135 | if (this._limit !== undefined) { 136 | query += " LIMIT ?"; 137 | args.push(this._limit); 138 | } 139 | 140 | if (this._offset !== undefined) { 141 | query += " OFFSET ?"; 142 | args.push(this._offset); 143 | } 144 | 145 | if (this._group !== undefined) { 146 | query += ` GROUP BY ${this._group}`; 147 | } 148 | 149 | if (this._having !== undefined) { 150 | query += ` HAVING ${this._having}`; 151 | } 152 | 153 | return [query, args]; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /generator/client/typescript/templates/queryx/update.ts: -------------------------------------------------------------------------------- 1 | // Code generated by queryx, DO NOT EDIT. 2 | 3 | import { Clause } from "./Clause"; 4 | 5 | export const newUpdate = (table: string) => { 6 | return new UpdateStatement(table); 7 | }; 8 | 9 | export class UpdateStatement { 10 | private _table: string; 11 | private _columns?: string[]; 12 | private _values?: any[]; 13 | private _where?: Clause; 14 | private _returning?: string[]; 15 | 16 | constructor(table: string) { 17 | this._table = table; 18 | } 19 | 20 | columns(...columns: string[]) { 21 | this._columns = columns; 22 | return this; 23 | } 24 | 25 | values(...values: any[]) { 26 | this._values = values; 27 | return this; 28 | } 29 | 30 | where(expr: Clause) { 31 | this._where = expr; 32 | return this; 33 | } 34 | 35 | returning(...returning: string[]) { 36 | this._returning = returning; 37 | return this; 38 | } 39 | 40 | toSQL(): [string, any[]] { 41 | let sql = `UPDATE ${this._table} SET`; 42 | let args: any[] = []; 43 | 44 | if (this._values !== undefined) { 45 | args = args.concat(this._values); 46 | } 47 | 48 | let sets = []; 49 | if (this._columns !== undefined) { 50 | for (let col of this._columns) { 51 | sets.push(`${col} = ?`); 52 | } 53 | } 54 | 55 | sql = `${sql} ${sets.join(", ")}`; 56 | 57 | if (this._where !== undefined) { 58 | sql = `${sql} WHERE ${this._where.fragment}`; 59 | args = args.concat(this._where.args); 60 | } 61 | 62 | if (this._returning !== undefined) { 63 | sql = `${sql} RETURNING ${this._returning.join(", ")}`; 64 | } 65 | 66 | return [sql, args]; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/swiftcarrot/queryx 2 | 3 | go 1.16 4 | 5 | require ( 6 | ariga.io/atlas v0.14.2 7 | github.com/agext/levenshtein v1.2.3 // indirect 8 | github.com/go-openapi/inflect v0.19.0 9 | github.com/go-sql-driver/mysql v1.6.0 10 | github.com/google/go-cmp v0.5.8 // indirect 11 | github.com/hashicorp/hcl/v2 v2.13.0 12 | github.com/lib/pq v1.10.5 13 | github.com/mattn/go-sqlite3 v1.14.17 14 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 15 | github.com/spf13/cobra v1.4.0 16 | github.com/stretchr/testify v1.8.1 17 | github.com/zclconf/go-cty v1.10.0 18 | golang.org/x/mod v0.8.0 19 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /inflect/golang.go: -------------------------------------------------------------------------------- 1 | package inflect 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | ) 8 | 9 | // type abbreviation used for a method's receiver variable 10 | // https://github.com/golang/go/wiki/CodeReviewComments#receiver-names 11 | func GoReceiver(typ string) string { 12 | return strings.ToLower(typ[0:1]) 13 | } 14 | 15 | // avoid go keyword with syntax error 16 | func goKeywordFix(s string) string { 17 | switch s { 18 | case "type": 19 | return "typ" 20 | default: 21 | return s 22 | } 23 | } 24 | 25 | // TODO: use a map 26 | // convert column type to corresponding queryx type 27 | func goModelType(t string, null bool, array bool) string { 28 | if null { 29 | switch t { 30 | case "bigint": 31 | return "queryx.BigInt" 32 | case "uuid": 33 | return "queryx.UUID" 34 | case "string", "text": 35 | if array { 36 | return "queryx.StringArray" 37 | } 38 | return "queryx.String" 39 | case "datetime": 40 | return "queryx.Datetime" 41 | case "date": 42 | return "queryx.Date" 43 | case "time": 44 | return "queryx.Time" 45 | case "integer": 46 | return "queryx.Integer" 47 | case "boolean": 48 | return "queryx.Boolean" 49 | case "float": 50 | return "queryx.Float" 51 | case "json", "jsonb": 52 | return "queryx.JSON" 53 | default: 54 | log.Fatal(fmt.Errorf("unhandled data type %s in goModelType", t)) 55 | return "" 56 | } 57 | } else { 58 | switch t { 59 | case "bigint": 60 | return "int64" 61 | case "uuid": 62 | return "string" 63 | case "string", "text": 64 | return "string" 65 | case "datetime": 66 | return "queryx.Datetime" 67 | case "date": 68 | return "queryx.Date" 69 | case "time": 70 | return "queryx.Time" 71 | case "integer": 72 | return "int32" 73 | case "boolean": 74 | return "bool" 75 | case "float": 76 | return "float" 77 | case "json", "jsonb": 78 | return "queryx.JSON" 79 | default: 80 | log.Fatal(fmt.Errorf("unhandled data type %s in goModelType", t)) 81 | return "" 82 | } 83 | } 84 | } 85 | 86 | // convert column type to corresponding queryx type 87 | func goType(t string, null bool, array bool) string { 88 | switch t { 89 | case "bigint": 90 | return "BigInt" 91 | case "uuid": 92 | return "UUID" 93 | case "string", "text": 94 | if array { 95 | return "StringArray" 96 | } 97 | return "String" 98 | case "datetime": 99 | return "Datetime" 100 | case "date": 101 | return "Date" 102 | case "time": 103 | return "Time" 104 | case "integer": 105 | return "Integer" 106 | case "boolean": 107 | return "Boolean" 108 | case "float": 109 | return "Float" 110 | case "json", "jsonb": 111 | return "JSON" 112 | default: 113 | log.Fatal(fmt.Errorf("unhandled data type %s in goType", t)) 114 | return "" 115 | } 116 | } 117 | 118 | // convert column type to go type in setter method of change object 119 | func goChangeSetType(t string, null bool, array bool) string { 120 | switch t { 121 | case "bigint": 122 | return "int64" 123 | case "boolean": 124 | return "bool" 125 | case "integer": 126 | return "int32" 127 | case "string", "text", "date", "time", "datetime", "uuid": 128 | if array { 129 | return "[]string" 130 | } 131 | return "string" 132 | case "float": 133 | return "float64" 134 | case "json", "jsonb": 135 | return "map[string]interface{}" 136 | default: 137 | log.Fatal(fmt.Errorf("unhandled data type %s in goChangeSetType", t)) 138 | return "" 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /inflect/golang_test.go: -------------------------------------------------------------------------------- 1 | package inflect 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestGoReceiverName(t *testing.T) { 10 | require.Equal(t, "u", GoReceiver("User")) 11 | require.Equal(t, "u", GoReceiver("UserPost")) 12 | } 13 | -------------------------------------------------------------------------------- /inflect/inflect_test.go: -------------------------------------------------------------------------------- 1 | package inflect 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestSnake(t *testing.T) { 10 | require.Equal(t, "user", Snake("User")) 11 | require.Equal(t, "user_post", Snake("UserPost")) 12 | } 13 | 14 | func TestCamel(t *testing.T) { 15 | require.Equal(t, "user", Camel("user")) 16 | require.Equal(t, "userPost", Camel("user_post")) 17 | require.Equal(t, "userpost", Camel("UserPost")) // TODO: may fix this? 18 | } 19 | 20 | func TestPascal(t *testing.T) { 21 | require.Equal(t, "User", Pascal("user")) 22 | require.Equal(t, "UserPost", Pascal("user_post")) 23 | require.Equal(t, "UserPost", Pascal("userPost")) 24 | } 25 | 26 | func TestPlural(t *testing.T) { 27 | require.Equal(t, "users", Plural("user")) 28 | require.Equal(t, "Users", Plural("User")) 29 | require.Equal(t, "user_posts", Plural("user_post")) 30 | require.Equal(t, "userPosts", Plural("userPost")) 31 | require.Equal(t, "moneySlice", Plural("money")) 32 | } 33 | 34 | func TestSingular(t *testing.T) { 35 | require.Equal(t, "user_post", Singular("user_posts")) 36 | } 37 | -------------------------------------------------------------------------------- /inflect/typescript.go: -------------------------------------------------------------------------------- 1 | package inflect 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | ) 7 | 8 | func tsType(t string, null bool, array bool) string { 9 | switch t { 10 | case "uuid", "string", "text": 11 | if array { 12 | return "string[]" 13 | } 14 | return "string" 15 | case "datetime", "date": 16 | return "Date" 17 | case "time": 18 | return "string" 19 | case "bigint", "integer", "float": 20 | return "number" 21 | case "boolean": 22 | return "boolean" 23 | case "json", "jsonb": 24 | return "{ [key: string]: any }" 25 | default: 26 | log.Fatal(fmt.Errorf("unhandled data type %s in tsType", t)) 27 | return "" 28 | } 29 | } 30 | 31 | func tsChangeSetType(t string, null bool, array bool) string { 32 | switch t { 33 | case "bigint", "integer", "float": 34 | return "number" 35 | case "uuid": 36 | return "string" 37 | case "string", "text": 38 | if array { 39 | return "string[]" 40 | } 41 | return "string" 42 | case "datetime", "date", "time": 43 | return "string" 44 | case "boolean": 45 | return "boolean" 46 | case "json", "jsonb": 47 | return "object" 48 | default: 49 | log.Fatal(fmt.Errorf("unhandled data type %s in tsChangeSetType", t)) 50 | return "" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /internal/integration/client/mysql.hcl: -------------------------------------------------------------------------------- 1 | database "db" { 2 | adapter = "mysql" 3 | time_zone = "Asia/Shanghai" 4 | 5 | config "development" { 6 | url = "mysql://root:@127.0.0.1:3306/queryx_test" 7 | } 8 | config "test" { 9 | url = env("DATABASE_URL") 10 | } 11 | 12 | generator "client-golang" { 13 | test = true 14 | } 15 | generator "client-typescript" { 16 | test = true 17 | } 18 | 19 | model "User" { 20 | has_one "account" {} 21 | has_many "user_posts" {} 22 | has_many "posts" { 23 | through = "user_posts" 24 | } 25 | 26 | column "name" { 27 | type = string 28 | } 29 | column "type" { 30 | type = string 31 | } 32 | column "email" { 33 | type = string 34 | } 35 | column "age" { 36 | type = integer 37 | } 38 | column "is_admin" { 39 | type = boolean 40 | } 41 | column "payload" { 42 | type = jsonb 43 | } 44 | column "weight" { 45 | type = float 46 | } 47 | column "date" { 48 | type = date 49 | } 50 | column "datetime" { 51 | type = datetime 52 | } 53 | column "time" { 54 | type = time 55 | } 56 | column "uuid" { 57 | type = uuid 58 | } 59 | } 60 | 61 | model "Post" { 62 | has_many "user_posts" {} 63 | has_many "users" { 64 | through = "user_posts" 65 | } 66 | belongs_to "author" { 67 | model_name = "User" 68 | } 69 | 70 | column "title" { 71 | type = string 72 | } 73 | column "content" { 74 | type = text 75 | } 76 | column "payload" { 77 | type = json 78 | } 79 | } 80 | 81 | model "UserPost" { 82 | belongs_to "user" {} 83 | belongs_to "post" {} 84 | 85 | index { 86 | columns = ["user_id", "post_id"] 87 | unique = true 88 | } 89 | } 90 | 91 | model "Account" { 92 | belongs_to "user" { 93 | index = true 94 | null = false 95 | } 96 | 97 | column "name" { 98 | type = string 99 | } 100 | column "id_num" { 101 | type = integer 102 | } 103 | } 104 | 105 | model "Tag" { 106 | timestamps = false 107 | 108 | column "name" { 109 | type = string 110 | } 111 | 112 | index { 113 | columns = ["name"] 114 | unique = true 115 | } 116 | } 117 | 118 | model "Code" { 119 | table_name = "queryx_codes" 120 | default_primary_key = false 121 | timestamps = false 122 | 123 | column "type" { 124 | type = string 125 | null = false 126 | } 127 | column "key" { 128 | type = string 129 | null = false 130 | } 131 | 132 | primary_key { 133 | columns = ["type", "key"] 134 | } 135 | } 136 | 137 | model "Client" { 138 | default_primary_key = false 139 | timestamps = false 140 | 141 | column "name" { 142 | type = string 143 | } 144 | column "float" { 145 | type = float 146 | } 147 | } 148 | 149 | model "Device" { 150 | default_primary_key = false 151 | 152 | column "id" { 153 | type = uuid 154 | null = false 155 | } 156 | 157 | primary_key { 158 | columns = ["id"] 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /internal/integration/client/postgresql.hcl: -------------------------------------------------------------------------------- 1 | database "db" { 2 | adapter = "postgresql" 3 | time_zone = "Asia/Shanghai" 4 | 5 | config "development" { 6 | url = "postgresql://postgres:postgres@localhost:5432/queryx_test?sslmode=disable" 7 | } 8 | config "test" { 9 | url = env("DATABASE_URL") 10 | } 11 | 12 | generator "client-golang" { 13 | test = true 14 | } 15 | generator "client-typescript" { 16 | test = true 17 | } 18 | 19 | model "User" { 20 | has_one "account" {} 21 | has_many "user_posts" {} 22 | has_many "posts" { 23 | through = "user_posts" 24 | } 25 | 26 | column "name" { 27 | type = string 28 | } 29 | column "type" { 30 | type = string 31 | } 32 | column "email" { 33 | type = string 34 | } 35 | column "age" { 36 | type = integer 37 | } 38 | column "is_admin" { 39 | type = boolean 40 | } 41 | column "payload" { 42 | type = jsonb 43 | } 44 | column "weight" { 45 | type = float 46 | } 47 | column "date" { 48 | type = date 49 | } 50 | column "datetime" { 51 | type = datetime 52 | } 53 | column "time" { 54 | type = time 55 | } 56 | column "uuid" { 57 | type = uuid 58 | } 59 | } 60 | 61 | model "Post" { 62 | has_many "user_posts" {} 63 | has_many "users" { 64 | through = "user_posts" 65 | } 66 | belongs_to "author" { 67 | model_name = "User" 68 | } 69 | 70 | column "title" { 71 | type = string 72 | } 73 | column "content" { 74 | type = text 75 | } 76 | column "payload" { 77 | type = json 78 | } 79 | } 80 | 81 | model "UserPost" { 82 | belongs_to "user" {} 83 | belongs_to "post" {} 84 | 85 | index { 86 | columns = ["user_id", "post_id"] 87 | unique = true 88 | } 89 | } 90 | 91 | model "Account" { 92 | belongs_to "user" { 93 | index = true 94 | null = false 95 | } 96 | 97 | column "name" { 98 | type = string 99 | } 100 | column "id_num" { 101 | type = integer 102 | } 103 | } 104 | 105 | model "Tag" { 106 | timestamps = false 107 | 108 | column "name" { 109 | type = string 110 | } 111 | 112 | index { 113 | columns = ["name",] 114 | unique = true 115 | } 116 | } 117 | 118 | model "Code" { 119 | table_name = "queryx_codes" 120 | default_primary_key = false 121 | timestamps = false 122 | 123 | column "type" { 124 | type = string 125 | null = false 126 | } 127 | column "key" { 128 | type = string 129 | null = false 130 | } 131 | 132 | primary_key { 133 | columns = ["type", "key"] 134 | } 135 | } 136 | 137 | model "Client" { 138 | default_primary_key = false 139 | timestamps = false 140 | 141 | column "name" { 142 | type = string 143 | } 144 | column "float" { 145 | type = float 146 | } 147 | } 148 | 149 | model "Device" { 150 | default_primary_key = false 151 | 152 | column "id" { 153 | type = uuid 154 | null = false 155 | } 156 | 157 | primary_key { 158 | columns = ["id"] 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /internal/integration/client/sqlite.hcl: -------------------------------------------------------------------------------- 1 | database "db" { 2 | adapter = "sqlite" 3 | time_zone = "Asia/Shanghai" 4 | 5 | config "development" { 6 | url = "sqlite:test.sqlite3" 7 | } 8 | config "test" { 9 | url = env("DATABASE_URL") 10 | } 11 | 12 | generator "client-golang" { 13 | test = true 14 | } 15 | generator "client-typescript" { 16 | test = true 17 | } 18 | 19 | model "User" { 20 | has_one "account" {} 21 | has_many "user_posts" {} 22 | has_many "posts" { 23 | through = "user_posts" 24 | } 25 | 26 | column "name" { 27 | type = string 28 | } 29 | column "type" { 30 | type = string 31 | } 32 | column "email" { 33 | type = string 34 | } 35 | column "age" { 36 | type = integer 37 | } 38 | column "is_admin" { 39 | type = boolean 40 | } 41 | column "payload" { 42 | type = jsonb 43 | } 44 | column "weight" { 45 | type = float 46 | } 47 | column "date" { 48 | type = date 49 | } 50 | column "datetime" { 51 | type = datetime 52 | } 53 | column "time" { 54 | type = time 55 | } 56 | column "uuid" { 57 | type = uuid 58 | } 59 | } 60 | 61 | model "Post" { 62 | has_many "user_posts" {} 63 | has_many "users" { 64 | through = "user_posts" 65 | } 66 | belongs_to "author" { 67 | model_name = "User" 68 | } 69 | 70 | column "title" { 71 | type = string 72 | } 73 | column "content" { 74 | type = text 75 | } 76 | column "payload" { 77 | type = json 78 | } 79 | } 80 | 81 | model "UserPost" { 82 | belongs_to "user" {} 83 | belongs_to "post" {} 84 | 85 | index { 86 | columns = ["user_id", "post_id"] 87 | unique = true 88 | } 89 | } 90 | 91 | model "Account" { 92 | belongs_to "user" { 93 | index = true 94 | null = false 95 | } 96 | 97 | column "name" { 98 | type = string 99 | } 100 | column "id_num" { 101 | type = integer 102 | } 103 | } 104 | 105 | model "Tag" { 106 | timestamps = false 107 | 108 | column "name" { 109 | type = string 110 | } 111 | 112 | index { 113 | columns = ["name"] 114 | unique = true 115 | } 116 | } 117 | 118 | model "Code" { 119 | table_name = "queryx_codes" 120 | default_primary_key = false 121 | timestamps = false 122 | 123 | column "type" { 124 | type = string 125 | null = false 126 | } 127 | column "key" { 128 | type = string 129 | null = false 130 | } 131 | 132 | primary_key { 133 | columns = ["type", "key"] 134 | } 135 | } 136 | 137 | model "Client" { 138 | default_primary_key = false 139 | timestamps = false 140 | 141 | column "name" { 142 | type = string 143 | } 144 | column "float" { 145 | type = float 146 | } 147 | } 148 | 149 | model "Device" { 150 | default_primary_key = false 151 | 152 | column "id" { 153 | type = uuid 154 | null = false 155 | } 156 | 157 | primary_key { 158 | columns = ["id"] 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /internal/integration/migrate/mysql1.hcl: -------------------------------------------------------------------------------- 1 | database "db" { 2 | adapter = "mysql" 3 | 4 | config "development" { 5 | url = "mysql://root:@127.0.0.1:3306/queryx_test" 6 | } 7 | 8 | model "User" { 9 | timestamps = false 10 | 11 | column "name" { 12 | type = string 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/integration/migrate/mysql2.hcl: -------------------------------------------------------------------------------- 1 | database "db" { 2 | adapter = "mysql" 3 | 4 | config "development" { 5 | url = "mysql://root:@127.0.0.1:3306/queryx_test" 6 | } 7 | 8 | model "User" { 9 | timestamps = false 10 | 11 | column "name" { 12 | type = string 13 | } 14 | column "email" { 15 | type = string 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /internal/integration/migrate/postgresql1.hcl: -------------------------------------------------------------------------------- 1 | database "db" { 2 | adapter = "postgresql" 3 | 4 | config "development" { 5 | url = "postgresql://postgres:postgres@localhost:5432/queryx_test?sslmode=disable" 6 | } 7 | 8 | model "User" { 9 | timestamps = false 10 | 11 | column "name" { 12 | type = string 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/integration/migrate/postgresql2.hcl: -------------------------------------------------------------------------------- 1 | database "db" { 2 | adapter = "postgresql" 3 | 4 | config "development" { 5 | url = "postgresql://postgres:postgres@localhost:5432/queryx_test?sslmode=disable" 6 | } 7 | 8 | model "User" { 9 | timestamps = false 10 | 11 | column "name" { 12 | type = string 13 | } 14 | column "email" { 15 | type = string 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /internal/integration/migrate/sqlite1.hcl: -------------------------------------------------------------------------------- 1 | database "db" { 2 | adapter = "sqlite" 3 | 4 | config "development" { 5 | url = "sqlite:test.sqlite3" 6 | } 7 | 8 | model "User" { 9 | timestamps = false 10 | 11 | column "name" { 12 | type = string 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/integration/migrate/sqlite2.hcl: -------------------------------------------------------------------------------- 1 | database "db" { 2 | adapter = "sqlite" 3 | 4 | config "development" { 5 | url = "sqlite:test.sqlite3" 6 | } 7 | 8 | model "User" { 9 | timestamps = false 10 | 11 | column "name" { 12 | type = string 13 | } 14 | column "email" { 15 | type = string 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /internal/integration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "integration", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "test": "vitest run" 8 | }, 9 | "dependencies": { 10 | "@types/better-sqlite3": "^7.6.4", 11 | "@types/node": "^20.4.5", 12 | "@types/pg": "^8.10.2", 13 | "better-sqlite3": "^8.5.0", 14 | "date-fns": "^2.30.0", 15 | "mysql2": "^3.5.2", 16 | "pg": "^8.11.1", 17 | "typescript": "^5.1.6", 18 | "vitest": "^0.32.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /internal/integration/postgresql/client.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, beforeAll } from "vitest"; 2 | import { newClient, QXClient } from "./db"; 3 | 4 | let c: QXClient; 5 | 6 | beforeAll(async () => { 7 | c = newClient(); 8 | }); 9 | 10 | test("string array", async () => { 11 | let strings = ["test1", "test2"]; 12 | let user = await c.queryUser().create({ strings }); 13 | expect(user.strings).toEqual(strings); 14 | let row = await c.queryUser().find(user.id); 15 | expect(row.strings).toEqual(strings); 16 | }); 17 | 18 | test("text array", async () => { 19 | let texts = ["test1", "test2"]; 20 | let user = await c.queryUser().create({ texts }); 21 | expect(user.texts).toEqual(texts); 22 | let row = await c.queryUser().find(user.id); 23 | expect(row.texts).toEqual(texts); 24 | }); 25 | 26 | test("integer array", async () => { 27 | let integers = [1, 2]; 28 | let user = await c.queryUser().create({ integers }); 29 | expect(user.integers).toEqual(integers); 30 | let row = await c.queryUser().find(user.id); 31 | expect(row.integers).toEqual(integers); 32 | }); 33 | -------------------------------------------------------------------------------- /internal/integration/postgresql/client_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | "github.com/swiftcarrot/queryx/internal/integration/postgresql/db" 9 | ) 10 | 11 | var c *db.QXClient 12 | 13 | func init() { 14 | client, err := db.NewClient() 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | c = client 19 | } 20 | 21 | func TestStringArray(t *testing.T) { 22 | strings := []string{"test1", "test2"} 23 | user, err := c.QueryUser().Create(c.ChangeUser().SetStrings(strings)) 24 | require.NoError(t, err) 25 | require.Equal(t, strings, user.Strings.Val) 26 | row, err := c.QueryUser().Find(user.ID) 27 | require.NoError(t, err) 28 | require.Equal(t, strings, row.Strings.Val) 29 | } 30 | 31 | // func TestTextArray(t *testing.T) { 32 | // texts := []string{"test1", "test2"} 33 | // user, err := c.QueryUser().Create(c.ChangeUser().SetTexts(texts)) 34 | // require.NoError(t, err) 35 | // require.Equal(t, texts, user.Texts) 36 | // row, err := c.QueryUser().Find(user.ID) 37 | // require.NoError(t, err) 38 | // require.Equal(t, texts, row.Texts) 39 | // } 40 | 41 | // func TestIntegerArray(t *testing.T) { 42 | // integers := []int{1, 2} 43 | // user, err := c.QueryUser().Create(c.ChangeUser().SetIntegers(integers)) 44 | // require.NoError(t, err) 45 | // require.Equal(t, integers, user.Integers) 46 | // row, err := c.QueryUser().Find(user.ID) 47 | // require.NoError(t, err) 48 | // require.Equal(t, integers, row.Integers) 49 | // } 50 | -------------------------------------------------------------------------------- /internal/integration/postgresql/schema.hcl: -------------------------------------------------------------------------------- 1 | database "db" { 2 | adapter = "postgresql" 3 | time_zone = "Asia/Shanghai" 4 | 5 | config "development" { 6 | url = "postgresql://postgres:postgres@localhost:5432/queryx_test?sslmode=disable" 7 | } 8 | config "test" { 9 | url = env("DATABASE_URL") 10 | } 11 | 12 | generator "client-golang" { 13 | test = true 14 | } 15 | generator "client-typescript" { 16 | test = true 17 | } 18 | 19 | model "User" { 20 | column "strings" { 21 | type = string 22 | array = true 23 | } 24 | column "integers" { 25 | type = integer 26 | array = true 27 | } 28 | column "texts" { 29 | type = text 30 | array = true 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /internal/integration/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "noEmit": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "skipLibCheck": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "queryx", 3 | "version": "0.2.18", 4 | "private": true, 5 | "scripts": { 6 | "website:dev": "vitepress dev website", 7 | "website:build": "vitepress build website", 8 | "website:preview": "vitepress preview website" 9 | }, 10 | "devDependencies": { 11 | "prettier": "^3.0.3", 12 | "vitepress": "1.0.0-rc.10", 13 | "vitepress-plugin-tabs": "^0.4.1" 14 | }, 15 | "prettier": { 16 | "trailingComma": "all" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /schema/config.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/swiftcarrot/queryx/types" 5 | ) 6 | 7 | type Config struct { 8 | Adapter string 9 | Environment string 10 | URL types.StringOrEnv 11 | Database string 12 | Host string 13 | Port string 14 | User string 15 | Password string 16 | Encoding string 17 | Pool int 18 | Timeout int 19 | Socket string 20 | Options map[string]string 21 | RawOptions string 22 | } 23 | -------------------------------------------------------------------------------- /schema/database.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | type Database struct { 4 | Name string 5 | Adapter string 6 | TimeZone string 7 | Generators []*Generator 8 | Configs []*Config 9 | Models []*Model 10 | models map[string]*Model 11 | } 12 | 13 | func (d *Database) LoadConfig(environment string) *Config { 14 | for _, config := range d.Configs { 15 | if config.Environment == environment { 16 | return config 17 | } 18 | } 19 | return nil 20 | } 21 | 22 | func (d *Database) Tables() []string { 23 | tables := []string{} 24 | for _, model := range d.Models { 25 | tables = append(tables, model.TableName) 26 | } 27 | return tables 28 | } 29 | 30 | func (d *Database) buildAssociation() { 31 | for _, m := range d.Models { // User 32 | for _, h := range m.HasMany { // has_many 33 | if m1, ok := d.models[h.ModelName]; ok { // Post 34 | for _, b := range m1.BelongsTo { // belongs_to 35 | if b.ModelName == m.Name { // User 36 | h.BelongsTo = b 37 | } 38 | } 39 | } 40 | } 41 | for _, h := range m.HasOne { 42 | if m1, ok := d.models[h.ModelName]; ok { 43 | for _, b := range m1.BelongsTo { 44 | if b.ModelName == m.Name { 45 | h.BelongsTo = b 46 | } 47 | } 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /schema/dsl_test.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestNewModel(t *testing.T) { 10 | schema := NewSchema() 11 | database := schema.NewDatabase("test") 12 | user := database.NewModel("User") 13 | 14 | require.Equal(t, "User", user.Name) 15 | require.Equal(t, "users", user.TableName) 16 | 17 | require.Equal(t, 0, len(user.Columns)) 18 | 19 | user.AddDefaultPrimaryKey() 20 | 21 | require.Equal(t, 1, len(user.Columns)) 22 | require.Equal(t, "id", user.Columns[0].Name) 23 | require.Equal(t, []string{"id"}, user.PrimaryKey.ColumnNames) 24 | 25 | user.AddTimestamps() 26 | require.Equal(t, 3, len(user.Columns)) 27 | require.Equal(t, "created_at", user.Columns[1].Name) 28 | require.Equal(t, "updated_at", user.Columns[2].Name) 29 | 30 | require.Equal(t, "User(id: bigint, created_at: datetime, updated_at: datetime)", user.String()) 31 | 32 | post := database.NewModel("Post") 33 | b := &BelongsTo{Name: "user"} 34 | post.AddBelongsTo(b) 35 | require.Equal(t, 1, len(post.Columns)) 36 | require.Equal(t, "user_id", post.Columns[0].Name) 37 | require.Equal(t, []*BelongsTo{ 38 | {Name: "user", ModelName: "User", ForeignKey: "user_id"}, 39 | }, post.BelongsTo) 40 | 41 | user.AddHasMany(&HasMany{Name: "posts"}) 42 | user.AddHasOne(&HasOne{Name: "account"}) 43 | require.Equal(t, []*HasMany{ 44 | {Name: "posts", ModelName: "Post", ForeignKey: "user_id", BelongsTo: b}, 45 | }, user.HasMany) 46 | require.Equal(t, []*HasOne{ 47 | {Name: "account", ModelName: "Account", ForeignKey: "user_id"}, 48 | }, user.HasOne) 49 | } 50 | -------------------------------------------------------------------------------- /schema/model.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import "strings" 4 | 5 | type Model struct { 6 | Database *Database 7 | // model name in camel case 8 | Name string 9 | TableName string 10 | TimeZone string 11 | Timestamps bool 12 | Columns []*Column 13 | Attributes []*Attribute 14 | BelongsTo []*BelongsTo 15 | HasMany []*HasMany 16 | HasOne []*HasOne 17 | Index []*Index 18 | // whether to automatically add a default primary key column named "id", default true 19 | DefaultPrimaryKey bool 20 | PrimaryKey *PrimaryKey 21 | } 22 | 23 | // String implements the stringer interface. 24 | func (m *Model) String() string { 25 | var b strings.Builder 26 | b.WriteString(m.Name + "(") 27 | for i, col := range m.Columns { 28 | if i > 0 { 29 | b.WriteString(", ") 30 | } 31 | b.WriteString(col.Name + ": " + col.Type) 32 | } 33 | b.WriteString(")") 34 | return b.String() 35 | } 36 | 37 | type PrimaryKey struct { 38 | TableName string 39 | Columns []*Column 40 | ColumnNames []string 41 | } 42 | 43 | type Column struct { 44 | // column name in snake case 45 | Name string 46 | Type string 47 | // array type 48 | Array bool 49 | // nullable, allow_null by default 50 | Null bool 51 | // sql auto_increment 52 | AutoIncrement bool 53 | Default interface{} // TODO: support default 54 | } 55 | 56 | type Type struct { 57 | Name string 58 | } 59 | 60 | type Enum struct { 61 | Name string 62 | } 63 | 64 | type Attribute struct { 65 | Name string 66 | Type string 67 | Null bool 68 | } 69 | 70 | type HasMany struct { 71 | Name string 72 | ModelName string 73 | Through string 74 | ForeignKey string 75 | Source string 76 | BelongsTo *BelongsTo 77 | } 78 | 79 | type HasOne struct { 80 | Name string 81 | ModelName string 82 | Through string 83 | ForeignKey string 84 | BelongsTo *BelongsTo 85 | } 86 | 87 | type BelongsTo struct { 88 | Name string 89 | ModelName string 90 | ForeignKey string 91 | ForeignType string 92 | PrimaryKey string 93 | Type string 94 | Index bool 95 | Null bool 96 | Dependent string 97 | Optional bool 98 | Required bool 99 | Default bool 100 | } 101 | 102 | type Index struct { 103 | TableName string 104 | ColumnNames []string 105 | Unique bool 106 | } 107 | -------------------------------------------------------------------------------- /schema/mysql.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "ariga.io/atlas/sql/mysql" 8 | "ariga.io/atlas/sql/schema" 9 | ) 10 | 11 | // convert a mysql queryx database schema to an atlas sql schema 12 | func (d *Database) CreateMySQLSchema(dbName string) *schema.Schema { 13 | public := schema.New(dbName) 14 | 15 | for _, model := range d.Models { 16 | t := schema.NewTable(model.TableName) 17 | columnMap := map[string]*schema.Column{} 18 | 19 | for _, c := range model.Columns { 20 | col := schema.NewColumn(c.Name) 21 | switch c.Type { 22 | case "bigint": 23 | col.SetType(&schema.IntegerType{T: mysql.TypeBigInt}) 24 | if c.AutoIncrement { 25 | col.AddAttrs(&mysql.AutoIncrement{}) 26 | } 27 | case "string": 28 | col.SetType(&schema.StringType{T: mysql.TypeVarchar, Size: 255}) 29 | case "text": 30 | col.SetType(&schema.StringType{T: mysql.TypeText}) 31 | case "integer": 32 | col.SetType(&schema.IntegerType{T: mysql.TypeInt}) 33 | case "float": 34 | col.SetType(&schema.FloatType{T: mysql.TypeFloat}) 35 | case "boolean": 36 | col.SetType(&schema.BoolType{T: mysql.TypeBoolean}) 37 | case "enum": 38 | col.SetType(&schema.StringType{T: mysql.TypeEnum, Size: 0}) 39 | case "date": 40 | col.SetType(&schema.TimeType{T: mysql.TypeDate}) 41 | case "time": 42 | col.SetType(&schema.TimeType{T: mysql.TypeDateTime}) 43 | case "datetime": 44 | i := 6 45 | col.SetType(&schema.TimeType{T: mysql.TypeDateTime, Precision: &i}) 46 | case "json", "jsonb": 47 | col.SetType(&schema.JSONType{T: mysql.TypeJSON}) 48 | case "uuid": 49 | col.SetType(&schema.StringType{T: mysql.TypeVarchar, Size: 36}) 50 | } 51 | 52 | col.SetNull(c.Null) 53 | t.AddColumns(col) 54 | columnMap[c.Name] = col 55 | } 56 | 57 | cols := []*schema.Column{} 58 | if model.PrimaryKey != nil { 59 | for _, name := range model.PrimaryKey.ColumnNames { 60 | cols = append(cols, columnMap[name]) 61 | } 62 | pk := schema.NewPrimaryKey(cols...) 63 | t.SetPrimaryKey(pk) 64 | } 65 | 66 | for _, i := range model.Index { 67 | name := fmt.Sprintf("%s_%s_index", i.TableName, strings.Join(i.ColumnNames, "_")) 68 | columns := []*schema.Column{} 69 | for _, name := range i.ColumnNames { 70 | columns = append(columns, columnMap[name]) 71 | } 72 | var index *schema.Index 73 | if i.Unique { 74 | index = schema.NewUniqueIndex(name).AddColumns(columns...) 75 | } else { 76 | index = schema.NewIndex(name).AddColumns(columns...) 77 | } 78 | t.AddIndexes(index) 79 | } 80 | 81 | public.AddTables(t) 82 | } 83 | 84 | return public 85 | } 86 | -------------------------------------------------------------------------------- /schema/schema.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | type Schema struct { 4 | Databases []*Database 5 | } 6 | 7 | type Generator struct { 8 | Name string 9 | Test bool 10 | } 11 | -------------------------------------------------------------------------------- /schema/schema_test.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestTableWithoutPrimaryKey(t *testing.T) { 10 | schema := NewSchema() 11 | database := schema.NewDatabase("test") 12 | user := database.NewModel("User") 13 | user.AddColumn(&Column{Name: "name", Type: "string"}) 14 | 15 | require.Nil(t, user.PrimaryKey) 16 | database.CreatePostgreSQLSchema("test") 17 | database.CreateMySQLSchema("test") 18 | database.CreateSQLiteSchema("test") 19 | } 20 | -------------------------------------------------------------------------------- /schema/sqlite.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "ariga.io/atlas/sql/schema" 9 | "ariga.io/atlas/sql/sqlite" 10 | ) 11 | 12 | func (d *Database) CreateSQLiteSchema(dbName string) *schema.Schema { 13 | public := schema.New(dbName) 14 | 15 | for _, model := range d.Models { 16 | columnMap := map[string]*schema.Column{} 17 | 18 | t := schema.NewTable(model.TableName) 19 | if t.Name == "sqlite_master" { 20 | continue 21 | } 22 | for _, c := range model.Columns { 23 | col := schema.NewColumn(c.Name) 24 | 25 | switch c.Type { 26 | case "string", "uuid": 27 | col.SetType(&schema.StringType{T: "varchar", Size: 0}) 28 | if c.Default != nil { 29 | d, ok := c.Default.(string) 30 | if ok { 31 | col.SetType(&schema.StringType{T: "varchar", Size: 0}).SetDefault(&schema.RawExpr{X: fmt.Sprintf("'%s'", d)}) 32 | } 33 | } 34 | case "text": 35 | col.SetType(&schema.StringType{T: "text", Size: 0}) 36 | case "integer": 37 | if c.Default != nil { 38 | d, ok := c.Default.(int) 39 | if ok { 40 | col.SetType(&schema.IntegerType{T: "integer"}).SetDefault(&schema.RawExpr{X: strconv.Itoa(d)}) 41 | } 42 | } else { 43 | col.SetType(&schema.IntegerType{T: "integer"}) 44 | } 45 | case "bigint": 46 | if c.AutoIncrement { 47 | col.SetType(&schema.IntegerType{T: "integer"}) 48 | col.AddAttrs(&sqlite.AutoIncrement{}) 49 | } else { 50 | col.SetType(&schema.IntegerType{T: "bigint"}) 51 | } 52 | case "float": 53 | col.SetType(&schema.FloatType{T: "float"}) 54 | if c.Default != nil { 55 | d, ok := c.Default.(float64) 56 | if ok { 57 | col.SetType(&schema.FloatType{T: "float"}).SetDefault(&schema.RawExpr{X: strconv.FormatFloat(d, 'f', 10, 64)}) 58 | } 59 | } 60 | case "boolean": 61 | col.SetType(&schema.BoolType{T: "boolean"}) 62 | if c.Default != nil { 63 | d, ok := c.Default.(bool) 64 | if ok { 65 | col.SetType(&schema.BoolType{T: "boolean"}).SetDefault(&schema.RawExpr{X: strconv.FormatBool(d)}) 66 | } 67 | } 68 | case "enum": 69 | col.SetType(&schema.StringType{T: "enum", Size: 0}) 70 | case "date": 71 | col.SetType(&schema.TimeType{T: "date"}) 72 | case "time": 73 | col.SetType(&schema.TimeType{T: "datetime"}) // TODO: "time" 74 | case "datetime": 75 | col.SetType(&schema.TimeType{T: "datetime"}) 76 | case "json", "jsonb": 77 | col.SetType(&schema.JSONType{T: "json"}) 78 | default: 79 | fmt.Printf("This type is not supported:%+v", col.Type) 80 | } 81 | 82 | col.SetNull(c.Null) 83 | t.AddColumns(col) 84 | columnMap[c.Name] = col 85 | } 86 | 87 | if model.PrimaryKey != nil { 88 | cols := []*schema.Column{} 89 | for _, name := range model.PrimaryKey.ColumnNames { 90 | cols = append(cols, columnMap[name]) 91 | } 92 | pk := schema.NewPrimaryKey(cols...) 93 | t.SetPrimaryKey(pk) 94 | } 95 | 96 | for _, i := range model.Index { 97 | name := fmt.Sprintf("%s_%s_index", i.TableName, strings.Join(i.ColumnNames, "_")) 98 | columns := []*schema.Column{} 99 | for _, name := range i.ColumnNames { 100 | columns = append(columns, columnMap[name]) 101 | } 102 | var index *schema.Index 103 | if i.Unique { 104 | index = schema.NewUniqueIndex(name).AddColumns(columns...) 105 | } else { 106 | index = schema.NewIndex(name).AddColumns(columns...) 107 | } 108 | t.AddIndexes(index) 109 | } 110 | 111 | public.AddTables(t) 112 | public.Name = "main" 113 | } 114 | 115 | return public 116 | } 117 | -------------------------------------------------------------------------------- /types/string.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type String struct{} 4 | 5 | type StringOrEnv struct { 6 | Value string 7 | EnvKey string 8 | Default string 9 | } 10 | -------------------------------------------------------------------------------- /website/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import type { Theme } from 'vitepress' 2 | import DefaultTheme from 'vitepress/theme' 3 | import { enhanceAppWithTabs } from 'vitepress-plugin-tabs/client' 4 | 5 | export default { 6 | extends: DefaultTheme, 7 | enhanceApp({ app }) { 8 | enhanceAppWithTabs(app) 9 | } 10 | } satisfies Theme 11 | -------------------------------------------------------------------------------- /website/caitou.yml: -------------------------------------------------------------------------------- 1 | site: queryx 2 | production_branch: main 3 | public: .vitepress/dist 4 | -------------------------------------------------------------------------------- /website/docs/association.md: -------------------------------------------------------------------------------- 1 | # Association 2 | 3 | Queryx supports association definition in the schema file. It also generates corresponding preload query methods to avoid "N+1" query. 4 | 5 | ## belongs_to 6 | 7 | ```hcl 8 | model "Post" { 9 | belongs_to "author" { 10 | model_name = "User" 11 | } 12 | } 13 | ``` 14 | 15 | ::: tabs key:lang 16 | == Go 17 | 18 | ```go 19 | c.QueryPost().PreloadAuthor().All() 20 | ``` 21 | 22 | == TypeScript 23 | 24 | ```typescript 25 | await c.queryPost().preloadAuthor().all(); 26 | ``` 27 | 28 | ::: 29 | 30 | ## has_one 31 | 32 | ```hcl 33 | model "User" { 34 | has_one "account" {} 35 | 36 | column "name" { 37 | type = string 38 | } 39 | } 40 | 41 | model "Account" { 42 | belongs_to "user" {} 43 | 44 | column "name" { 45 | type = string 46 | } 47 | } 48 | ``` 49 | 50 | ![](./has_one.png) 51 | 52 | ::: tabs key:lang 53 | == Go 54 | 55 | ```go 56 | c.QueryUser().PreloadAccount().All() 57 | c.QueryAccount().PreloadUser().All() 58 | ``` 59 | 60 | == TypeScript 61 | 62 | ```typescript 63 | await c.queryUser().preloadAccount().All(); 64 | await c.queryAccount().preloadUser().All(); 65 | ``` 66 | 67 | ::: 68 | 69 | ## has_many 70 | 71 | ```hcl 72 | model "User" { 73 | belongs_to "group" {} 74 | 75 | column "name" { 76 | type = string 77 | } 78 | } 79 | 80 | model "Group" { 81 | has_many "users" {} 82 | 83 | column "name" { 84 | type = string 85 | } 86 | } 87 | ``` 88 | 89 | ![](./has_many.png) 90 | 91 | ::: tabs key:lang 92 | == Go 93 | 94 | ```go 95 | c.QueryUser().PreloadGroup().All() 96 | c.QueryGroup().PreloadUsers().All() 97 | ``` 98 | 99 | == TypeScript 100 | 101 | ```typescript 102 | await c.queryUser().preloadGroup().all(); 103 | await c.queryGroup().preloadUsers().all(); 104 | ``` 105 | 106 | ::: 107 | 108 | ## has_many through 109 | 110 | ```hcl 111 | model "User" { 112 | has_many "user_posts" {} 113 | has_many "posts" { 114 | through = "user_posts" 115 | } 116 | } 117 | 118 | model "Post" { 119 | has_many "user_posts" {} 120 | has_many "users" { 121 | through = "user_posts" 122 | } 123 | } 124 | 125 | model "UserPost" { 126 | belongs_to "user" {} 127 | belongs_to "post" {} 128 | } 129 | ``` 130 | 131 | ![](./has_many_through.png) 132 | 133 | ::: tabs key:lang 134 | == Go 135 | 136 | ```go 137 | c.QueryUser().PreloadPosts().All() 138 | c.QueryPost().PreloadUsers().All() 139 | ``` 140 | 141 | == TypeScript 142 | 143 | ```typescript 144 | await c.queryUser().preloadPosts().All(); 145 | await c.queryPost().preloadUsers().All(); 146 | ``` 147 | 148 | ::: 149 | -------------------------------------------------------------------------------- /website/docs/build-from-source.md: -------------------------------------------------------------------------------- 1 | # Build from source 2 | 3 | Queryx is Go project, the CLI program can be easily built with Go as 4 | 5 | ```bash 6 | go build -o bin/queryx cmd/queryx/main.go 7 | ``` 8 | -------------------------------------------------------------------------------- /website/docs/cli.md: -------------------------------------------------------------------------------- 1 | # CLI 2 | 3 | By default, the queryx cli will read from `schema.hcl` in the current directory. To use an alternative schema file, you can specify the file path using the `--schema` flag: 4 | 5 | ```sh 6 | queryx format --schema db.hcl 7 | ``` 8 | 9 | All available commands: 10 | 11 | - `queryx init`: creates a sample schema.hcl 12 | - `queryx db:create`: create the database 13 | - `queryx db:drop`: drop the database 14 | - `queryx db:migrate`: generate migration files and run pending migrations 15 | - `queryx db:migrate:generate`: generate migration files 16 | 17 | 18 | 19 | - `queryx format`: format schema file with HCL formatter 20 | - `queryx generate`: generate code based on schema 21 | - `queryx version`: print current installed queryx version 22 | -------------------------------------------------------------------------------- /website/docs/custom-primary-key.md: -------------------------------------------------------------------------------- 1 | # Custom primary key 2 | 3 | By default, each model defined in the schema will generate an auto-incremented integer `id` column in the corresponding table. This behavior can be customized using the `primary_key` block. 4 | 5 | ```hcl{2,14-16} 6 | model "Code" { 7 | default_primary_key = false 8 | 9 | column "type" { 10 | type = string 11 | null = false 12 | } 13 | 14 | column "key" { 15 | type = string 16 | null = false 17 | } 18 | 19 | primary_key { 20 | columns = ["type", "key"] 21 | } 22 | } 23 | ``` 24 | 25 | In the example, the `Code` model, which corrsponds to the `codes` table, will have a primary key of `type` and `key`. It is important to note that customizing primary key will affect generated methods, including `Find` and `Delete`. The `Find` method in generated code for the `Code` example will no longer accepts an integer but two strings: 26 | 27 | ::: tabs key:lang 28 | == Go 29 | 30 | ```go 31 | func (q *CodeQuery) Find(typ string, key string) (*Code, error) 32 | ``` 33 | 34 | == TypeScript 35 | 36 | ```typescript 37 | async find(type: string, key: string) {} 38 | ``` 39 | 40 | ::: 41 | 42 | UUID primary key is common in many application, to support it in queryx: 43 | 44 | ```hcl{2,9-11} 45 | model "Device" { 46 | default_primary_key = false 47 | 48 | column "id" { 49 | type = uuid 50 | null = false 51 | } 52 | 53 | primary_key { 54 | columns = ["id"] 55 | } 56 | } 57 | ``` 58 | -------------------------------------------------------------------------------- /website/docs/custom-table-name.md: -------------------------------------------------------------------------------- 1 | # Custom table name 2 | 3 | By default, queryx generates a `table_name` in plural form. For example, a `User` model will have a table named `users`. However, you can customize this behavior using the `table_name` attribute in model block. For example: 4 | 5 | ```hcl{2} 6 | model "User" { 7 | table_name = "queryx_users" 8 | } 9 | ``` 10 | 11 | In this example, queryx will generate the table `queryx_users` for the `User` model. 12 | -------------------------------------------------------------------------------- /website/docs/data-types.md: -------------------------------------------------------------------------------- 1 | # Data Types 2 | 3 | ::: warning 4 | The following document is a work in progress. 5 | ::: 6 | 7 | Predefined data types in queryx: 8 | 9 | - `integer`: 10 | - `bigint`: 11 | - `string`: 12 | - `text`: 13 | - `boolean`: A true/false value 14 | - `float`: 15 | - `json/jsonb`: 16 | - `uuid`: 17 | - `datetime`: A time and date (2006-01-02 15:04:05) 18 | - `time`: A time without date (2006-01-02) 19 | - `date`: A date without time (15:04:05) 20 | -------------------------------------------------------------------------------- /website/docs/database-index.md: -------------------------------------------------------------------------------- 1 | ## Database Index 2 | 3 | Database index can be declared in schema via the `index` block: 4 | 5 | ```hcl 6 | model "UserPost" { 7 | belongs_to "user" {} 8 | belongs_to "post" {} 9 | 10 | index { 11 | columns = ["user_id", "post_id"] 12 | unique = true 13 | } 14 | } 15 | ``` 16 | -------------------------------------------------------------------------------- /website/docs/environment-variable.md: -------------------------------------------------------------------------------- 1 | # Environment Variable 2 | 3 | Queryx provides a convenient feature for reading from environment variables using the built-in `env()` HCL function. It is a common practice for applications to read configuration settings from environment variables in production environments. In the following example, by setting `QUERYX_ENV` to `production`, queryx will automatically read the database connection URL from the `DATABASE_URL` environment variable. 4 | 5 | ```hcl{6-8} 6 | database "db" { 7 | config "development" { 8 | url = "postgres://postgres:postgres@localhost:5432/blog_development?sslmode=disable" 9 | } 10 | 11 | config "production" { 12 | url = env("DATABASE_URL") 13 | } 14 | } 15 | ``` 16 | 17 | -------------------------------------------------------------------------------- /website/docs/has_many.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swiftcarrot/queryx/fe240fe878be207080c71d204deec9d06d612887/website/docs/has_many.png -------------------------------------------------------------------------------- /website/docs/has_many_through.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swiftcarrot/queryx/fe240fe878be207080c71d204deec9d06d612887/website/docs/has_many_through.png -------------------------------------------------------------------------------- /website/docs/has_one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swiftcarrot/queryx/fe240fe878be207080c71d204deec9d06d612887/website/docs/has_one.png -------------------------------------------------------------------------------- /website/docs/primary-key.md: -------------------------------------------------------------------------------- 1 | # Primary Key 2 | 3 | By default, each model defined in the schema will generate an auto-incremented integer `id` column in the corresponding table. This behavior can be customized using the `primary_key` block. 4 | 5 | ```hcl 6 | model "Code" { 7 | default_primary_key = false 8 | 9 | column "type" { 10 | type = string 11 | null = false 12 | } 13 | 14 | column "key" { 15 | type = string 16 | null = false 17 | } 18 | 19 | primary_key { 20 | columns = ["type", "key"] 21 | } 22 | } 23 | ``` 24 | 25 | In the example, the `Code` model, which corrsponds to the `codes` table, will have a primary key of `type` and `key`. It is important to note that customizing primary key will affect generated methods, including `Find` and `Delete`. The `Find` method in generated code for the `Code` example will no longer accepts an integer but two strings: 26 | 27 | ```go 28 | func (q *CodeQuery) Find(typ string, key string) (*Code, error) 29 | ``` 30 | 31 | UUID primary key is common in many application, to support it in queryx: 32 | 33 | ```hcl 34 | model "Device" { 35 | default_primary_key = false 36 | 37 | column "id" { 38 | type = uuid 39 | null = false 40 | } 41 | 42 | primary_key { 43 | columns = ["id"] 44 | } 45 | } 46 | ``` 47 | -------------------------------------------------------------------------------- /website/docs/query-methods.md: -------------------------------------------------------------------------------- 1 | # Query Methods 2 | 3 | ::: warning 4 | The following document is a work in progress. 5 | ::: 6 | 7 | For each model defined in the schema, queryx generates a corresponding query object. 8 | 9 | ::: tabs key:lang 10 | == Go 11 | 12 | ```go 13 | q := c.QueryPost() 14 | ``` 15 | 16 | == TypeScript 17 | 18 | ```typescript 19 | let q = c.queryPost(); 20 | ``` 21 | 22 | ::: 23 | 24 | ### Finder Methods 25 | 26 | A query object supports the following find methods: 27 | 28 | - Find 29 | - FindBy 30 | - FindBySQL 31 | 32 | ### Query Methods 33 | 34 | Query contruction: 35 | 36 | - Where 37 | - Limit 38 | - Offset 39 | - Order 40 | - Joins 41 | 42 | Query execution: 43 | 44 | - All 45 | - First 46 | - Count 47 | - Exists 48 | - UpdateAll 49 | - DeleteAll 50 | 51 | ### Record Methods 52 | 53 | - Update 54 | - Delete 55 | -------------------------------------------------------------------------------- /website/docs/raw-sql.md: -------------------------------------------------------------------------------- 1 | # Raw SQL 2 | 3 | Queryx provides direct support for executing raw SQL queries through the following methods. 4 | 5 | ## Query 6 | 7 | `Query` returns all rows from the database. 8 | 9 | ::: tabs key:lang 10 | == Go 11 | 12 | ```go 13 | type User struct { 14 | Name string `db:"user_name"` 15 | } 16 | var users []User 17 | err := c.Query("select name as user_name from users where id in (?)", []int64{1, 2}).Scan(&users) 18 | ``` 19 | 20 | == TypeScript 21 | 22 | ```typescript 23 | let users = await c.query<{ user_name: string }>( 24 | "select name as user_name from users where id in (?)", 25 | [1, 2], 26 | ); 27 | ``` 28 | 29 | ::: 30 | 31 | ## Query One 32 | 33 | `QueryOne` returns at most a single row from the database. 34 | 35 | ::: tabs key:lang 36 | == Go 37 | 38 | ```go 39 | var user struct { 40 | ID int64 `db:"user_id"` 41 | } 42 | err = c.QueryOne("select id as user_id from users where id = ?", 1).Scan(&user) 43 | ``` 44 | 45 | == TypeScript 46 | 47 | ```typescript 48 | let user = await c.queryOne<{ user_id: number }>( 49 | "select id as user_id from users where id = ?", 50 | 1, 51 | ); 52 | ``` 53 | 54 | ::: 55 | 56 | ## Exec 57 | 58 | `Exec` for SQL statement that don't return data. 59 | 60 | ::: tabs key:lang 61 | == Go 62 | 63 | ```go 64 | rowsAffected, err := c.Exec("update users set name = ? where id = ?", "test1", 1) 65 | ``` 66 | 67 | == TypeScript 68 | 69 | ```typescript 70 | let rowAffected = await c.exec( 71 | "update users set name = ? where id = ?", 72 | "test1", 73 | 1, 74 | ); 75 | ``` 76 | 77 | ::: 78 | -------------------------------------------------------------------------------- /website/docs/time-zone.md: -------------------------------------------------------------------------------- 1 | # Custom Time Zone 2 | 3 | By default, each database uses the "Local" time zone. 4 | 5 | ```hcl{2} 6 | database "db" { 7 | time_zone = "Local" # this is optional 8 | } 9 | ``` 10 | 11 | To specify time zone: 12 | 13 | ```hcl{2} 14 | database "db" { 15 | time_zone = "Africa/Lagos" 16 | } 17 | ``` 18 | -------------------------------------------------------------------------------- /website/docs/transaction.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Transaction 4 | 5 | Queryx also supported type-safe database transactions, making it easy to execute database transactions safely. 6 | 7 | Creating a transaction: 8 | 9 | ::: tabs key:lang 10 | == Go 11 | ```go 12 | c, err := db.NewClient() 13 | tx := c.Tx() 14 | ``` 15 | == TypeScript 16 | ```typescript 17 | let c = newClient(); 18 | let tx = await c.tx(); 19 | ``` 20 | ::: 21 | 22 | The queryx transaction object works similarly to the queryx client methods with the exception that it requires an additional commit call to make changes to the database. 23 | 24 | ::: tabs key:lang 25 | == Go 26 | 27 | ```go 28 | post, err := tx.QueryPost().Create(tx.ChangPost().SetTitle("post title")) 29 | err := post.Update(tx.ChangePost().SetTitle("new post title")) 30 | if err := tx.Commit() { 31 | tx.Rollback() 32 | } 33 | ``` 34 | 35 | == TypeScript 36 | 37 | ```typescript 38 | try { 39 | let post = await tx.queryPost().create(tx.changPost().setTitle("post title")); 40 | await post.update(tx.changePost().setTitle("new post title")); 41 | await tx.Commit(); 42 | } catch (err) { 43 | await tx.Rollback(); 44 | } 45 | ``` 46 | 47 | ::: 48 | 49 | Queryx also supports wrapping transactions within a function block to avoid manual commit and rollback operations. Queryx automatically performs the rollback operation in case of an error returned from the function block. 50 | 51 | ::: tabs key:lang 52 | == Go 53 | ```go 54 | err := c.Transaction(func (tx *db.Tx) error { 55 | post, err := tx.QueryPost().Create(tx.ChangPost().SetTitle("post title")) 56 | if err != nil { 57 | return err 58 | } 59 | err := post.Update(tx.ChangePost().SetTitle("new post title")) 60 | if err != nil { 61 | return err 62 | } 63 | return nil 64 | }) 65 | ``` 66 | == TypeScript 67 | ```typescript 68 | await c.transaction(async function (tx: Tx) { 69 | let post = await tx.queryPost().create(tx.changePost().setTitle("post title")); 70 | await post.update(tx.changePost().setTitle("new post title")); 71 | }); 72 | ``` 73 | ::: 74 | 75 | 76 | -------------------------------------------------------------------------------- /website/docs/what-is-queryx.md: -------------------------------------------------------------------------------- 1 | # What is Queryx? 2 | 3 | ::: warning 4 | This project is currently in beta (v0), although it has been battled tested in internal projects. Feel free to [open an issue](https://github.com/swiftcarrot/queryx/issues) or [start a discussion](https://github.com/swiftcarrot/queryx/discussions) if you have any questions. 5 | ::: 6 | 7 | Queryx is schema-first and type-safe ORM for Go and TypeScript. 8 | 9 | - **Schema First**: Queryx automatically migrates the database based on defined models in a queryx schema file. 10 | - **Type Safe**: Queryx generates friendly, type-safe ORM methods and come with autocomplete support and are free from type-related errors. 11 | - **Go and TypeScript**: Queryx supports generating both Go and TypeScript ORM methods. 12 | 13 | This project is heavily inspired by [Active Record](https://guides.rubyonrails.org/active_record_basics.html) and [ent](https://entgo.io/). Database schema management is built upon [Atlas](https://atlasgo.io/). 14 | -------------------------------------------------------------------------------- /website/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "Queryx" 7 | text: "Schema-first and type-safe ORM for Go and TypeScript" 8 | # tagline: My great project tagline 9 | actions: 10 | - theme: brand 11 | text: Getting Started 12 | link: /docs/getting-started 13 | - theme: alt 14 | text: View Source 15 | link: https://github.com/swiftcarrot/queryx 16 | - theme: alt 17 | text: Join Discord 18 | link: https://discord.gg/QUTxjJBRfA 19 | features: 20 | - title: Schema First 21 | details: Queryx automatically migrates the database based on defined models in a queryx schema file. 22 | - title: Type Safe 23 | details: Queryx generates friendly, type-safe ORM methods and come with autocomplete support and are free from type-related errors. 24 | - title: Go and TypeScript 25 | details: Queryx supports generating both Go and TypeScript ORM methods. 26 | --- 27 | --------------------------------------------------------------------------------