Compare commits

..

14 Commits

Author SHA1 Message Date
dd62b18877 feat: v2.9.7 (#4576)
This pull request primarily updates the GoFrame (`gf`) framework and its
related driver dependencies from version `v2.9.6` to `v2.9.7` across the
repository. Additionally, it removes the `examples` submodule and
updates the contributors image in the `README.MD` to reflect the new
version.

Dependency updates:

* Updated all references to `github.com/gogf/gf/v2` and related driver
dependencies from `v2.9.6` to `v2.9.7` in various `go.mod` files
throughout the repository, including core modules and contributed
drivers/configs.
[[1]](diffhunk://#diff-ee0abb9c50b9f91f424349123e31b7b1ba1e1e4f7497250422696c5bda2e74ceL6-R12)
[[2]](diffhunk://#diff-cef597d401b6dad225f9e2e431bdde7e53cb60bdf287624cef38a6a7bb9ae7a3L7-R7)
[[3]](diffhunk://#diff-970f7eacff9cd97a0d8a00d59ea8041eedaa21c7544c6669aaa58ca692c6b274L6-R6)
[[4]](diffhunk://#diff-c23d0ca80cd6588b7df84de8ef84713f0ce0555ba05d2d9e7f5d1e0324b1ed3aL6-R6)
[[5]](diffhunk://#diff-aa230a2b1198e6ef8afeb7f48335eb2e2f51d87d918d63c4d891fea612d18ff0L6-R6)
[[6]](diffhunk://#diff-86c2390edbede20803cd862908fe95e7207f7dbabd5089ddd4838e1f26e7fecaL6-R6)
[[7]](diffhunk://#diff-5e1af33d38ced461fc0e13981d7051e125876d1692efc3aa9cb4b7faa4c18addL7-R7)
[[8]](diffhunk://#diff-8c6247829130f219981483ccf25af699a63de99afedeb0dd5c1b7bd8ff0919bdL9-R9)
[[9]](diffhunk://#diff-accbd2d37d45e51db3fcb0468043b1e1fd53eeac9e3d3558467ef24444188d2fL7-R7)
[[10]](diffhunk://#diff-15fac9b8e76d2782594c91da72f6a6f42fc18e359c3be35bf6564ac3ca09f700L6-R7)
[[11]](diffhunk://#diff-8e1a76afd564b6073aac7b02ca59f296ae45a24da3dc4d5c40f18169f48ceba1L6-R6)
[[12]](diffhunk://#diff-00a9db26966c21305c72e8f659628dffaff0d6e9dc98a751406d2141d51a5d90L7-R7)
[[13]](diffhunk://#diff-2cbf2f66d5cb77d9f4d00e4c0ce45055620fff50c941a588da31729f09a81f1bL6-R7)
[[14]](diffhunk://#diff-20a21d07addeea398c4adb76d077875894a73b4b5b181b9df1fafe497d3fc843L6-R6)
[[15]](diffhunk://#diff-909670f1c29b0bba24faf1420504b9eacdff124c4cbbec1ddec5de60653ad007L6-R6)
[[16]](diffhunk://#diff-8eef5f0c081743f8002e0faba686e838b323cb53b749706ea42e0440aaa793f1L7-R7)
[[17]](diffhunk://#diff-82345842a29e8eaffa4f51aab96fa2aa78597e6639fe4b0ece797bc60edacea8L6-R6)
[[18]](diffhunk://#diff-23c6a84d45f3b30ae7ab1a95dec0b30329e702923cc74c5344b3606237ddd929L6-R7)

Repository maintenance:

* Removed the `examples` submodule entry from `.gitmodules`, indicating
that the examples are no longer included as a submodule in the
repository.

Documentation update:

* Updated the contributors image in `README.MD` to reference version
`v2.9.7` instead of `v2.9.6`.
2025-12-27 16:07:23 +08:00
cb4681ce3e feat(‎crypto/grsa): Add RSA encryption and decryption function (#4571)
补充RSA加密解密功能
This pull request improves documentation and developer onboarding for
the project, with a particular focus on the RSA cryptography package and
general installation instructions. The main changes include the addition
of a comprehensive README for the `grsa` RSA package, updated
installation steps in both English and Chinese documentation, and minor
clarifications to documentation links.

**Documentation improvements:**

* Added a detailed `README.md` for the `crypto/grsa` package, including
features, security considerations, usage examples, API descriptions, key
format explanations, and error handling guidance.
* Updated the English (`README.MD`) and Chinese (`README.zh_CN.MD`)
documentation to include a clear installation section with `go get`
instructions for easier onboarding.
[[1]](diffhunk://#diff-01e6d9ffed056a02cae8d8a0ec5d476a64d017bf85c0d5a94bb23ca21f33f5aaR27-R32)
[[2]](diffhunk://#diff-c93759cb9a9500f20e551c741eb167fc72825fd638d36121357feb8253ce6ac1R27-R41)
* Clarified and improved documentation links in both English and Chinese
README files, including the addition of a link to the documentation
source and improved naming for the GoDoc/Go package documentation.
[[1]](diffhunk://#diff-01e6d9ffed056a02cae8d8a0ec5d476a64d017bf85c0d5a94bb23ca21f33f5aaR41)
[[2]](diffhunk://#diff-c93759cb9a9500f20e551c741eb167fc72825fd638d36121357feb8253ce6ac1R27-R41)

**Developer tooling:**

* Added a commented-out `go install` command for `golangci-lint` in the
`Makefile` to assist developers in setting up linting tools.

---------

Co-authored-by: hailaz <739476267@qq.com>
2025-12-26 18:18:30 +08:00
7daf916032 refactor(database/gdb): simplify order and group by alias quoting (bu… (#4555)
## What this PR does

Revert the auto table prefix behavior in `Order()` and `Group()`
introduced by #4521.

  ## Why

PR #4521 attempted to resolve column ambiguity in GROUP BY/ORDER BY with
MySQL JOIN by automatically adding table prefixes to unqualified
columns. However,
  this approach has issues:

1. When using `.As()` to set table alias, it uses the original table
name instead of the alias, causing errors in PostgreSQL and other
databases
2. The framework cannot reliably determine which table the user intends
when multiple tables have the same column
  3. Adds hidden behavior that users may not expect

  ## Example of the issue (#4554)

  ```go
  db.Model("demo_a").As("a").
      LeftJoin("demo_b", "b", "a.id=b.data_id").
      Order("sort").All()

  Expected (v2.9.5):
  ORDER BY "sort"

  Actual (v2.9.6):
  ORDER BY "demo_a".sort  -- Wrong! Should use alias "a" or no prefix

  Solution

Revert to v2.9.5 behavior: Order("sort") generates ORDER BY "sort"
without auto-prefixing. Users should explicitly specify table prefix
when needed:
  Order("a.sort").

  Closes #4554
  ```

---------

Co-authored-by: hailaz <739476267@qq.com>
2025-12-26 16:43:19 +08:00
c82da1e57c feat(cmd/gf): improve gf run watching (#4573)
This pull request introduces a significant enhancement to the `gf run`
command, focusing on improving the directory watching mechanism for
hot-reload functionality. The main improvements include a more
intelligent and efficient algorithm for determining which directories to
watch (recursively or non-recursively), support for custom ignore
patterns, and a comprehensive set of unit tests to ensure correctness.
Additionally, some outdated database drivers were removed from the
dependencies.

**Key changes:**

### Directory Watching Improvements

* Refactored the directory watching logic in `cmd_run.go` to use a
DFS-based algorithm that minimizes the number of watched directories
while respecting ignored patterns. Directories and their descendants
without ignored subdirectories are watched recursively; otherwise,
non-recursive watches are set, and valid children are recursed into.
This results in more efficient and accurate hot-reload behavior.
(`cmd/gf/internal/cmd/cmd_run.go`)
* Added support for custom ignore patterns via the new
`-i`/`--ignorePatterns` flag, allowing users to specify directories to
be excluded from watching. Default ignored patterns include
`node_modules`, `vendor`, hidden directories, and directories starting
with an underscore. (`cmd/gf/internal/cmd/cmd_run.go`)
[[1]](diffhunk://#diff-406a97355fde87f9a6fc118877430c2720632eb94eb2aaba72025571e5fe5146R97)
[[2]](diffhunk://#diff-406a97355fde87f9a6fc118877430c2720632eb94eb2aaba72025571e5fe5146L61-R69)
[[3]](diffhunk://#diff-406a97355fde87f9a6fc118877430c2720632eb94eb2aaba72025571e5fe5146L104-R132)
* Improved parsing of comma-separated arguments for both watch paths and
ignore patterns to support flexible CLI usage.
(`cmd/gf/internal/cmd/cmd_run.go`)

### User Experience and Documentation

* Updated help messages, usage examples, and documentation to reflect
the new features and more intuitive CLI options for specifying watch
paths and ignore patterns. (`cmd/gf/internal/cmd/cmd_run.go`)
[[1]](diffhunk://#diff-406a97355fde87f9a6fc118877430c2720632eb94eb2aaba72025571e5fe5146L51-R58)
[[2]](diffhunk://#diff-406a97355fde87f9a6fc118877430c2720632eb94eb2aaba72025571e5fe5146R85)

### Testing

* Added a comprehensive unit test suite for the new `getWatchPaths`
logic, covering various scenarios including custom ignore patterns,
deeply nested structures, multiple roots, non-existent directories, and
edge cases. (`cmd/gf/internal/cmd/cmd_z_unit_run_test.go`)

### Dependency Cleanup

* Removed unused database driver dependencies from `go.mod` to
streamline the project dependencies. (`cmd/gf/go.mod`)

These changes collectively make the hot-reload feature more robust,
configurable, and efficient, while ensuring maintainability through
thorough testing.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2025-12-26 16:42:06 +08:00
90564f9fb0 feat(os/gfile): add MatchGlob function with globstar support (#4570) (#4574)
This pull request introduces a new glob pattern matching utility to the
`gfile` package, adding support for advanced glob patterns including the
"**" (globstar) operator, which matches across directory boundaries,
similar to bash and gitignore. It also includes a comprehensive set of
unit tests to verify the correctness and cross-platform compatibility of
the new functionality.

**Glob pattern matching feature:**

* Added `MatchGlob` function to `gfile`, which extends `filepath.Match`
with support for the "**" (globstar) pattern, enabling recursive
directory matching and more flexible file pattern matching.
* Implemented internal helpers (`matchGlobstar` and `doMatchGlobstar`)
to handle normalization of path separators and recursive matching logic
for patterns containing "**".

**Testing and validation:**

* Added `gfile_z_unit_match_test.go` with extensive unit tests covering
basic glob patterns, globstar usage, prefix/suffix combinations,
multiple globstars, edge cases, and Windows path compatibility to ensure
robust and cross-platform behavior.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2025-12-26 16:37:45 +08:00
4d6c7e3d3a feat(cmd/gf): init update (#4572)
This pull request introduces a significant enhancement to the `gf init`
command by adding support for initializing GoFrame projects from remote
templates, including interactive and advanced options for template
selection. The changes include new interactive flows, support for remote
repositories (including git subdirectories), and modularization of the
template initialization logic into a new `geninit` package.

The most important changes are:

### New Features & Interactive Initialization

* Added support for initializing projects from remote templates via the
`--repo/-r` flag, interactive mode (`--interactive/-i`), and version
selection (`--select/-s`). Users can now select built-in or remote
templates, specify custom repositories, and interactively choose project
configuration. (`cmd/gf/internal/cmd/cmd_init.go`)
[[1]](diffhunk://#diff-1213f1d7ea9ec0979d1b7aafaf9c84d53846c95f541e0252ab976cca90c677bdR50-R55)
[[2]](diffhunk://#diff-1213f1d7ea9ec0979d1b7aafaf9c84d53846c95f541e0252ab976cca90c677bdL68-R161)
[[3]](diffhunk://#diff-1213f1d7ea9ec0979d1b7aafaf9c84d53846c95f541e0252ab976cca90c677bdR267-R398)
* Introduced interactive prompts for template and project configuration,
including project name, module path, and dependency upgrade options.
(`cmd/gf/internal/cmd/cmd_init.go`)

### Code Organization & Modularization

* Extracted remote template initialization logic into a new package,
`geninit`, with a clear API for processing templates, handling
Go/gomod/git environments, and managing project generation.
(`cmd/gf/internal/cmd/geninit/geninit.go`)
* Added helper modules for Go and Git environment checks
(`geninit_env.go`), template downloading (`geninit_downloader.go`), and
AST-based Go import path replacement (`geninit_ast.go`).
[[1]](diffhunk://#diff-6238f52cc62f1e0dd569c7b1eacec609337e6e9eb9faf8604dcfc82149d907d1R1-R90)
[[2]](diffhunk://#diff-bbc29bf9a77f7097721185062041ff8ef622176bfb2c3886a94e68485773b5e6R1-R99)
[[3]](diffhunk://#diff-269925976ae0929279513615dbafc06f8560859ff0830ce82702735a5a7d6c61R1-R127)

### Usability Improvements

* Updated help and usage documentation to reflect new flags and
initialization modes, making it easier for users to discover and use the
new features. (`cmd/gf/internal/cmd/cmd_init.go`)

These changes greatly improve the flexibility and user experience of
project initialization in GoFrame, enabling both simple and advanced
workflows.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-12-26 12:01:32 +08:00
18e77de02f fix(net/ghttp): fix #4567 (#4569)
fix:  #4567
2025-12-18 15:21:57 +08:00
bf6238e178 feat(contrib/drivers/gaussdb): add gaussdb driver support (#4563)
This pull request introduces a new database driver for openGauss
(GaussDB), integrating it into the GoFrame framework. The implementation
includes connection handling, SQL execution, type conversion, and other
driver-specific logic. Additionally, the CI workflow is updated to
include an openGauss server for testing. The main themes are: new driver
implementation, SQL and type handling, and CI integration.

**GaussDB Driver Implementation:**

* Added a new driver in `contrib/drivers/gaussdb` to support
openGauss/GaussDB databases, including initialization, connection
handling, and registration with GoFrame's database abstraction.
(`gaussdb.go`, `gaussdb_open.go`)
[[1]](diffhunk://#diff-4f0d2a9160a039ccdf1dc98205ed7cd9f3bb8d606fed57c5a4813937eecca81fL11-R49)
[[2]](diffhunk://#diff-a0534a00c87159a3a3d2ea20a9779ead115cc7e38ab274484cfd4b2aa86b6055R1-R69)
* Implemented custom SQL execution and result handling to support
GaussDB's PostgreSQL-based features, including primary key handling on
insert and custom result types. (`gaussdb_do_exec.go`,
`gaussdb_result.go`)
[[1]](diffhunk://#diff-528b2ec06651f4af022e0550526794a606bf257d59bc18b6bce58373c784a2f2R1-R110)
[[2]](diffhunk://#diff-ad33dffe3bbccae20b113e3865aa491ef3b54c68ef586a89cf09a581a1c2abedR1-R24)

**SQL and Type Handling:**

* Added SQL filtering and placeholder conversion to support
PostgreSQL-style parameterization and GaussDB-specific SQL quirks, such
as handling `INSERT IGNORE` and JSONB syntax. (`gaussdb_do_filter.go`)
* Implemented comprehensive type conversion logic for mapping
PostgreSQL/GaussDB types to Go types, including arrays, UUIDs, and
custom handling for JSON and numeric types. (`gaussdb_convert.go`)
* Provided a function for random ordering (`ORDER BY RANDOM()`) and
explicitly disabled upsert/ON CONFLICT support, as GaussDB does not
support this feature. (`gaussdb_order.go`, `gaussdb_format_upsert.go`)
[[1]](diffhunk://#diff-510fc9393899057fddacc7dd6d14f0ca2fff145b52341dd3cfa5db48c960e5c1R1-R12)
[[2]](diffhunk://#diff-c89496520a15032be867e26861b248f11557cc45d683b5216ca1756949a7b9adR1-R94)

**CI Integration:**

* Updated the CI workflow to start an openGauss server in Docker,
enabling automated tests against the new driver.
(`.github/workflows/ci-main.yml`)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-16 21:42:29 +08:00
887a776441 fix(cmd/gf): fix gf gen dao with removeFieldPrefix (#4243)
Fixed: #4113 
when use "removeFieldPrefix" config to generate entity, also delete
prefix in json tag

Co-authored-by: zhang <zhangtao@changxinsec.com>
Co-authored-by: hailaz <739476267@qq.com>
2025-12-13 17:37:45 +08:00
7274a7399a feature(crypto): add gsha256 (#4558)
This pull request introduces a new package, `gsha256`, providing SHA256
encryption utilities for both arbitrary data and file contents. It also
adds comprehensive unit tests to ensure the correctness of these new
APIs.

**New SHA256 encryption utilities:**

* Added the `gsha256` package with three main functions:
-
[`Encrypt`](diffhunk://#diff-664839ae1ff382c08d451abed4ad531eabffa7ef294becde4a0c580be482a9cfR1-R52):
Hashes any variable using SHA256, converting input to bytes via `gconv`.
-
[`EncryptFile`](diffhunk://#diff-664839ae1ff382c08d451abed4ad531eabffa7ef294becde4a0c580be482a9cfR1-R52):
Hashes the contents of a file at a given path, returning the SHA256
digest as a hex string. Errors are wrapped for clarity.
-
[`MustEncryptFile`](diffhunk://#diff-664839ae1ff382c08d451abed4ad531eabffa7ef294becde4a0c580be482a9cfR1-R52):
Like `EncryptFile`, but panics on error for convenience in situations
where failure is unexpected.

**Unit tests for new functionality:**

* Added `gsha256_z_unit_test.go` to test the new APIs:
- Verifies correct hash output for string and struct input to `Encrypt`.
- Validates file hashing and error handling for non-existent files in
`EncryptFile`.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joy999 <5414344+joy999@users.noreply.github.com>
2025-12-12 15:15:08 +08:00
b59824e9dc feat(database/gdb): Optimize SoftTime feature (#4559)
本次PR主要针对GoFrame ORM中的软删除`SoftTime`功能进行了优化
   - 新增`SoftTimeFieldType`枚举类型,用于区分创建、更新、删除三种不同的软时间字段
   - 替代之前使用的魔数方式,提高类型安全性
   - 将原有的6个方法精简为4个方法, 合并了三个几乎相同的`GetFieldNameAndTypeFor*`方法为统一的方法


This pull request refactors and simplifies the "soft time"
(created/updated/deleted timestamp) handling logic in the database
layer, making the codebase more maintainable and extensible. The changes
consolidate multiple similar methods into general-purpose ones, improve
cache key generation, and clarify the logic for generating and applying
soft time field values and conditions.

Key changes include:

**Soft Time API Refactoring and Simplification**
- Consolidated multiple methods (`GetFieldNameAndTypeForCreate`,
`GetFieldNameAndTypeForUpdate`, `GetFieldNameAndTypeForDelete`) into a
single, parameterized method `GetFieldInfo`, reducing code duplication
and making it easier to support new soft time field types. The interface
and implementation for soft time maintenance (`iSoftTimeMaintainer`,
`softTimeMaintainer`) have been updated accordingly.
[[1]](diffhunk://#diff-6c1d606032d981a7b8aecd3a7167823f76b69407a29eb9a244175a82f59965d8L46-R66)
[[2]](diffhunk://#diff-6c1d606032d981a7b8aecd3a7167823f76b69407a29eb9a244175a82f59965d8L105-R180)
- Combined and renamed methods for generating soft time field values and
delete conditions, such as merging
`GetValueByFieldTypeForCreateOrUpdate` into `GetFieldValue`, and
`GetWhereConditionForDelete` into `GetDeleteCondition`. Related usages
throughout the codebase have been updated to use the new methods.
[[1]](diffhunk://#diff-6c1d606032d981a7b8aecd3a7167823f76b69407a29eb9a244175a82f59965d8L255-R206)
[[2]](diffhunk://#diff-97beb485550e4381182a04bbb857a25b7f4ecd4a594dff8ac884cfaae38f3046L34-R35)
[[3]](diffhunk://#diff-97beb485550e4381182a04bbb857a25b7f4ecd4a594dff8ac884cfaae38f3046L55-R55)
[[4]](diffhunk://#diff-88304ddb7791aedbd83dafb68374aecab286d1356a7f2f149a8e57ac1a7f40b4L265-R267)
[[5]](diffhunk://#diff-88304ddb7791aedbd83dafb68374aecab286d1356a7f2f149a8e57ac1a7f40b4L298-R311)
[[6]](diffhunk://#diff-d4f6e0370e049dea52f3db9a13c64e2cfb2f7ef012433186e21179149b626d0fL944-R944)

**Soft Delete Logic Improvements**
- Refactored the logic for building soft delete WHERE conditions and
generating update data, with clearer and more robust handling of field
types and prefixes. Introduced helper methods like
`buildDeleteCondition`, `GetDeleteData`, and improved error logging for
invalid field types.
[[1]](diffhunk://#diff-6c1d606032d981a7b8aecd3a7167823f76b69407a29eb9a244175a82f59965d8L287-R234)
[[2]](diffhunk://#diff-6c1d606032d981a7b8aecd3a7167823f76b69407a29eb9a244175a82f59965d8L313-R395)

**Cache Key Generation Enhancements**
- Added dedicated helper functions for generating cache keys for table
names, table fields, select queries, and soft time field/type lookups,
improving cache consistency and code readability.
[[1]](diffhunk://#diff-d57d57e6f9b342ba6fa30c4bb413e2f4f3514a8cd5ad36949eef126e5f8b7ac9R969)
[[2]](diffhunk://#diff-d57d57e6f9b342ba6fa30c4bb413e2f4f3514a8cd5ad36949eef126e5f8b7ac9R980)
[[3]](diffhunk://#diff-d57d57e6f9b342ba6fa30c4bb413e2f4f3514a8cd5ad36949eef126e5f8b7ac9R993-R1002)
[[4]](diffhunk://#diff-b1bbe5e3995261813e4e0ac6ffee8a37c236eaa2759f2bd82e211711695a70bcL790-R790)

**General Code Cleanup**
- Removed redundant code, clarified comments, and improved naming
throughout the affected files, making the code easier to follow and
maintain.
[[1]](diffhunk://#diff-6c1d606032d981a7b8aecd3a7167823f76b69407a29eb9a244175a82f59965d8L105-R180)
[[2]](diffhunk://#diff-6c1d606032d981a7b8aecd3a7167823f76b69407a29eb9a244175a82f59965d8L255-R206)

Let me know if you'd like a walkthrough of any specific part of the
refactored soft time logic!
2025-12-12 15:14:21 +08:00
5cbe421aaa feat(contrib/drivers): more database drivers (#4553)
This pull request adds first-class support for MariaDB, TiDB, OceanBase,
and GaussDB as separate database drivers in the GoFrame ecosystem,
rather than relying solely on MySQL compatibility. It introduces new
driver packages for each database, updates documentation to reflect
these additions, and adjusts dependency management files accordingly.
The changes also deprecate the MariaDB-specific logic in the MySQL
driver in favor of the new dedicated MariaDB driver.

**New Database Driver Support**

* Added new driver packages for MariaDB, TiDB, OceanBase, and GaussDB
under `contrib/drivers/`, each with their own Go module files and driver
implementation that wraps the MySQL driver for protocol compatibility
and future extensibility.
[[1]](diffhunk://#diff-0dd9dca0fb712c3691a95186853d1fc38a30a74ba34cbdc9aa6facee5457d681R1-R48)
[[2]](diffhunk://#diff-23c6a84d45f3b30ae7ab1a95dec0b30329e702923cc74c5344b3606237ddd929R1-R44)
[[3]](diffhunk://#diff-a8a6766c0d5b9c0788d0276b41b33fdbe786e0584fda19fd26db715bcf46fbcdR1-R48)
[[4]](diffhunk://#diff-2cbf2f66d5cb77d9f4d00e4c0ce45055620fff50c941a588da31729f09a81f1bR1-R44)
[[5]](diffhunk://#diff-4f0d2a9160a039ccdf1dc98205ed7cd9f3bb8d606fed57c5a4813937eecca81fR1-R47)
[[6]](diffhunk://#diff-accbd2d37d45e51db3fcb0468043b1e1fd53eeac9e3d3558467ef24444188d2fR1-R44)
[[7]](diffhunk://#diff-15fac9b8e76d2782594c91da72f6a6f42fc18e359c3be35bf6564ac3ca09f700R1-R44)
* Registered these new drivers in the main module's `go.mod` and
`go.work` files for proper dependency resolution and local development.
[[1]](diffhunk://#diff-ee0abb9c50b9f91f424349123e31b7b1ba1e1e4f7497250422696c5bda2e74ceR12-R15)
[[2]](diffhunk://#diff-a70c108de96ca9b56b7768254143b2b9f20ce1dcab51d92ce083fdfcba2efd6cR17-R20)

**Documentation Updates**

* Expanded the `contrib/drivers/README.MD` to include installation and
import instructions for the new drivers, and clarified the supported
drivers section with dedicated code examples for each.
[[1]](diffhunk://#diff-d49f5bc3a34b11a6ccb82cc54675b06a7dea5f0a943ae91c4ca0d28bd5003299R12-R24)
[[2]](diffhunk://#diff-d49f5bc3a34b11a6ccb82cc54675b06a7dea5f0a943ae91c4ca0d28bd5003299L46-R80)

**MariaDB Driver Enhancements**

* Implemented a MariaDB-specific `TableFields` method and SQL query in
the new driver, improving the accuracy of table field retrieval for
MariaDB databases.
* Added unit test initialization code for MariaDB to ensure driver
functionality.

**Deprecation and Refactoring**

* Marked the MariaDB-specific logic and SQL in the MySQL driver as
deprecated, with a note to remove it in the next version, directing
users to the new MariaDB driver instead.
[[1]](diffhunk://#diff-9892cdfb158af82d92f3bfe9e418011bd47a0596638428e61c70993dd72b9c47R18-R20)
[[2]](diffhunk://#diff-9892cdfb158af82d92f3bfe9e418011bd47a0596638428e61c70993dd72b9c47R74-R75)

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Lance Add <1196661499@qq.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-12-09 16:33:55 +08:00
852c3dda62 feat(contrib/drivers/dm&pgsql&mssql&oracle): add Replace/LastInsertId features support for dm/pgsql/mssql/oracle (#4547)
This pull request introduces significant improvements to the handling of
the `Replace` and `Save` operations for multiple database drivers,
especially for MSSQL and PostgreSQL. The changes ensure that these
operations now auto-detect primary keys when conflict columns are not
explicitly provided, improving usability and aligning behavior across
drivers. Additionally, the pull request updates related tests to reflect
these enhancements and includes some minor documentation and code
cleanup.

**Key changes:**

### Enhanced Replace/Save Logic for Database Drivers

* **MSSQL Driver:**
- `Replace` and `Save` operations now auto-detect primary keys if
`OnConflict` is not specified, using the `MERGE` statement for upsert
functionality. If no primary key is found in the data, a detailed error
is returned.
[[1]](diffhunk://#diff-87815aa559a927e2de09bd05148f9841dfc06a1b5f3ecc5e3d5fcb80323a87f8L23-R61)
[[2]](diffhunk://#diff-87815aa559a927e2de09bd05148f9841dfc06a1b5f3ecc5e3d5fcb80323a87f8L43-L59)
- Updated tests to verify that `Replace` correctly updates or inserts
records, and that missing conflict columns are properly handled.
[[1]](diffhunk://#diff-bdbde9d7d6ee14c795343767b414740c4396f4dd3e97788b1f9d4e615405a42dL141-R151)
[[2]](diffhunk://#diff-26338e93e473300b1313936eb0f6826546473793442f24715fa294b595f7a805L2661-R2707)

* **PostgreSQL Driver:**
- Similar to MSSQL, `Replace` and `Save` now auto-detect primary keys
for conflict resolution if `OnConflict` is not set, and treat `Replace`
as a `Save` operation.
- Adjusted tests to ensure `Save` and `Replace` work as expected,
including verifying data replacement and insertion.
[[1]](diffhunk://#diff-c22703c37ebb6836c332f7cd2ada570577ba4564fe39886db02f7c2d0e7a2048L93-R93)
[[2]](diffhunk://#diff-c22703c37ebb6836c332f7cd2ada570577ba4564fe39886db02f7c2d0e7a2048R102)
[[3]](diffhunk://#diff-c22703c37ebb6836c332f7cd2ada570577ba4564fe39886db02f7c2d0e7a2048L110-R130)

* **DM Driver:**
- Improved conflict detection: now checks that at least one primary key
exists in the provided data when `OnConflict` is not specified, and
provides clearer error messages.
- Refactored to use the core method for primary key detection and
removed redundant code.

### Minor Improvements and Documentation

* Added clarifying comments to `DoInsert` methods for ClickHouse, DM,
MSSQL, Oracle, and PostgreSQL drivers, specifying that the input list
must have at least one validated record.
[[1]](diffhunk://#diff-f2e003895041ed3c52b91bb8c270696adc3528d77c39d2f7137af3396267444cR19)
[[2]](diffhunk://#diff-f51b30e3f0b0f1284b905385a89992efd0de2fe9ff8c5a4062344dfab17d428eR23)
[[3]](diffhunk://#diff-87815aa559a927e2de09bd05148f9841dfc06a1b5f3ecc5e3d5fcb80323a87f8L23-R61)
[[4]](diffhunk://#diff-f61dac3fcfd5df4a3936cd8743499c8c0fc45f4f5d0f5398ed84a0cb1603202cR24)
[[5]](diffhunk://#diff-c1dfed79aaa3a432057d2bd74d270e4b4094ebcf72984f1161d4972bea009410R16-R72)
* Minor code and comment cleanups, including improved formatting and
error handling.
[[1]](diffhunk://#diff-f61dac3fcfd5df4a3936cd8743499c8c0fc45f4f5d0f5398ed84a0cb1603202cR37)
[[2]](diffhunk://#diff-f61dac3fcfd5df4a3936cd8743499c8c0fc45f4f5d0f5398ed84a0cb1603202cL96-R98)
[[3]](diffhunk://#diff-f61dac3fcfd5df4a3936cd8743499c8c0fc45f4f5d0f5398ed84a0cb1603202cL106-L116)
[[4]](diffhunk://#diff-a17b44c76aaac53d1f164a2bb9440a5531659f4355e7ccfabdadff8dc8633c09L170-R171)
[[5]](diffhunk://#diff-56189fa9ae1df51716b50d34d7fe56bfe67a330e8ac2c6b0de7b958db6817ed5R83-R98)

### Workflow and Documentation Updates

* Updated example Docker commands in the CI workflow for consistency and
clarity.
[[1]](diffhunk://#diff-a1a3cb9bdeb5541d148091d973cf266aa3b317e6415a86630e816cbe27cf8b9cL57-R57)
[[2]](diffhunk://#diff-a1a3cb9bdeb5541d148091d973cf266aa3b317e6415a86630e816cbe27cf8b9cL78-R78)
[[3]](diffhunk://#diff-a1a3cb9bdeb5541d148091d973cf266aa3b317e6415a86630e816cbe27cf8b9cL92-R92)
[[4]](diffhunk://#diff-a1a3cb9bdeb5541d148091d973cf266aa3b317e6415a86630e816cbe27cf8b9cL106-R106)
[[5]](diffhunk://#diff-a1a3cb9bdeb5541d148091d973cf266aa3b317e6415a86630e816cbe27cf8b9cL153-R153)
[[6]](diffhunk://#diff-a1a3cb9bdeb5541d148091d973cf266aa3b317e6415a86630e816cbe27cf8b9cL164-R164)
* Removed outdated note about `Replace` support from the SQLite driver
documentation.

These changes improve the consistency, reliability, and developer
experience when performing upsert operations across different database
backends.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Lance Add <1196661499@qq.com>
2025-12-09 15:46:41 +08:00
d8b857f930 fix(net/ghttp): Fix specification routing custom parameter recognition exception (#4549)
fix #4442
2025-12-09 08:13:11 +08:00
130 changed files with 14070 additions and 1085 deletions

View File

@ -198,6 +198,17 @@ jobs:
ports:
- 5236:5236
# openGauss server
# docker run --privileged=true -e GS_PASSWORD=UTpass@1234 -p 9950:5432 opengauss/opengauss:7.0.0-RC1.B023
gaussdb:
image: opengauss/opengauss:7.0.0-RC1.B023
env:
GS_PASSWORD: UTpass@1234
TZ: Asia/Shanghai
ports:
- 9950:5432
zookeeper:
image: zookeeper:3.8
ports:

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "examples"]
path = examples
url = git@github.com:gogf/examples.git

View File

@ -6,6 +6,7 @@ tidy:
./.make_tidy.sh
# execute "golangci-lint" to check code style
# go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
.PHONY: lint
lint:
golangci-lint run -c .golangci.yml

View File

@ -24,6 +24,12 @@ English | [简体中文](README.zh_CN.MD)
A powerful framework for faster, easier, and more efficient project development.
## Installation
```bash
go get -u github.com/gogf/gf/v2
```
## Documentation
- Official Site: [https://goframe.org](https://goframe.org)
@ -32,13 +38,14 @@ A powerful framework for faster, easier, and more efficient project development.
- Mirror Site: [Github Pages](https://pages.goframe.org)
- Mirror Site: [Offline Docs](https://github.com/gogf/goframe.org-pdf?tab=readme-ov-file#%E6%9C%80%E6%96%B0%E7%89%88%E6%9C%AC)
- GoDoc API: [https://pkg.go.dev/github.com/gogf/gf/v2](https://pkg.go.dev/github.com/gogf/gf/v2)
- Doc Source: [https://github.com/gogf/gf-site](https://github.com/gogf/gf-site)
## Contributors
💖 [Thanks to all the contributors who made GoFrame possible](https://github.com/gogf/gf/graphs/contributors) 💖
<a href="https://github.com/gogf/gf/graphs/contributors">
<img src="https://goframe.org/img/contributors.svg?version=v2.9.6" alt="goframe contributors"/>
<img src="https://goframe.org/img/contributors.svg?version=v2.9.7" alt="goframe contributors"/>
</a>
## License

View File

@ -24,6 +24,12 @@
一个强大的框架,为了更快、更轻松、更高效的项目开发。
## 安装
```bash
go get -u github.com/gogf/gf/v2
```
## 文档
- 官方网站: [https://goframe.org](https://goframe.org)
@ -31,7 +37,8 @@
- 国内镜像: [https://goframe.org.cn](https://goframe.org.cn)
- 镜像网站: [Github Pages](https://pages.goframe.org)
- 镜像网站: [离线文档](https://github.com/gogf/goframe.org-pdf?tab=readme-ov-file#%E6%9C%80%E6%96%B0%E7%89%88%E6%9C%AC)
- GoDoc API: [https://pkg.go.dev/github.com/gogf/gf/v2](https://pkg.go.dev/github.com/gogf/gf/v2)
- Go包文档: [https://pkg.go.dev/github.com/gogf/gf/v2](https://pkg.go.dev/github.com/gogf/gf/v2)
- 文档源码: [https://github.com/gogf/gf-site](https://github.com/gogf/gf-site)
## 贡献者

View File

@ -3,13 +3,13 @@ module github.com/gogf/gf/cmd/gf/v2
go 1.23.0
require (
github.com/gogf/gf/contrib/drivers/clickhouse/v2 v2.9.6
github.com/gogf/gf/contrib/drivers/mssql/v2 v2.9.6
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.6
github.com/gogf/gf/contrib/drivers/oracle/v2 v2.9.6
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.9.6
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.9.6
github.com/gogf/gf/v2 v2.9.6
github.com/gogf/gf/contrib/drivers/clickhouse/v2 v2.9.7
github.com/gogf/gf/contrib/drivers/mssql/v2 v2.9.7
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.7
github.com/gogf/gf/contrib/drivers/oracle/v2 v2.9.7
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.9.7
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.9.7
github.com/gogf/gf/v2 v2.9.7
github.com/gogf/selfupdate v0.0.0-20231215043001-5c48c528462f
github.com/olekukonko/tablewriter v1.1.0
github.com/schollz/progressbar/v3 v3.15.0

View File

@ -46,20 +46,6 @@ github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiU
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/gogf/gf/contrib/drivers/clickhouse/v2 v2.9.6 h1:rJzRmA5TGWMeKDebdDosYODoUrMUHqfA5pWO1MBC5b0=
github.com/gogf/gf/contrib/drivers/clickhouse/v2 v2.9.6/go.mod h1:u+bUsuftf8qpKpPZPdOFhzh3F5KQzo6Wqa9JFTCLFqg=
github.com/gogf/gf/contrib/drivers/mssql/v2 v2.9.6 h1:3QTlIbSdrVYvRMNUF6nckspA6Eh5Uy2NqwB3/auxIwk=
github.com/gogf/gf/contrib/drivers/mssql/v2 v2.9.6/go.mod h1:oMteYgkWImPpUVe1aqPKtZ8jX1dG3v60lS7IA87MwFQ=
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.6 h1:BY1ThxMo0bTx2P18PuCe57ARmjHuEithSdob/CbH/rw=
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.6/go.mod h1:v/jKO9JJdLctlPlnUSnnG0SNSEpElM51Qx3KoI5crkU=
github.com/gogf/gf/contrib/drivers/oracle/v2 v2.9.6 h1:12+sWI/hm1D4KxG+1FMZpfoU3PwtSLJ9KbLNa20roLg=
github.com/gogf/gf/contrib/drivers/oracle/v2 v2.9.6/go.mod h1:gjjhgxqjafnORK0F4Fa5W8TJlassw7svKy7RFj5GKss=
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.9.6 h1:LG/bTOJEpyNu6+IdREqFyi6J8LdZIeceeyxhuyV58LQ=
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.9.6/go.mod h1:Ekd5IgUGyBlbfqKD/69hkIL9vHF6F4V2FeEP3h/pH08=
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.9.6 h1:3QZvWIlz3dLjNELQU+5ZZZWuzEx9gsRFLU+qIKVUG6M=
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.9.6/go.mod h1:7EEAe8UYI5dLeuwCWN3HgC62OhjIYbkynaoavw1U/k4=
github.com/gogf/gf/v2 v2.9.6 h1:fQ6uPtS1Ra8qY+OuzPPZTlgksJ4eOXmTZ1/a2l3Idog=
github.com/gogf/gf/v2 v2.9.6/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0=
github.com/gogf/selfupdate v0.0.0-20231215043001-5c48c528462f h1:7xfXR/BhG3JDqO1s45n65Oyx9t4E/UqDOXep6jXdLCM=
github.com/gogf/selfupdate v0.0.0-20231215043001-5c48c528462f/go.mod h1:HnYoio6S7VaFJdryKcD/r9HgX+4QzYfr00XiXUo/xz0=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=

View File

@ -14,5 +14,9 @@ replace (
github.com/gogf/gf/contrib/drivers/oracle/v2 => ../../contrib/drivers/oracle
github.com/gogf/gf/contrib/drivers/pgsql/v2 => ../../contrib/drivers/pgsql
github.com/gogf/gf/contrib/drivers/sqlite/v2 => ../../contrib/drivers/sqlite
github.com/gogf/gf/contrib/drivers/mariadb/v2 => ../../contrib/drivers/mariadb
github.com/gogf/gf/contrib/drivers/tidb/v2 => ../../contrib/drivers/tidb
github.com/gogf/gf/contrib/drivers/oceanbase/v2 => ../../contrib/drivers/oceanbase
github.com/gogf/gf/contrib/drivers/gaussdb/v2 => ../../contrib/drivers/gaussdb
github.com/gogf/gf/v2 => ../../
)

View File

@ -7,9 +7,11 @@
package cmd
import (
"bufio"
"context"
"fmt"
"os"
"strconv"
"strings"
"github.com/gogf/gf/v2/frame/g"
@ -20,6 +22,7 @@ import (
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gtag"
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/geninit"
"github.com/gogf/gf/cmd/gf/v2/internal/utility/allyes"
"github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
"github.com/gogf/gf/cmd/gf/v2/internal/utility/utils"
@ -44,6 +47,12 @@ const (
gf init my-project
gf init my-mono-repo -m
gf init my-mono-repo -a
gf init my-project -u
gf init my-project -g "github.com/myorg/myproject"
gf init -r github.com/gogf/template-single my-project
gf init -r github.com/gogf/template-single my-project -s
gf init -r github.com/gogf/examples/httpserver/jwt my-jwt
gf init -i
`
cInitNameBrief = `
name for the project. It will create a folder with NAME in current directory.
@ -55,6 +64,16 @@ The NAME will also be the module name for the project.
cInitGitignore = ".gitignore"
)
// defaultTemplates is the list of predefined templates for interactive selection
var defaultTemplates = []struct {
Name string
Repo string
Desc string
}{
{"template-single", "github.com/gogf/template-single", "Single project template"},
{"template-mono", "github.com/gogf/template-mono", "Mono-repo project template"},
}
func init() {
gtag.Sets(g.MapStrStr{
`cInitBrief`: cInitBrief,
@ -64,17 +83,86 @@ func init() {
}
type cInitInput struct {
g.Meta `name:"init"`
Name string `name:"NAME" arg:"true" v:"required" brief:"{cInitNameBrief}"`
Mono bool `name:"mono" short:"m" brief:"initialize a mono-repo instead a single-repo" orphan:"true"`
MonoApp bool `name:"monoApp" short:"a" brief:"initialize a mono-repo-app instead a single-repo" orphan:"true"`
Update bool `name:"update" short:"u" brief:"update to the latest goframe version" orphan:"true"`
Module string `name:"module" short:"g" brief:"custom go module"`
g.Meta `name:"init"`
Name string `name:"NAME" arg:"true" brief:"{cInitNameBrief}"`
Mono bool `name:"mono" short:"m" brief:"initialize a mono-repo instead a single-repo" orphan:"true"`
MonoApp bool `name:"monoApp" short:"a" brief:"initialize a mono-repo-app instead a single-repo" orphan:"true"`
Update bool `name:"update" short:"u" brief:"update to the latest goframe version" orphan:"true"`
Module string `name:"module" short:"g" brief:"custom go module"`
Repo string `name:"repo" short:"r" brief:"remote repository URL for template download"`
SelectVer bool `name:"select" short:"s" brief:"enable interactive version selection for remote template" orphan:"true"`
Interactive bool `name:"interactive" short:"i" brief:"enable interactive mode to select template" orphan:"true"`
}
type cInitOutput struct{}
func (c cInit) Index(ctx context.Context, in cInitInput) (out *cInitOutput, err error) {
// Check if using remote template mode
if in.Repo != "" || in.Interactive {
return c.initFromRemote(ctx, in)
}
// If no name provided and no remote mode, enter interactive mode
if in.Name == "" {
return c.initInteractive(ctx, in)
}
// Default: use built-in template
return c.initFromBuiltin(ctx, in)
}
// initFromRemote initializes project from remote repository
func (c cInit) initFromRemote(ctx context.Context, in cInitInput) (out *cInitOutput, err error) {
repo := in.Repo
name := in.Name
// If interactive mode and no repo specified, let user select
if in.Interactive && repo == "" {
var modPath string
var upgradeDeps bool
repo, name, modPath, upgradeDeps, err = interactiveSelectTemplate()
if err != nil {
return nil, err
}
if modPath != "" {
in.Module = modPath
}
if upgradeDeps {
in.Update = true
}
}
if repo == "" {
return nil, fmt.Errorf("repository URL is required for remote template mode")
}
// Default name to repo basename if empty
if name == "" {
name = gfile.Basename(repo)
mlog.Printf("Using repository basename as project name: %s", name)
}
mlog.Print("initializing from remote template...")
opts := &geninit.ProcessOptions{
SelectVersion: in.SelectVer,
ModulePath: in.Module,
UpgradeDeps: in.Update,
}
if err = geninit.Process(ctx, repo, name, opts); err != nil {
return nil, err
}
mlog.Print("initialization done!")
if name != "" && name != "." {
mlog.Printf(`you can now run "cd %s && gf run main.go" to start your journey, enjoy!`, name)
}
return
}
// initFromBuiltin initializes project from built-in template
func (c cInit) initFromBuiltin(ctx context.Context, in cInitInput) (out *cInitOutput, err error) {
var overwrote = false
if !gfile.IsEmpty(in.Name) && !allyes.Check() {
s := gcmd.Scanf(`the folder "%s" is not empty, files might be overwrote, continue? [y/n]: `, in.Name)
@ -180,3 +268,170 @@ func (c cInit) Index(ctx context.Context, in cInitInput) (out *cInitOutput, err
}
return
}
// initInteractive enters interactive mode when no arguments provided
func (c cInit) initInteractive(ctx context.Context, in cInitInput) (out *cInitOutput, err error) {
reader := bufio.NewReader(os.Stdin)
// Ask user which mode to use
fmt.Println("\nPlease select initialization mode:")
fmt.Println(strings.Repeat("-", 50))
fmt.Println(" [1] Built-in template (default)")
fmt.Println(" [2] Remote template")
fmt.Println(strings.Repeat("-", 50))
fmt.Print("Select mode [1-2] (default: 1): ")
input, err := reader.ReadString('\n')
if err != nil {
mlog.Fatalf("failed to read input: %v", err)
return
}
input = strings.TrimSpace(input)
if input == "2" {
in.Interactive = true
return c.initFromRemote(ctx, in)
}
// Built-in template mode
fmt.Println("\nPlease select project type:")
fmt.Println(strings.Repeat("-", 50))
fmt.Println(" [1] Single project (default)")
fmt.Println(" [2] Mono-repo project")
fmt.Println(" [3] Mono-repo app")
fmt.Println(strings.Repeat("-", 50))
fmt.Print("Select type [1-3] (default: 1): ")
input, err = reader.ReadString('\n')
if err != nil {
mlog.Fatalf("failed to read input: %v", err)
return
}
input = strings.TrimSpace(input)
switch input {
case "2":
in.Mono = true
case "3":
in.MonoApp = true
}
// Get project name
for {
fmt.Print("Enter project name: ")
input, err = reader.ReadString('\n')
if err != nil {
mlog.Fatalf("failed to read input: %v", err)
return
}
in.Name = strings.TrimSpace(input)
if in.Name != "" {
break
}
fmt.Println("Project name cannot be empty")
}
// Get module path (optional)
fmt.Printf("Enter Go module path (leave empty to use \"%s\"): ", in.Name)
input, err = reader.ReadString('\n')
if err != nil {
mlog.Fatalf("failed to read input: %v", err)
return
}
in.Module = strings.TrimSpace(input)
// Ask about update
fmt.Print("Update to latest GoFrame version? [y/N]: ")
input, err = reader.ReadString('\n')
if err != nil {
mlog.Fatalf("failed to read input: %v", err)
return
}
input = strings.TrimSpace(strings.ToLower(input))
in.Update = input == "y" || input == "yes"
fmt.Println()
return c.initFromBuiltin(ctx, in)
}
// interactiveSelectTemplate prompts user to select a template interactively
func interactiveSelectTemplate() (repo, name, modPath string, upgradeDeps bool, err error) {
reader := bufio.NewReader(os.Stdin)
// 1. Select template
fmt.Println("\nPlease select a project template:")
fmt.Println(strings.Repeat("-", 50))
for i, t := range defaultTemplates {
fmt.Printf(" [%d] %s - %s\n", i+1, t.Name, t.Desc)
}
fmt.Printf(" [%d] Custom repository URL\n", len(defaultTemplates)+1)
fmt.Println(strings.Repeat("-", 50))
for {
fmt.Printf("Select template [1-%d]: ", len(defaultTemplates)+1)
input, err := reader.ReadString('\n')
if err != nil {
return "", "", "", false, fmt.Errorf("failed to read template selection: %w", err)
}
input = strings.TrimSpace(input)
idx, e := strconv.Atoi(input)
if e != nil || idx < 1 || idx > len(defaultTemplates)+1 {
fmt.Printf("Invalid selection, please enter a number between 1-%d\n", len(defaultTemplates)+1)
continue
}
if idx <= len(defaultTemplates) {
repo = defaultTemplates[idx-1].Repo
fmt.Printf("Selected: %s\n\n", repo)
} else {
// Custom URL
fmt.Print("Enter repository URL: ")
input, err = reader.ReadString('\n')
if err != nil {
return "", "", "", false, fmt.Errorf("failed to read repository URL: %w", err)
}
repo = strings.TrimSpace(input)
if repo == "" {
fmt.Println("Repository URL cannot be empty")
continue
}
}
break
}
// 2. Enter project name
for {
fmt.Print("Enter project name: ")
input, err := reader.ReadString('\n')
if err != nil {
return "", "", "", false, fmt.Errorf("failed to read project name: %w", err)
}
name = strings.TrimSpace(input)
if name == "" {
fmt.Println("Project name cannot be empty")
continue
}
break
}
// 3. Enter module path (optional)
fmt.Printf("Enter Go module path (leave empty to use \"%s\"): ", name)
input, err := reader.ReadString('\n')
if err != nil {
return "", "", "", false, fmt.Errorf("failed to read module path: %w", err)
}
modPath = strings.TrimSpace(input)
// 4. Ask about upgrade
fmt.Print("Upgrade dependencies to latest (go get -u)? [y/N]: ")
input, err = reader.ReadString('\n')
if err != nil {
return "", "", "", false, fmt.Errorf("failed to read upgrade confirmation: %w", err)
}
input = strings.TrimSpace(strings.ToLower(input))
upgradeDeps = input == "y" || input == "yes"
fmt.Println()
return repo, name, modPath, upgradeDeps, nil
}

View File

@ -33,12 +33,18 @@ type cRun struct {
g.Meta `name:"run" usage:"{cRunUsage}" brief:"{cRunBrief}" eg:"{cRunEg}" dc:"{cRunDc}"`
}
type watchPath struct {
Path string
Recursive bool
}
type cRunApp struct {
File string // Go run file name.
Path string // Directory storing built binary.
Options string // Extra "go run" options.
Args string // Custom arguments.
WatchPaths []string // Watch paths for live reload.
File string // Go run file name.
Path string // Directory storing built binary.
Options string // Extra "go run" options.
Args string // Custom arguments.
WatchPaths []string // Watch paths for live reload.
IgnorePatterns []string // Custom ignore patterns.
}
const (
@ -48,43 +54,47 @@ const (
gf run main.go
gf run main.go --args "server -p 8080"
gf run main.go -mod=vendor
gf run main.go -w "manifest/config/*.yaml"
gf run main.go -w internal,api
gf run main.go -i ".git,node_modules"
`
cRunDc = `
The "run" command is used for running go codes with hot-compiled-like feature,
which compiles and runs the go codes asynchronously when codes change.
`
cRunFileBrief = `building file path.`
cRunPathBrief = `output directory path for built binary file. it's "./" in default`
cRunExtraBrief = `the same options as "go run"/"go build" except some options as follows defined`
cRunArgsBrief = `custom arguments for your process`
cRunWatchPathsBrief = `watch additional paths for live reload, separated by ",". i.e. "manifest/config/*.yaml"`
cRunFileBrief = `building file path.`
cRunPathBrief = `output directory path for built binary file. it's "./" in default`
cRunExtraBrief = `the same options as "go run"/"go build" except some options as follows defined`
cRunArgsBrief = `custom arguments for your process`
cRunWatchPathsBrief = `watch additional paths for live reload, separated by ",". i.e. "internal,api"`
cRunIgnorePatternBrief = `custom ignore patterns for watch, separated by ",". i.e. ".git,node_modules". default patterns: node_modules, vendor, .*, _*. Glob syntax: "*" matches any chars, "?" matches single char, "[abc]" matches char class. Note: patterns match directory names only, not paths`
)
var process *gproc.Process
func init() {
gtag.Sets(g.MapStrStr{
`cRunUsage`: cRunUsage,
`cRunBrief`: cRunBrief,
`cRunEg`: cRunEg,
`cRunDc`: cRunDc,
`cRunFileBrief`: cRunFileBrief,
`cRunPathBrief`: cRunPathBrief,
`cRunExtraBrief`: cRunExtraBrief,
`cRunArgsBrief`: cRunArgsBrief,
`cRunWatchPathsBrief`: cRunWatchPathsBrief,
`cRunUsage`: cRunUsage,
`cRunBrief`: cRunBrief,
`cRunEg`: cRunEg,
`cRunDc`: cRunDc,
`cRunFileBrief`: cRunFileBrief,
`cRunPathBrief`: cRunPathBrief,
`cRunExtraBrief`: cRunExtraBrief,
`cRunArgsBrief`: cRunArgsBrief,
`cRunWatchPathsBrief`: cRunWatchPathsBrief,
`cRunIgnorePatternBrief`: cRunIgnorePatternBrief,
})
}
type (
cRunInput struct {
g.Meta `name:"run" config:"gfcli.run"`
File string `name:"FILE" arg:"true" brief:"{cRunFileBrief}" v:"required"`
Path string `name:"path" short:"p" brief:"{cRunPathBrief}" d:"./"`
Extra string `name:"extra" short:"e" brief:"{cRunExtraBrief}"`
Args string `name:"args" short:"a" brief:"{cRunArgsBrief}"`
WatchPaths []string `name:"watchPaths" short:"w" brief:"{cRunWatchPathsBrief}"`
g.Meta `name:"run" config:"gfcli.run"`
File string `name:"FILE" arg:"true" brief:"{cRunFileBrief}" v:"required"`
Path string `name:"path" short:"p" brief:"{cRunPathBrief}" d:"./"`
Extra string `name:"extra" short:"e" brief:"{cRunExtraBrief}"`
Args string `name:"args" short:"a" brief:"{cRunArgsBrief}"`
WatchPaths []string `name:"watchPaths" short:"w" brief:"{cRunWatchPathsBrief}"`
IgnorePatterns []string `name:"ignorePatterns" short:"i" brief:"{cRunIgnorePatternBrief}"`
}
cRunOutput struct{}
)
@ -101,17 +111,25 @@ func (c cRun) Index(ctx context.Context, in cRunInput) (out *cRunOutput, err err
mlog.Fatalf(`command "go" not found in your environment, please install golang first to proceed this command`)
}
if len(in.WatchPaths) == 1 {
in.WatchPaths = strings.Split(in.WatchPaths[0], ",")
// Parse comma-separated values in WatchPaths
if len(in.WatchPaths) > 0 {
in.WatchPaths = parseCommaSeparatedArgs(in.WatchPaths)
mlog.Printf("watchPaths: %v", in.WatchPaths)
}
// Parse comma-separated values in IgnorePatterns
if len(in.IgnorePatterns) > 0 {
in.IgnorePatterns = parseCommaSeparatedArgs(in.IgnorePatterns)
mlog.Printf("ignorePatterns: %v", in.IgnorePatterns)
}
app := &cRunApp{
File: in.File,
Path: filepath.FromSlash(in.Path),
Options: in.Extra,
Args: in.Args,
WatchPaths: in.WatchPaths,
File: in.File,
Path: filepath.FromSlash(in.Path),
Options: in.Extra,
Args: in.Args,
WatchPaths: in.WatchPaths,
IgnorePatterns: in.IgnorePatterns,
}
dirty := gtype.NewBool()
@ -121,6 +139,7 @@ func (c cRun) Index(ctx context.Context, in cRunInput) (out *cRunOutput, err err
return
}
// Check if the file extension is 'go'.
if gfile.ExtName(event.Path) != "go" {
return
}
@ -138,15 +157,11 @@ func (c cRun) Index(ctx context.Context, in cRunInput) (out *cRunOutput, err err
})
}
if len(app.WatchPaths) > 0 {
for _, path := range app.WatchPaths {
_, err = gfsnotify.Add(gfile.RealPath(path), callbackFunc)
if err != nil {
mlog.Fatal(err)
}
}
} else {
_, err = gfsnotify.Add(gfile.RealPath("."), callbackFunc)
// Get directories to watch (recursive or non-recursive monitoring).
watchPaths := app.getWatchPaths()
for _, wp := range watchPaths {
option := gfsnotify.WatchOption{NoRecursive: !wp.Recursive}
_, err = gfsnotify.Add(wp.Path, callbackFunc, option)
if err != nil {
mlog.Fatal(err)
}
@ -249,35 +264,181 @@ func (app *cRunApp) End(ctx context.Context, sig os.Signal, outputPath string) {
}
func (app *cRunApp) genOutputPath() (outputPath string) {
var renamePath string
outputPath = gfile.Join(app.Path, gfile.Name(app.File))
if runtime.GOOS == "windows" {
outputPath += ".exe"
if gfile.Exists(outputPath) {
renamePath = outputPath + "~"
renamePath := outputPath + "~"
if err := gfile.Rename(outputPath, renamePath); err != nil {
mlog.Print(err)
}
// Clean up the renamed old binary file
defer func() {
if gfile.Exists(renamePath) {
_ = gfile.Remove(renamePath)
}
}()
}
}
return filepath.FromSlash(outputPath)
}
func matchWatchPaths(watchPaths []string, eventPath string) bool {
for _, path := range watchPaths {
absPath, err := filepath.Abs(path)
if err != nil {
mlog.Printf("match watchPath '%s' error: %s", path, err.Error())
// getWatchPaths uses DFS to find the minimal set of directories to watch.
// Rule: if a directory and all its descendants have no ignored subdirectories, watch it;
// otherwise, recurse into valid children and watch the current directory non-recursively.
func (app *cRunApp) getWatchPaths() []watchPath {
roots := []string{"."}
if len(app.WatchPaths) > 0 {
roots = app.WatchPaths
}
// Use custom ignore patterns if provided, otherwise use default.
ignorePatterns := defaultIgnorePatterns
if len(app.IgnorePatterns) > 0 {
ignorePatterns = app.IgnorePatterns
}
var watchPaths []watchPath
for _, root := range roots {
absRoot := gfile.RealPath(root)
if absRoot == "" {
mlog.Printf("watch path '%s' not found, skipping", root)
continue
}
matched, err := filepath.Match(absPath, eventPath)
if err != nil {
mlog.Printf("match watchPath '%s' error: %s", path, err.Error())
if isIgnoredDirName(absRoot, ignorePatterns) {
continue
}
if matched {
app.collectWatchPaths(absRoot, ignorePatterns, &watchPaths)
}
if len(watchPaths) == 0 {
mlog.Printf("no directories to watch, using current directory")
if absCur := gfile.RealPath("."); absCur != "" {
return []watchPath{{Path: absCur, Recursive: true}}
}
return []watchPath{{Path: ".", Recursive: true}}
}
mlog.Printf("watching %d paths", len(watchPaths))
for _, wp := range watchPaths {
recursiveStr := "recursive"
if !wp.Recursive {
recursiveStr = "non-recursive"
}
mlog.Debugf(" - %s (%s)", wp.Path, recursiveStr)
}
return watchPaths
}
// collectWatchPaths performs a DFS traversal to collect the minimal set of directories to watch.
// Returns true if the directory or any of its descendants contains ignored directories.
// Rule: if a directory has no ignored descendants at any depth, watch it recursively;
// otherwise, watch it non-recursively and recurse into valid children.
func (app *cRunApp) collectWatchPaths(dir string, ignorePatterns []string, watchPaths *[]watchPath) bool {
entries, err := gfile.ScanDir(dir, "*", false)
if err != nil {
mlog.Printf("scan directory '%s' error: %s", dir, err.Error())
// If we can't scan the directory, add it to watch list as fallback
*watchPaths = append(*watchPaths, watchPath{Path: dir, Recursive: true})
return false
}
// First pass: identify valid subdirectories and check for directly ignored children
var validSubDirs []string
hasIgnoredChild := false
for _, entry := range entries {
if !gfile.IsDir(entry) {
continue
}
if isIgnoredDirName(entry, ignorePatterns) {
hasIgnoredChild = true
} else {
validSubDirs = append(validSubDirs, entry)
}
}
// If already has ignored child, we know this dir needs non-recursive watch
if hasIgnoredChild {
*watchPaths = append(*watchPaths, watchPath{Path: dir, Recursive: false})
for _, subDir := range validSubDirs {
app.collectWatchPaths(subDir, ignorePatterns, watchPaths)
}
return true
}
// No ignored children, but need to check descendants recursively
// Collect results from all subdirectories first
subResults := make([]bool, len(validSubDirs))
subWatchPaths := make([][]watchPath, len(validSubDirs))
hasIgnoredDescendant := false
for i, subDir := range validSubDirs {
var subPaths []watchPath
subResults[i] = app.collectWatchPaths(subDir, ignorePatterns, &subPaths)
subWatchPaths[i] = subPaths
if subResults[i] {
hasIgnoredDescendant = true
}
}
if !hasIgnoredDescendant {
// No ignored descendants at any depth, watch this directory recursively
*watchPaths = append(*watchPaths, watchPath{Path: dir, Recursive: true})
return false
}
// Has ignored descendants, watch current directory non-recursively
// and add all collected subdirectory watch paths
*watchPaths = append(*watchPaths, watchPath{Path: dir, Recursive: false})
for _, subPaths := range subWatchPaths {
*watchPaths = append(*watchPaths, subPaths...)
}
return true
}
// defaultIgnorePatterns contains glob patterns for directory names that should be ignored when watching.
// These directories typically contain third-party code or non-source files.
// Supported glob syntax (filepath.Match):
// - "*" matches any sequence of non-separator characters
// - "?" matches any single non-separator character
// - "[abc]" matches any character in the bracket
// - "[a-z]" matches any character in the range
// - "[^abc]" or "[!abc]" matches any character not in the bracket
//
// Note: patterns match directory base names only, not full paths (no "/" or path separators allowed).
var defaultIgnorePatterns = []string{
"node_modules",
"vendor",
".*", // All hidden directories (covers .git, .svn, .hg, .idea, .vscode, etc.)
"_*", // Directories starting with underscore
}
// isIgnoredDirName checks if a directory name matches any ignored pattern.
// It accepts either a full path or just the directory name, but only matches against the base name.
// Note: patterns should not contain "/" as they only match directory names, not paths.
func isIgnoredDirName(name string, ignorePatterns []string) bool {
baseName := gfile.Basename(name)
for _, pattern := range ignorePatterns {
if matched, _ := filepath.Match(pattern, baseName); matched {
return true
}
}
return false
}
// parseCommaSeparatedArgs parses command line arguments that may contain comma-separated values.
// It handles both single argument with commas (e.g., "a,b,c") and multiple arguments.
func parseCommaSeparatedArgs(args []string) []string {
var result []string
for _, arg := range args {
parts := strings.Split(arg, ",")
for _, part := range parts {
trimmed := strings.TrimSpace(part)
if trimmed != "" {
result = append(result, trimmed)
}
}
}
return result
}

View File

@ -0,0 +1,336 @@
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package cmd
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/test/gtest"
)
func Test_cRunApp_getWatchPaths_Basic(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
app := &cRunApp{
WatchPaths: []string{"."},
}
watchPaths := app.getWatchPaths()
t.AssertGT(len(watchPaths), 0)
for _, v := range watchPaths {
t.Log(v)
}
})
}
func Test_cRunApp_getWatchPaths_EmptyWatchPaths(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
app := &cRunApp{
WatchPaths: []string{},
}
watchPaths := app.getWatchPaths()
// Should default to current directory "."
t.AssertGT(len(watchPaths), 0)
})
}
func Test_cRunApp_getWatchPaths_CustomIgnorePattern(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
app := &cRunApp{
WatchPaths: []string{"testdata"},
IgnorePatterns: []string{"2572"},
}
watchPaths := app.getWatchPaths()
// Ensure the "2572" directory is not watched directly.
for _, wp := range watchPaths {
t.Log("watch path:", wp)
t.Assert(strings.HasSuffix(wp.Path, "2572"), false)
}
t.AssertGT(len(watchPaths), 0)
})
}
func Test_cRunApp_getWatchPaths_WithIgnoredDirectories(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Create a temporary directory structure for testing
tempDir := gfile.Temp("gf_run_test")
defer gfile.Remove(tempDir)
// Create directory structure:
// tempDir/
// ├── src/
// │ ├── api/
// │ └── internal/
// ├── vendor/ <-- ignored
// └── node_modules/ <-- ignored
gfile.Mkdir(filepath.Join(tempDir, "src", "api"))
gfile.Mkdir(filepath.Join(tempDir, "src", "internal"))
gfile.Mkdir(filepath.Join(tempDir, "vendor"))
gfile.Mkdir(filepath.Join(tempDir, "node_modules"))
app := &cRunApp{
WatchPaths: []string{tempDir},
}
watchPaths := app.getWatchPaths()
// Should watch tempDir non-recursively (to catch top-level files) and src recursively
t.Assert(len(watchPaths), 2)
// First path is tempDir (non-recursive)
t.Assert(watchPaths[0].Path, tempDir)
t.Assert(watchPaths[0].Recursive, false)
// Second path is src (recursive, since it has no ignored descendants)
t.Assert(watchPaths[1].Path, filepath.Join(tempDir, "src"))
t.Assert(watchPaths[1].Recursive, true)
})
}
func Test_cRunApp_getWatchPaths_NoIgnoredDirectories(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Create a temporary directory structure without ignored directories
tempDir := gfile.Temp("gf_run_test_no_ignore")
defer gfile.Remove(tempDir)
// Create directory structure without ignored patterns:
// tempDir/
// ├── src/
// │ ├── api/
// │ └── internal/
gfile.Mkdir(filepath.Join(tempDir, "src", "api"))
gfile.Mkdir(filepath.Join(tempDir, "src", "internal"))
app := &cRunApp{
WatchPaths: []string{tempDir},
}
watchPaths := app.getWatchPaths()
// Should watch the root directory recursively since no ignored directories exist
t.Assert(len(watchPaths), 1)
t.Assert(watchPaths[0].Path, tempDir)
t.Assert(watchPaths[0].Recursive, true)
})
}
func Test_cRunApp_getWatchPaths_CustomIgnorePatterns(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Create a temporary directory structure
tempDir := gfile.Temp("gf_run_test_custom_ignore")
defer gfile.Remove(tempDir)
// Create directory structure:
// tempDir/
// ├── src/
// │ ├── api/
// │ └── internal/
// ├── build/ <-- ignored
// └── dist/ <-- ignored
gfile.Mkdir(filepath.Join(tempDir, "src", "api"))
gfile.Mkdir(filepath.Join(tempDir, "src", "internal"))
gfile.Mkdir(filepath.Join(tempDir, "build"))
gfile.Mkdir(filepath.Join(tempDir, "dist"))
app := &cRunApp{
WatchPaths: []string{tempDir},
IgnorePatterns: []string{"build", "dist"},
}
watchPaths := app.getWatchPaths()
// Should watch tempDir non-recursively and src recursively
t.Assert(len(watchPaths), 2)
t.Assert(watchPaths[0].Path, tempDir)
t.Assert(watchPaths[0].Recursive, false)
t.Assert(watchPaths[1].Path, filepath.Join(tempDir, "src"))
t.Assert(watchPaths[1].Recursive, true)
})
}
func Test_cRunApp_getWatchPaths_DeepNestedStructure(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Create a deep nested directory structure
tempDir := gfile.Temp("gf_run_test_deep")
defer gfile.Remove(tempDir)
// Create deep directory structure:
// tempDir/
// ├── a/
// │ ├── b/
// │ │ └── c/
// │ └── vendor/ <-- ignored
// └── d/
gfile.Mkdir(filepath.Join(tempDir, "a", "b", "c"))
gfile.Mkdir(filepath.Join(tempDir, "a", "vendor"))
gfile.Mkdir(filepath.Join(tempDir, "d"))
app := &cRunApp{
WatchPaths: []string{tempDir},
}
watchPaths := app.getWatchPaths()
// Should watch individual valid directories due to ignored vendor directory
t.AssertGT(len(watchPaths), 0)
// Verify that vendor directory is not in watch list
for _, wp := range watchPaths {
t.Assert(strings.Contains(wp.Path, "vendor"), false)
}
})
}
func Test_cRunApp_getWatchPaths_MultipleRoots(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Create multiple temporary directories
tempDir1 := gfile.Temp("gf_run_test_multi1")
tempDir2 := gfile.Temp("gf_run_test_multi2")
defer gfile.Remove(tempDir1)
defer gfile.Remove(tempDir2)
gfile.Mkdir(filepath.Join(tempDir1, "src"))
gfile.Mkdir(filepath.Join(tempDir2, "api"))
app := &cRunApp{
WatchPaths: []string{tempDir1, tempDir2},
}
watchPaths := app.getWatchPaths()
// Should watch both root directories recursively
t.Assert(len(watchPaths), 2)
// Both directories should be in the watch list
foundDir1, foundDir2 := false, false
for _, wp := range watchPaths {
if wp.Path == tempDir1 {
foundDir1 = true
t.Assert(wp.Recursive, true)
}
if wp.Path == tempDir2 {
foundDir2 = true
t.Assert(wp.Recursive, true)
}
}
t.Assert(foundDir1, true)
t.Assert(foundDir2, true)
})
}
func Test_cRunApp_getWatchPaths_NonExistentDirectory(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
app := &cRunApp{
WatchPaths: []string{"/non/existent/path"},
}
watchPaths := app.getWatchPaths()
// Should fall back to current directory when no valid paths found
t.AssertGT(len(watchPaths), 0)
// Should contain current directory
currentDir, _ := os.Getwd()
foundCurrentDir := false
for _, wp := range watchPaths {
if wp.Path == currentDir {
foundCurrentDir = true
break
}
}
t.Assert(foundCurrentDir, true)
})
}
func Test_isIgnoredDirName(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Test default ignore patterns
t.Assert(isIgnoredDirName("node_modules", defaultIgnorePatterns), true)
t.Assert(isIgnoredDirName("vendor", defaultIgnorePatterns), true)
t.Assert(isIgnoredDirName(".git", defaultIgnorePatterns), true)
t.Assert(isIgnoredDirName("_private", defaultIgnorePatterns), true)
t.Assert(isIgnoredDirName("src", defaultIgnorePatterns), false)
t.Assert(isIgnoredDirName("api", defaultIgnorePatterns), false)
// Test custom ignore patterns
customPatterns := []string{"build", "dist", "*.tmp"}
t.Assert(isIgnoredDirName("build", customPatterns), true)
t.Assert(isIgnoredDirName("dist", customPatterns), true)
t.Assert(isIgnoredDirName("test.tmp", customPatterns), true)
t.Assert(isIgnoredDirName("src", customPatterns), false)
})
}
func Test_cRunApp_getWatchPaths_DeeplyNestedIgnore(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Create a temporary directory structure with deeply nested ignored directory
tempDir := gfile.Temp("gf_run_test_deeply_nested")
defer gfile.Remove(tempDir)
// Create directory structure:
// tempDir/
// ├── a/
// │ ├── b/
// │ │ ├── c/
// │ │ │ └── vendor/ <-- deeply nested ignored (4 levels)
// │ │ └── d/
// │ └── e/
// └── f/
gfile.Mkdir(filepath.Join(tempDir, "a", "b", "c", "vendor"))
gfile.Mkdir(filepath.Join(tempDir, "a", "b", "d"))
gfile.Mkdir(filepath.Join(tempDir, "a", "e"))
gfile.Mkdir(filepath.Join(tempDir, "f"))
app := &cRunApp{
WatchPaths: []string{tempDir},
}
watchPaths := app.getWatchPaths()
// Expected watch paths:
// 1. tempDir (non-recursive) - has ignored descendant
// 2. a (non-recursive) - has ignored descendant in b/c/vendor
// 3. b (non-recursive) - has ignored descendant in c/vendor
// 4. c (non-recursive) - has ignored child vendor
// 5. d (recursive) - no ignored descendants
// 6. e (recursive) - no ignored descendants
// 7. f (recursive) - no ignored descendants
t.AssertGT(len(watchPaths), 0)
// Verify vendor is not in watch paths
for _, wp := range watchPaths {
t.Assert(strings.Contains(wp.Path, "vendor"), false)
}
// Find specific paths and verify their recursive flags
foundF := false
for _, wp := range watchPaths {
if wp.Path == filepath.Join(tempDir, "f") {
foundF = true
t.Assert(wp.Recursive, true) // f should be recursive (no ignored descendants)
}
}
t.Assert(foundF, true)
})
}
func Test_cRunApp_getWatchPaths_EmptyDirectory(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Create an empty temporary directory
tempDir := gfile.Temp("gf_run_test_empty")
defer gfile.Remove(tempDir)
gfile.Mkdir(tempDir)
app := &cRunApp{
WatchPaths: []string{tempDir},
}
watchPaths := app.getWatchPaths()
// Empty directory should be watched recursively (no ignored descendants)
t.Assert(len(watchPaths), 1)
t.Assert(watchPaths[0].Path, tempDir)
t.Assert(watchPaths[0].Recursive, true)
})
}

View File

@ -98,7 +98,6 @@ func generateStructFieldDefinition(
err error
localTypeName gdb.LocalType
localTypeNameStr string
jsonTag = gstr.CaseConvert(field.Name, gstr.CaseTypeMatch(in.JsonCase))
)
if in.TypeMapping != nil && len(in.TypeMapping) > 0 {
@ -156,6 +155,8 @@ func generateStructFieldDefinition(
" #" + formatFieldName(newFiledName, FieldNameCaseCamel),
" #" + localTypeNameStr,
}
jsonTag := gstr.CaseConvert(newFiledName, gstr.CaseTypeMatch(in.JsonCase))
attrLines = append(attrLines, fmt.Sprintf(` #%sjson:"%s"`, tagKey, jsonTag))
// orm tag
if !in.IsDo {

View File

@ -0,0 +1,236 @@
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package geninit
import (
"context"
"path/filepath"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
)
// ProcessOptions contains options for the Process function
type ProcessOptions struct {
SelectVersion bool // Enable interactive version selection
ModulePath string // Custom go.mod module path (e.g., github.com/xxx/xxx)
UpgradeDeps bool // Upgrade dependencies to latest (go get -u ./...)
}
// Process handles the template generation flow from remote repository
func Process(ctx context.Context, repo, name string, opts *ProcessOptions) error {
if opts == nil {
opts = &ProcessOptions{}
}
// 0. Check Go environment first
mlog.Print("Checking Go environment...")
goEnv, err := CheckGoEnv(ctx)
if err != nil {
mlog.Printf("Go environment check failed: %v", err)
return err
}
mlog.Printf("Go environment OK (version: %s)", goEnv.GOVERSION)
// Check if this is a git subdirectory URL
if IsSubdirRepo(repo) {
return processGitSubdir(ctx, repo, name, opts)
}
// Try Go module download first, fallback to git subdirectory if it fails
// This handles edge cases where the heuristic may be incorrect
err = processGoModule(ctx, repo, name, opts)
if err != nil {
mlog.Printf("Go module download failed, trying git subdirectory mode: %v", err)
mlog.Print("Note: If this is a git subdirectory, you can force git mode by using a full git URL")
// If Go module download fails, try git subdirectory as fallback
// This handles cases where the heuristic incorrectly classified a git subdir as Go module
if IsSubdirRepo(repo) {
mlog.Print("Falling back to git subdirectory download...")
return processGitSubdir(ctx, repo, name, opts)
}
}
return err
}
// processGoModule handles standard Go module download via go get
func processGoModule(ctx context.Context, repo, name string, opts *ProcessOptions) error {
// Extract module path (without version)
modulePath := repo
specifiedVersion := ""
if gstr.Contains(repo, "@") {
parts := gstr.Split(repo, "@")
modulePath = parts[0]
specifiedVersion = parts[1]
}
// Default name to repo basename if empty
if name == "" {
name = filepath.Base(modulePath)
}
// Determine the target module path for go.mod
targetModulePath := name
if opts.ModulePath != "" {
targetModulePath = opts.ModulePath
}
// 1. Determine version to use
var targetVersion string
if specifiedVersion != "" {
// User specified version
targetVersion = specifiedVersion
mlog.Printf("Using specified version: %s", targetVersion)
} else if opts.SelectVersion {
// Interactive version selection
mlog.Print("Fetching available versions...")
versionInfo, err := GetModuleVersions(ctx, modulePath)
if err != nil {
mlog.Printf("Failed to get versions: %v", err)
return err
}
targetVersion, err = SelectVersion(ctx, versionInfo.Versions, modulePath)
if err != nil {
mlog.Printf("Version selection failed: %v", err)
return err
}
} else {
// Default: use latest version
mlog.Print("Fetching latest version...")
latest, err := GetLatestVersion(ctx, modulePath)
if err != nil {
mlog.Printf("Failed to get latest version, will try @latest tag: %v", err)
targetVersion = "latest"
} else {
targetVersion = latest
mlog.Printf("Latest version: %s", targetVersion)
}
}
// 2. Download Template with determined version
repoWithVersion := modulePath + "@" + targetVersion
srcDir, err := downloadTemplate(ctx, repoWithVersion)
if err != nil {
mlog.Printf("Download failed: %v", err)
return err
}
mlog.Debugf("Template located at: %s", srcDir)
// 3. Generate Project
if err := generateProject(ctx, srcDir, name, modulePath, targetModulePath); err != nil {
mlog.Printf("Generation failed: %v", err)
return err
}
// 4. Handle dependencies
var projectDir string
if name == "." {
projectDir = gfile.Pwd()
} else {
projectDir = filepath.Join(gfile.Pwd(), name)
}
if opts.UpgradeDeps {
// Upgrade all dependencies to latest
if err := upgradeDependencies(ctx, projectDir); err != nil {
mlog.Printf("Failed to upgrade dependencies: %v", err)
}
} else {
// Default: just tidy dependencies
if err := tidyDependencies(ctx, projectDir); err != nil {
mlog.Printf("Failed to tidy dependencies: %v", err)
}
}
return nil
}
// processGitSubdir handles git subdirectory download via sparse checkout
func processGitSubdir(ctx context.Context, repo, name string, opts *ProcessOptions) error {
mlog.Print("Detected subdirectory URL, using git sparse checkout...")
// Check if git is available
gitVersion, err := CheckGitEnv(ctx)
if err != nil {
mlog.Printf("Git is required for subdirectory templates: %v", err)
return err
}
mlog.Printf("Git available (%s)", gitVersion)
// Download via git sparse checkout
srcDir, gitInfo, err := downloadGitSubdir(ctx, repo)
if err != nil {
mlog.Printf("Git download failed: %v", err)
return err
}
// Clean up temp directory after generation
// The temp dir is parent of parent of srcDir (tempDir/repo/subpath)
tempDir := filepath.Dir(filepath.Dir(srcDir))
if tempDir != "" && gfile.Exists(tempDir) && gstr.Contains(tempDir, "gf-init-git") {
defer func() {
if err := gfile.Remove(tempDir); err != nil {
mlog.Debugf("Failed to remove temp directory %s: %v", tempDir, err)
} else {
mlog.Debugf("Cleaned up temp directory: %s", tempDir)
}
}()
}
// Default name to subpath basename if empty
if name == "" {
name = filepath.Base(gitInfo.SubPath)
}
// Get original module name from go.mod (might be "main" or something else)
oldModule := GetModuleNameFromGoMod(srcDir)
if oldModule == "" {
// Fallback: construct from git info
oldModule = gitInfo.Host + "/" + gitInfo.Owner + "/" + gitInfo.Repo + "/" + gitInfo.SubPath
}
// Determine the target module path for go.mod
targetModulePath := name
if opts.ModulePath != "" {
targetModulePath = opts.ModulePath
}
mlog.Debugf("Template located at: %s", srcDir)
mlog.Debugf("Original module: %s", oldModule)
// Generate Project
if err := generateProject(ctx, srcDir, name, oldModule, targetModulePath); err != nil {
mlog.Printf("Generation failed: %v", err)
return err
}
// Handle dependencies
var projectDir string
if name == "." {
projectDir = gfile.Pwd()
} else {
projectDir = filepath.Join(gfile.Pwd(), name)
}
if opts.UpgradeDeps {
// Upgrade all dependencies to latest
if err := upgradeDependencies(ctx, projectDir); err != nil {
mlog.Printf("Failed to upgrade dependencies: %v", err)
}
} else {
// Default: just tidy dependencies
if err := tidyDependencies(ctx, projectDir); err != nil {
mlog.Printf("Failed to tidy dependencies: %v", err)
}
}
return nil
}

View File

@ -0,0 +1,126 @@
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package geninit
import (
"bytes"
"context"
"go/ast"
"go/parser"
"go/printer"
"go/token"
"os"
"path/filepath"
"strings"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
)
// ASTReplacer handles import path replacement using Go AST
type ASTReplacer struct {
oldModule string
newModule string
fset *token.FileSet
}
// NewASTReplacer creates a new AST-based import replacer
func NewASTReplacer(oldModule, newModule string) *ASTReplacer {
return &ASTReplacer{
oldModule: oldModule,
newModule: newModule,
fset: token.NewFileSet(),
}
}
// ReplaceInFile replaces import paths in a single Go file
func (r *ASTReplacer) ReplaceInFile(ctx context.Context, filePath string) error {
// Read file content
content := gfile.GetContents(filePath)
if content == "" {
return nil
}
// Parse the file
file, err := parser.ParseFile(r.fset, filePath, content, parser.ParseComments)
if err != nil {
mlog.Debugf("Failed to parse %s: %v", filePath, err)
return nil // Skip files that can't be parsed
}
// Track if any changes were made
changed := false
// Traverse and modify imports
ast.Inspect(file, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.ImportSpec:
if x.Path != nil {
importPath := strings.Trim(x.Path.Value, `"`)
if strings.HasPrefix(importPath, r.oldModule) {
// Replace only the leading module prefix for clarity and correctness.
newPath := r.newModule + strings.TrimPrefix(importPath, r.oldModule)
x.Path.Value = `"` + newPath + `"`
changed = true
mlog.Debugf("Replaced import: %s -> %s in %s", importPath, newPath, filePath)
}
}
}
return true
})
if !changed {
return nil
}
// Write back to file
var buf bytes.Buffer
// Use default printer configuration to match gofmt output
cfg := &printer.Config{}
if err := cfg.Fprint(&buf, r.fset, file); err != nil {
return err
}
return gfile.PutContents(filePath, buf.String())
}
// ReplaceInDir replaces import paths in all Go files in a directory (recursively)
func (r *ASTReplacer) ReplaceInDir(ctx context.Context, dir string) error {
mlog.Printf("Replacing imports: %s -> %s", r.oldModule, r.newModule)
// Find all .go files
files, err := findGoFiles(dir)
if err != nil {
return err
}
for _, file := range files {
if err := r.ReplaceInFile(ctx, file); err != nil {
mlog.Printf("Failed to process %s: %v", file, err)
}
}
return nil
}
// findGoFiles recursively finds all .go files in a directory
func findGoFiles(dir string) ([]string, error) {
var files []string
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && strings.HasSuffix(path, ".go") {
files = append(files, path)
}
return nil
})
return files, err
}

View File

@ -0,0 +1,111 @@
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package geninit
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
)
// downloadTemplate fetches the remote repository using go get
func downloadTemplate(ctx context.Context, repo string) (string, error) {
// 1. Create a temporary directory workspace
tempDir := gfile.Temp("gf-init-cli")
if tempDir == "" {
return "", fmt.Errorf("failed to create temporary directory")
}
if err := gfile.Mkdir(tempDir); err != nil {
return "", err
}
defer func() {
if err := gfile.Remove(tempDir); err != nil {
mlog.Debugf("Failed to remove temp directory %s: %v", tempDir, err)
}
}() // Clean up the temp workspace
mlog.Debugf("Using temp workspace: %s", tempDir)
// 2. Initialize a temp go module to perform go get
// We run commands inside the temp directory
if err := runCmd(ctx, tempDir, "go", "mod", "init", "temp"); err != nil {
return "", err
}
// 3. Run go get <repo>
// Try different version strategies: original -> @latest -> @master
moduleName := repo
if gstr.Contains(repo, "@") {
moduleName = gstr.Split(repo, "@")[0]
}
var downloadErrs []string
versionsToTry := []string{repo}
if !gstr.Contains(repo, "@") {
versionsToTry = append(versionsToTry, repo+"@latest", repo+"@master")
}
var successRepo string
for _, tryRepo := range versionsToTry {
mlog.Printf("Downloading template %s...", tryRepo)
if err := runCmd(ctx, tempDir, "go", "get", tryRepo); err == nil {
successRepo = tryRepo
break
} else {
downloadErrs = append(downloadErrs, fmt.Sprintf("%s: %v", tryRepo, err))
mlog.Debugf("Failed to download %s, trying next...", tryRepo)
}
}
if successRepo == "" {
errMsg := "all download attempts failed"
if len(downloadErrs) > 0 {
errMsg = strings.Join(downloadErrs, "; ")
}
return "", fmt.Errorf("failed to download repo %s: %s", repo, errMsg)
}
// 4. Find the local path using go list -m -json <repo>
listCmd := exec.CommandContext(ctx, "go", "list", "-m", "-json", moduleName)
listCmd.Dir = tempDir
output, err := listCmd.Output()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
return "", fmt.Errorf("go list failed: %s", string(exitErr.Stderr))
}
return "", fmt.Errorf("failed to locate module path: %w", err)
}
var modInfo struct {
Dir string `json:"Dir"`
}
if err := json.Unmarshal(output, &modInfo); err != nil {
return "", fmt.Errorf("failed to parse go list output: %w", err)
}
if modInfo.Dir == "" {
return "", fmt.Errorf("module directory not found for %s", repo)
}
return modInfo.Dir, nil
}
func runCmd(ctx context.Context, dir string, name string, args ...string) error {
cmd := exec.CommandContext(ctx, name, args...)
cmd.Dir = dir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}

View File

@ -0,0 +1,90 @@
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package geninit
import (
"context"
"encoding/json"
"fmt"
"os/exec"
"strings"
"github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
)
// GoEnv represents Go environment variables
type GoEnv struct {
GOVERSION string `json:"GOVERSION"`
GOROOT string `json:"GOROOT"`
GOPATH string `json:"GOPATH"`
GOMODCACHE string `json:"GOMODCACHE"`
GOPROXY string `json:"GOPROXY"`
GO111MODULE string `json:"GO111MODULE"`
}
// CheckGoEnv verifies Go is installed and properly configured
func CheckGoEnv(ctx context.Context) (*GoEnv, error) {
// 1. Check if go binary exists
goPath, err := exec.LookPath("go")
if err != nil {
return nil, fmt.Errorf("go is not installed or not in PATH: %w", err)
}
mlog.Debugf("Found go binary at: %s", goPath)
// 2. Get go env as JSON
cmd := exec.CommandContext(ctx, "go", "env", "-json")
output, err := cmd.Output()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
return nil, fmt.Errorf("go env failed: %s", string(exitErr.Stderr))
}
return nil, fmt.Errorf("failed to run go env: %w", err)
}
// 3. Parse JSON output
var env GoEnv
if err := json.Unmarshal(output, &env); err != nil {
return nil, fmt.Errorf("failed to parse go env output: %w", err)
}
// 4. Validate critical environment variables
if env.GOROOT == "" {
return nil, fmt.Errorf("GOROOT is not set")
}
if env.GOMODCACHE == "" && env.GOPATH == "" {
return nil, fmt.Errorf("neither GOMODCACHE nor GOPATH is set")
}
mlog.Debugf("Go Version: %s", env.GOVERSION)
mlog.Debugf("GOROOT: %s", env.GOROOT)
mlog.Debugf("GOMODCACHE: %s", env.GOMODCACHE)
mlog.Debugf("GOPROXY: %s", env.GOPROXY)
return &env, nil
}
// CheckGitEnv verifies Git is installed and returns its version
func CheckGitEnv(ctx context.Context) (string, error) {
// 1. Check if git binary exists
gitPath, err := exec.LookPath("git")
if err != nil {
return "", fmt.Errorf("git is not installed or not in PATH: %w", err)
}
mlog.Debugf("Found git binary at: %s", gitPath)
// 2. Get git version
cmd := exec.CommandContext(ctx, "git", "--version")
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to get git version: %w", err)
}
version := strings.TrimSpace(string(output))
mlog.Debugf("Git version: %s", version)
return version, nil
}

View File

@ -0,0 +1,110 @@
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package geninit
import (
"context"
"fmt"
"path/filepath"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
)
// generateProject copies the template to the destination and performs cleanup
// oldModule: original module path from template
// newModule: target module path for go.mod (can be different from project name)
func generateProject(ctx context.Context, srcPath, name, oldModule, newModule string) error {
pwd := gfile.Pwd()
dstPath := filepath.Join(pwd, name)
if name == "." {
dstPath = pwd
}
if gfile.Exists(dstPath) && !gfile.IsEmpty(dstPath) {
return fmt.Errorf("target directory %s is not empty", dstPath)
}
mlog.Printf("Generating project in %s...", dstPath)
// 1. Copy files
if err := gfile.Copy(srcPath, dstPath); err != nil {
return err
}
// 2. Clean up .git directory
gitDir := filepath.Join(dstPath, ".git")
if gfile.Exists(gitDir) {
if err := gfile.Remove(gitDir); err != nil {
mlog.Debugf("Failed to remove .git directory: %v", err)
}
}
// 3. Clean up go.work and go.work.sum (workspace files should not be in generated project)
for _, workFile := range []string{"go.work", "go.work.sum"} {
workPath := filepath.Join(dstPath, workFile)
if gfile.Exists(workPath) {
if err := gfile.Remove(workPath); err != nil {
mlog.Printf("Failed to remove %s: %v", workFile, err)
} else {
mlog.Debugf("Removed %s", workFile)
}
}
}
// 4. Update go.mod module name
goModPath := filepath.Join(dstPath, "go.mod")
if gfile.Exists(goModPath) {
content := gfile.GetContents(goModPath)
lines := gstr.Split(content, "\n")
if len(lines) > 0 && gstr.HasPrefix(lines[0], "module ") {
lines[0] = "module " + newModule
newContent := gstr.Join(lines, "\n")
if err := gfile.PutContents(goModPath, newContent); err != nil {
mlog.Printf("Failed to update go.mod: %v", err)
}
}
}
// 5. Use AST to replace import paths in all Go files
if oldModule != "" && oldModule != newModule {
replacer := NewASTReplacer(oldModule, newModule)
if err := replacer.ReplaceInDir(ctx, dstPath); err != nil {
return fmt.Errorf("failed to replace imports: %w", err)
}
}
mlog.Print("Project generated successfully!")
return nil
}
// tidyDependencies runs go mod tidy in the project directory
func tidyDependencies(ctx context.Context, projectDir string) error {
mlog.Print("Tidying dependencies (go mod tidy)...")
if err := runCmd(ctx, projectDir, "go", "mod", "tidy"); err != nil {
return fmt.Errorf("go mod tidy failed: %w", err)
}
mlog.Print("Dependencies tidied successfully!")
return nil
}
// upgradeDependencies runs go get -u ./... to upgrade all dependencies to latest
func upgradeDependencies(ctx context.Context, projectDir string) error {
mlog.Print("Upgrading dependencies to latest (go get -u ./...)...")
if err := runCmd(ctx, projectDir, "go", "get", "-u", "./..."); err != nil {
return fmt.Errorf("go get -u failed: %w", err)
}
// Run tidy again after upgrade
if err := runCmd(ctx, projectDir, "go", "mod", "tidy"); err != nil {
return fmt.Errorf("go mod tidy after upgrade failed: %w", err)
}
mlog.Print("Dependencies upgraded successfully!")
return nil
}

View File

@ -0,0 +1,241 @@
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package geninit
import (
"context"
"fmt"
"path/filepath"
"strings"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
)
// GitRepoInfo holds parsed git repository information
type GitRepoInfo struct {
Host string // e.g., github.com
Owner string // e.g., gogf
Repo string // e.g., examples
Branch string // e.g., main (default: main)
SubPath string // e.g., httpserver/jwt
CloneURL string // e.g., https://github.com/gogf/examples.git
}
// ParseGitURL parses a git URL and extracts repository info
// Supports formats:
// - github.com/owner/repo
// - github.com/owner/repo/subdir/path
// - github.com/owner/repo/tree/branch/subdir/path (from GitHub web URL)
func ParseGitURL(url string) (*GitRepoInfo, error) {
// Remove protocol prefix if present
url = strings.TrimPrefix(url, "https://")
url = strings.TrimPrefix(url, "http://")
url = strings.TrimSuffix(url, ".git")
// Remove version suffix like @v1.0.0
if idx := strings.Index(url, "@"); idx != -1 {
url = url[:idx]
}
parts := strings.Split(url, "/")
if len(parts) < 3 {
return nil, fmt.Errorf("invalid git URL: %s", url)
}
info := &GitRepoInfo{
Host: parts[0],
Owner: parts[1],
Repo: parts[2],
Branch: "main", // default branch
}
// Check for /tree/branch/ pattern (GitHub web URL)
if len(parts) > 4 && parts[3] == "tree" {
info.Branch = parts[4]
if len(parts) > 5 {
info.SubPath = strings.Join(parts[5:], "/")
}
} else if len(parts) > 3 {
// Direct subpath: github.com/owner/repo/subdir/path
info.SubPath = strings.Join(parts[3:], "/")
}
info.CloneURL = fmt.Sprintf("https://%s/%s/%s.git", info.Host, info.Owner, info.Repo)
return info, nil
}
// IsSubdirRepo checks if the URL points to a subdirectory of a repository
// Returns false for Go module paths (which may have /vN suffix or nested module paths)
// Note: This uses heuristics that may have false positives/negatives in edge cases
func IsSubdirRepo(url string) bool {
info, err := ParseGitURL(url)
if err != nil {
return false
}
if info.SubPath == "" {
return false
}
// Check if this looks like a Go module path rather than a git subdirectory
// Go modules can have nested paths like github.com/owner/repo/cmd/tool/v2
// We should try to resolve it as a Go module first
// If the URL can be resolved as a Go module, it's not a subdir repo
// We use a heuristic: check if the full path looks like a valid Go module
// by checking if it ends with /vN (major version) or contains common module patterns
// Remove version suffix for checking
cleanURL := url
if before, _, ok := strings.Cut(url, "@"); ok {
cleanURL = before
}
// Check if the path ends with /vN (Go module major version)
parts := strings.Split(cleanURL, "/")
if len(parts) > 0 {
lastPart := parts[len(parts)-1]
if len(lastPart) >= 2 && lastPart[0] == 'v' {
// Check if it's v2, v3, etc.
if _, err := fmt.Sscanf(lastPart, "v%d", new(int)); err == nil {
// This looks like a Go module with major version suffix
// It could be either a versioned module or a subdir ending in vN
// We'll treat it as a Go module and let go get handle it
mlog.Debugf("URL %s detected as Go module (ends with /vN)", url)
return false
}
}
}
// For GitHub URLs, check if the subpath could be a nested Go module
// Common patterns: cmd/*, internal/*, pkg/*, contrib/*
subPathParts := strings.Split(info.SubPath, "/")
if len(subPathParts) > 0 {
firstPart := subPathParts[0]
// These are common Go module nesting patterns
if firstPart == "cmd" || firstPart == "contrib" || firstPart == "tools" {
// This might be a nested Go module, not a simple subdirectory
// Let go get try first
mlog.Debugf("URL %s detected as Go module (starts with common pattern)", url)
return false
}
}
mlog.Debugf("URL %s detected as git subdirectory", url)
return true
}
// downloadGitSubdir downloads a subdirectory from a git repository using sparse checkout
func downloadGitSubdir(ctx context.Context, repoURL string) (string, *GitRepoInfo, error) {
info, err := ParseGitURL(repoURL)
if err != nil {
return "", nil, err
}
if info.SubPath == "" {
return "", nil, fmt.Errorf("not a subdirectory URL: %s", repoURL)
}
// Create temp directory for clone
tempDir := gfile.Temp("gf-init-git")
if tempDir == "" {
return "", nil, fmt.Errorf("failed to create temporary directory")
}
if err := gfile.Mkdir(tempDir); err != nil {
return "", nil, err
}
cloneDir := filepath.Join(tempDir, info.Repo)
mlog.Debugf("Using git temp workspace: %s", tempDir)
mlog.Printf("Cloning %s (sparse checkout: %s)...", info.CloneURL, info.SubPath)
// 1. Clone with no checkout, filter, and sparse
if err := runCmd(ctx, tempDir, "git", "clone", "--filter=blob:none", "--no-checkout", "--sparse", info.CloneURL); err != nil {
// Fallback: try without filter for older git versions
mlog.Debugf("Sparse clone failed, trying full clone...")
if err := gfile.Remove(cloneDir); err != nil {
mlog.Debugf("Failed to remove clone directory: %v", err)
}
if err := runCmd(ctx, tempDir, "git", "clone", "--no-checkout", info.CloneURL); err != nil {
if err := gfile.Remove(tempDir); err != nil {
mlog.Debugf("Failed to remove temp directory: %v", err)
}
return "", nil, fmt.Errorf("git clone failed: %w", err)
}
}
// 2. Set sparse-checkout to the subpath
if err := runCmd(ctx, cloneDir, "git", "sparse-checkout", "set", info.SubPath); err != nil {
// Fallback for older git: use sparse-checkout init + set
mlog.Debugf("sparse-checkout set failed, trying legacy method...")
if err := runCmd(ctx, cloneDir, "git", "sparse-checkout", "init", "--cone"); err != nil {
if err := gfile.Remove(tempDir); err != nil {
mlog.Debugf("Failed to remove temp directory: %v", err)
}
return "", nil, fmt.Errorf("git sparse-checkout init (legacy) failed: %w", err)
}
if err := runCmd(ctx, cloneDir, "git", "sparse-checkout", "set", info.SubPath); err != nil {
if err := gfile.Remove(tempDir); err != nil {
mlog.Debugf("Failed to remove temp directory: %v", err)
}
return "", nil, fmt.Errorf("git sparse-checkout set (legacy) failed: %w", err)
}
}
// 3. Checkout the branch
if err := runCmd(ctx, cloneDir, "git", "checkout", info.Branch); err != nil {
// Try master if main fails
if info.Branch == "main" {
mlog.Debugf("Branch 'main' not found, trying 'master'...")
info.Branch = "master"
if err := runCmd(ctx, cloneDir, "git", "checkout", "master"); err != nil {
if err := gfile.Remove(tempDir); err != nil {
mlog.Debugf("Failed to remove temp directory: %v", err)
}
return "", nil, fmt.Errorf("git checkout failed: %w", err)
}
} else {
if err := gfile.Remove(tempDir); err != nil {
mlog.Debugf("Failed to remove temp directory: %v", err)
}
return "", nil, fmt.Errorf("git checkout failed: %w", err)
}
}
// Return the path to the subdirectory
subDirPath := filepath.Join(cloneDir, info.SubPath)
if !gfile.Exists(subDirPath) {
if err := gfile.Remove(tempDir); err != nil {
mlog.Debugf("Failed to remove temp directory: %v", err)
}
return "", nil, fmt.Errorf("subdirectory not found: %s", info.SubPath)
}
mlog.Debugf("Subdirectory located at: %s", subDirPath)
return subDirPath, info, nil
}
// GetModuleNameFromGoMod reads module name from go.mod file
func GetModuleNameFromGoMod(dir string) string {
goModPath := filepath.Join(dir, "go.mod")
if !gfile.Exists(goModPath) {
return ""
}
content := gfile.GetContents(goModPath)
lines := gstr.Split(content, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if after, ok := strings.CutPrefix(line, "module "); ok {
return strings.TrimSpace(after)
}
}
return ""
}

View File

@ -0,0 +1,99 @@
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package geninit
import (
"bufio"
"context"
"fmt"
"os"
"strconv"
"strings"
"github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
)
// SelectVersion prompts user to select a version interactively
func SelectVersion(ctx context.Context, versions []string, modulePath string) (string, error) {
if len(versions) == 0 {
return "", fmt.Errorf("no versions available for selection")
}
if len(versions) == 1 {
mlog.Printf("Only one version available: %s", versions[0])
return versions[0], nil
}
// Display available versions
fmt.Printf("\nAvailable versions for %s:\n", modulePath)
fmt.Println(strings.Repeat("-", 40))
// Show versions with index (newest first)
maxDisplay := 20 // Limit display to avoid overwhelming output
displayCount := len(versions)
if displayCount > maxDisplay {
displayCount = maxDisplay
}
for i := 0; i < displayCount; i++ {
marker := ""
if i == 0 {
marker = " (latest)"
}
fmt.Printf(" [%2d] %s%s\n", i+1, versions[i], marker)
}
if len(versions) > maxDisplay {
fmt.Printf(" ... and %d more versions\n", len(versions)-maxDisplay)
}
fmt.Println(strings.Repeat("-", 40))
// Prompt for selection
reader := bufio.NewReader(os.Stdin)
for {
fmt.Printf("Select version [1-%d] or enter version string (default: 1 for latest): ", displayCount)
input, err := reader.ReadString('\n')
if err != nil {
return "", fmt.Errorf("failed to read input: %w", err)
}
input = strings.TrimSpace(input)
// Default to latest
if input == "" {
fmt.Printf("Selected: %s (latest)\n", versions[0])
return versions[0], nil
}
// Try parsing as number first
idx, err := strconv.Atoi(input)
if err == nil {
// Valid number - check if in range
if idx >= 1 && idx <= len(versions) {
// Allow selection from all versions, not just displayed ones
selected := versions[idx-1]
fmt.Printf("Selected: %s\n", selected)
return selected, nil
} else if idx < 1 || idx > displayCount {
fmt.Printf("Invalid selection. Please enter a number between 1 and %d, or type a version string.\n", displayCount)
continue
}
} else {
// Try matching the input as a version string (e.g., "v1.2.3")
for _, v := range versions {
if v == input || strings.Contains(v, input) {
fmt.Printf("Selected: %s\n", v)
return v, nil
}
}
fmt.Printf("Version '%s' not found. Please select by number or type a valid version string.\n", input)
continue
}
}
}

View File

@ -0,0 +1,138 @@
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package geninit
import (
"context"
"encoding/json"
"fmt"
"os/exec"
"sort"
"strings"
"golang.org/x/mod/semver"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
)
// VersionInfo contains module version information
type VersionInfo struct {
Module string `json:"module"`
Versions []string `json:"versions"`
Latest string `json:"latest"`
}
// GetModuleVersions fetches available versions for a Go module
func GetModuleVersions(ctx context.Context, modulePath string) (*VersionInfo, error) {
// Create a temporary directory for go list
tempDir := gfile.Temp("gf-init-version")
if tempDir == "" {
return nil, fmt.Errorf("failed to create temporary directory for go list")
}
if err := gfile.Mkdir(tempDir); err != nil {
return nil, err
}
defer func() {
if err := gfile.Remove(tempDir); err != nil {
mlog.Debugf("Failed to remove temp directory: %v", err)
}
}()
// Initialize a temp go module
if err := runCmd(ctx, tempDir, "go", "mod", "init", "temp"); err != nil {
return nil, fmt.Errorf("failed to init temp module: %w", err)
}
// Get versions using go list -m -versions
cmd := exec.CommandContext(ctx, "go", "list", "-m", "-versions", modulePath)
cmd.Dir = tempDir
output, err := cmd.Output()
if err != nil {
// Try with @latest to see if module exists
mlog.Debugf("go list -versions failed, trying @latest: %v", err)
return getLatestOnly(ctx, tempDir, modulePath)
}
// Parse output: "module/path v1.0.0 v1.1.0 v2.0.0"
parts := strings.Fields(strings.TrimSpace(string(output)))
if len(parts) < 1 {
return nil, fmt.Errorf("no version information found for %s", modulePath)
}
info := &VersionInfo{
Module: parts[0],
Versions: []string{},
}
if len(parts) > 1 {
info.Versions = parts[1:]
// Sort versions in descending order (newest first)
sort.Slice(info.Versions, func(i, j int) bool {
return semver.Compare(info.Versions[i], info.Versions[j]) > 0
})
info.Latest = info.Versions[0]
}
// If no tagged versions, try to get latest
if len(info.Versions) == 0 {
latestInfo, err := getLatestOnly(ctx, tempDir, modulePath)
if err != nil {
return nil, err
}
info.Latest = latestInfo.Latest
if latestInfo.Latest != "" {
info.Versions = []string{latestInfo.Latest}
}
}
return info, nil
}
// getLatestOnly gets only the latest version when go list -versions fails
func getLatestOnly(ctx context.Context, tempDir, modulePath string) (*VersionInfo, error) {
// Try go list -m modulePath@latest
cmd := exec.CommandContext(ctx, "go", "list", "-m", "-json", modulePath+"@latest")
cmd.Dir = tempDir
output, err := cmd.Output()
if err != nil {
// Try without @latest
cmd = exec.CommandContext(ctx, "go", "list", "-m", "-json", modulePath)
cmd.Dir = tempDir
output, err = cmd.Output()
if err != nil {
return nil, fmt.Errorf("failed to get module info for %s: %w", modulePath, err)
}
}
var modInfo struct {
Path string `json:"Path"`
Version string `json:"Version"`
}
if err := json.Unmarshal(output, &modInfo); err != nil {
return nil, fmt.Errorf("failed to parse module info: %w", err)
}
return &VersionInfo{
Module: modInfo.Path,
Versions: []string{modInfo.Version},
Latest: modInfo.Version,
}, nil
}
// GetLatestVersion returns the latest version of a module
func GetLatestVersion(ctx context.Context, modulePath string) (string, error) {
info, err := GetModuleVersions(ctx, modulePath)
if err != nil {
return "", err
}
if info.Latest == "" {
return "", fmt.Errorf("no version found for %s", modulePath)
}
return info.Latest, nil
}

View File

@ -1,13 +1,10 @@
package testdata
import (
"fmt"
"testing"
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/test/gtest"
"github.com/gogf/gf/v2/util/guid"
)

View File

@ -4,7 +4,7 @@ go 1.23.0
require (
github.com/apolloconfig/agollo/v4 v4.3.1
github.com/gogf/gf/v2 v2.9.6
github.com/gogf/gf/v2 v2.9.7
)
require (

View File

@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/config/consul/v2
go 1.23.0
require (
github.com/gogf/gf/v2 v2.9.6
github.com/gogf/gf/v2 v2.9.7
github.com/hashicorp/consul/api v1.24.0
github.com/hashicorp/go-cleanhttp v0.5.2
)

View File

@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/config/kubecm/v2
go 1.24.0
require (
github.com/gogf/gf/v2 v2.9.6
github.com/gogf/gf/v2 v2.9.7
k8s.io/api v0.33.4
k8s.io/apimachinery v0.33.4
k8s.io/client-go v0.33.4

View File

@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/config/nacos/v2
go 1.23.0
require (
github.com/gogf/gf/v2 v2.9.6
github.com/gogf/gf/v2 v2.9.7
github.com/nacos-group/nacos-sdk-go/v2 v2.3.3
)

View File

@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/config/polaris/v2
go 1.23.0
require (
github.com/gogf/gf/v2 v2.9.6
github.com/gogf/gf/v2 v2.9.7
github.com/polarismesh/polaris-go v1.6.1
)

View File

@ -9,14 +9,19 @@ Let's take `mysql` for example.
```shell
go get github.com/gogf/gf/contrib/drivers/mysql/v2@latest
# Easy to copy
# Easy for copying:
go get github.com/gogf/gf/contrib/drivers/clickhouse/v2@latest
go get github.com/gogf/gf/contrib/drivers/dm/v2@latest
go get github.com/gogf/gf/contrib/drivers/gaussdb/v2@latest
go get github.com/gogf/gf/contrib/drivers/mariadb/v2@latest
go get github.com/gogf/gf/contrib/drivers/mssql/v2@latest
go get github.com/gogf/gf/contrib/drivers/oceanbase/v2@latest
go get github.com/gogf/gf/contrib/drivers/oracle/v2@latest
go get github.com/gogf/gf/contrib/drivers/pgsql/v2@latest
go get github.com/gogf/gf/contrib/drivers/sqlite/v2@latest
go get github.com/gogf/gf/contrib/drivers/sqlitecgo/v2@latest
go get github.com/gogf/gf/contrib/drivers/tidb/v2@latest
```
Choose and import the driver to your project:
@ -43,12 +48,36 @@ func main() {
## Supported Drivers
### MySQL/MariaDB/TiDB/OceanBase
### MySQL
```go
import _ "github.com/gogf/gf/contrib/drivers/mysql/v2"
```
### MariaDB
```go
import _ "github.com/gogf/gf/contrib/drivers/mariadb/v2"
```
### TiDB
```go
import _ "github.com/gogf/gf/contrib/drivers/tidb/v2"
```
### OceanBase
```go
import _ "github.com/gogf/gf/contrib/drivers/oceanbase/v2"
```
### GaussDB
```go
import _ "github.com/gogf/gf/contrib/drivers/gaussdb/v2"
```
### SQLite
```go
@ -57,7 +86,7 @@ import _ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
#### cgo version
When the target is a 32-bit Windows system, the cgo version needs to be used.
When the target is a `32-bit` Windows system, the `cgo` version needs to be used.
```go
import _ "github.com/gogf/gf/contrib/drivers/sqlitecgo/v2"
@ -77,8 +106,10 @@ import _ "github.com/gogf/gf/contrib/drivers/mssql/v2"
Note:
- `InsertIgnore` returns error if there is no primary key or unique index submitted with record.
- It supports server version >= `SQL Server2005`
- It ONLY supports datetime2 and datetimeoffset types for auto handling created_at/updated_at/deleted_at columns, because datetime type does not support microseconds precision when column value is passed as string.
- It ONLY supports `datetime2` and `datetimeoffset` types for auto handling created_at/updated_at/deleted_at columns,
because datetime type does not support microseconds precision when column value is passed as string.
### Oracle
@ -88,8 +119,8 @@ import _ "github.com/gogf/gf/contrib/drivers/oracle/v2"
Note:
- It does not support `Replace` features.
- It does not support `LastInsertId`.
- `InsertIgnore` returns error if there is no primary key or unique index submitted with record.
### ClickHouse
@ -99,7 +130,7 @@ import _ "github.com/gogf/gf/contrib/drivers/clickhouse/v2"
Note:
- It does not support `InsertIgnore/InsertGetId` features.
- It does not support `InsertIgnore/InsertAndGetId` features.
- It does not support `Save/Replace` features.
- It does not support `Transaction` feature.
- It does not support `RowsAffected` feature.
@ -110,6 +141,10 @@ Note:
import _ "github.com/gogf/gf/contrib/drivers/dm/v2"
```
Note:
- `InsertIgnore` returns error if there is no primary key or unique index submitted with record.
## Custom Drivers
It's quick and easy, please refer to current driver source.

View File

@ -4,7 +4,7 @@ go 1.23.0
require (
github.com/ClickHouse/clickhouse-go/v2 v2.0.15
github.com/gogf/gf/v2 v2.9.6
github.com/gogf/gf/v2 v2.9.7
github.com/google/uuid v1.6.0
github.com/shopspring/decimal v1.3.1
)

View File

@ -37,6 +37,12 @@ func (d *Driver) DoInsert(
return d.doInsertIgnore(ctx, link, table, list, option)
default:
// DM database supports IDENTITY auto-increment columns natively.
// The driver automatically returns LastInsertId through sql.Result.
//
// Note: DM IDENTITY columns cannot accept explicit ID values unless
// IDENTITY_INSERT is enabled. When using tables with IDENTITY columns,
// avoid providing explicit ID values in the data.
return d.Core.DoInsert(ctx, link, table, list, option)
}
}
@ -94,6 +100,7 @@ func (d *Driver) doMergeInsert(
table,
)
}
// TODO consider composite primary keys.
conflictKeys = primaryKeys
}

View File

@ -7,7 +7,6 @@
package dm_test
import (
"database/sql"
"fmt"
"strings"
"testing"
@ -509,124 +508,3 @@ func Test_Empty_Slice_Argument(t *testing.T) {
t.Assert(len(result), 0)
})
}
func TestModelSave(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type User struct {
Id int
AccountName string
AttrIndex int
}
var (
user User
count int
result sql.Result
err error
)
result, err = db.Model(table).Data(g.Map{
"id": 1,
"accountName": "ac1",
"attrIndex": 100,
}).OnConflict("id").Save()
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 1)
err = db.Model(table).Scan(&user)
t.AssertNil(err)
t.Assert(user.Id, 1)
t.Assert(user.AccountName, "ac1")
t.Assert(user.AttrIndex, 100)
_, err = db.Model(table).Data(g.Map{
"id": 1,
"accountName": "ac2",
"attrIndex": 200,
}).OnConflict("id").Save()
t.AssertNil(err)
err = db.Model(table).Scan(&user)
t.AssertNil(err)
t.Assert(user.AccountName, "ac2")
t.Assert(user.AttrIndex, 200)
count, err = db.Model(table).Count()
t.AssertNil(err)
t.Assert(count, 1)
})
}
func TestModelInsert(t *testing.T) {
// g.Model.insert not lost default not null column
table := "A_tables"
createInitTable(table)
gtest.C(t, func(t *gtest.T) {
i := 200
data := User{
ID: int64(i),
AccountName: fmt.Sprintf(`A%dtwo`, i),
PwdReset: 0,
AttrIndex: 99,
CreatedTime: time.Now(),
UpdatedTime: time.Now(),
}
// _, err := db.Schema(TestDBName).Model(table).Data(data).Insert()
_, err := db.Model(table).Insert(&data)
gtest.AssertNil(err)
})
gtest.C(t, func(t *gtest.T) {
i := 201
data := User{
ID: int64(i),
AccountName: fmt.Sprintf(`A%dtwoONE`, i),
PwdReset: 1,
CreatedTime: time.Now(),
AttrIndex: 98,
UpdatedTime: time.Now(),
}
// _, err := db.Schema(TestDBName).Model(table).Data(data).Insert()
_, err := db.Model(table).Data(&data).Insert()
gtest.AssertNil(err)
})
}
func Test_Model_InsertIgnore(t *testing.T) {
table := createInitTable()
defer dropTable(table)
// db.SetDebug(true)
gtest.C(t, func(t *gtest.T) {
data := User{
ID: int64(666),
AccountName: fmt.Sprintf(`name_%d`, 666),
PwdReset: 0,
AttrIndex: 99,
CreatedTime: time.Now(),
UpdatedTime: time.Now(),
}
_, err := db.Model(table).Data(data).Insert()
t.AssertNil(err)
})
gtest.C(t, func(t *gtest.T) {
data := User{
ID: int64(666),
AccountName: fmt.Sprintf(`name_%d`, 777),
PwdReset: 0,
AttrIndex: 99,
CreatedTime: time.Now(),
UpdatedTime: time.Now(),
}
_, err := db.Model(table).Data(data).InsertIgnore()
t.AssertNil(err)
one, err := db.Model(table).Where("id", 666).One()
t.AssertNil(err)
t.Assert(one["ACCOUNT_NAME"].String(), "name_666")
})
}

View File

@ -220,3 +220,33 @@ func createInitTables(len int) []string {
}
return tables
}
// createTableWithIdentity creates a table with IDENTITY column for LastInsertId testing
func createTableWithIdentity(table ...string) (name string) {
if len(table) > 0 {
name = table[0]
} else {
name = fmt.Sprintf("random_%d", gtime.Timestamp())
}
dropTable(name)
if _, err := db.Exec(ctx, fmt.Sprintf(`
CREATE TABLE "%s"
(
"ID" BIGINT IDENTITY(1, 1) NOT NULL,
"ACCOUNT_NAME" VARCHAR(128) DEFAULT '' NOT NULL COMMENT 'Account Name',
"PWD_RESET" TINYINT DEFAULT 0 NOT NULL,
"ENABLED" INT DEFAULT 1 NOT NULL,
"DELETED" INT DEFAULT 0 NOT NULL,
"ATTR_INDEX" INT DEFAULT 0 ,
"CREATED_BY" VARCHAR(32) DEFAULT '' NOT NULL,
"CREATED_TIME" TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP() NOT NULL,
"UPDATED_BY" VARCHAR(32) DEFAULT '' NOT NULL,
"UPDATED_TIME" TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP() NOT NULL,
NOT CLUSTER PRIMARY KEY("ID")) STORAGE(ON "MAIN", CLUSTERBTR) ;
`, name)); err != nil {
gtest.Fatal(err)
}
return
}

View File

@ -0,0 +1,185 @@
// Copyright 2019 gf Author(https://github.com/gogf/gf). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package dm_test
import (
"database/sql"
"fmt"
"testing"
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/test/gtest"
)
func Test_Model_Save(t *testing.T) {
table := createTableWithIdentity()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type User struct {
Id int
AccountName string
AttrIndex int
}
var (
user User
count int
result sql.Result
err error
)
// First insert: let IDENTITY auto-generate ID - use Insert() instead of Save()
// because Save() requires a primary key in the data for conflict detection
result, err = db.Model(table).Data(g.Map{
"accountName": "ac1",
"attrIndex": 100,
}).Insert()
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 1)
err = db.Model(table).Scan(&user)
t.AssertNil(err)
t.AssertGT(user.Id, 0) // ID should be auto-generated
t.Assert(user.AccountName, "ac1")
t.Assert(user.AttrIndex, 100)
// Second save: update the existing record using the generated ID
_, err = db.Model(table).Data(g.Map{
"id": user.Id,
"accountName": "ac2",
"attrIndex": 200,
}).OnConflict("id").Save()
t.AssertNil(err)
err = db.Model(table).Scan(&user)
t.AssertNil(err)
t.Assert(user.AccountName, "ac2")
t.Assert(user.AttrIndex, 200)
_, err = db.Model(table).Data(g.Map{
"id": user.Id,
"accountName": "ac2",
"attrIndex": 2000,
}).Save()
t.AssertNil(err)
err = db.Model(table).Scan(&user)
t.AssertNil(err)
t.Assert(user.AccountName, "ac2")
t.Assert(user.AttrIndex, 2000)
count, err = db.Model(table).Count()
t.AssertNil(err)
t.Assert(count, 1)
})
}
func Test_Model_Insert(t *testing.T) {
// g.Model.insert not lost default not null column
table := "A_tables"
createInitTable(table)
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
i := 200
data := User{
ID: int64(i),
AccountName: fmt.Sprintf(`A%dtwo`, i),
PwdReset: 0,
AttrIndex: 99,
CreatedTime: time.Now(),
UpdatedTime: time.Now(),
}
result, err := db.Model(table).Insert(&data)
gtest.AssertNil(err)
n, err := result.RowsAffected()
gtest.AssertNil(err)
gtest.Assert(n, 1)
})
gtest.C(t, func(t *gtest.T) {
i := 201
data := User{
ID: int64(i),
AccountName: fmt.Sprintf(`A%dtwoONE`, i),
PwdReset: 1,
CreatedTime: time.Now(),
AttrIndex: 98,
UpdatedTime: time.Now(),
}
result, err := db.Model(table).Data(&data).Insert()
gtest.AssertNil(err)
n, err := result.RowsAffected()
gtest.AssertNil(err)
gtest.Assert(n, 1)
})
}
func Test_Model_InsertIgnore(t *testing.T) {
table := createInitTable()
defer dropTable(table)
// db.SetDebug(true)
gtest.C(t, func(t *gtest.T) {
data := g.Map{
"id": 1,
"account_name": fmt.Sprintf(`name_%d`, 777),
"pwd_reset": 0,
"attr_index": 777,
"created_time": gtime.Now(),
}
_, err := db.Model(table).Data(data).InsertIgnore()
t.AssertNil(err)
one, err := db.Model(table).WherePri(1).One()
t.AssertNil(err)
t.Assert(one["ACCOUNT_NAME"].String(), "name_1")
count, err := db.Model(table).Count()
t.AssertNil(err)
t.Assert(count, TableSize)
})
gtest.C(t, func(t *gtest.T) {
data := g.Map{
// "id": 1,
"account_name": fmt.Sprintf(`name_%d`, 777),
"pwd_reset": 0,
"attr_index": 777,
"created_time": gtime.Now(),
}
_, err := db.Model(table).Data(data).InsertIgnore()
t.AssertNE(err, nil)
count, err := db.Model(table).Count()
t.AssertNil(err)
t.Assert(count, TableSize)
})
}
func Test_Model_InsertAndGetId(t *testing.T) {
table := createTableWithIdentity()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
data := g.Map{
// "id": 1,
"account_name": fmt.Sprintf(`name_%d`, 1),
"pwd_reset": 0,
"attr_index": 1,
"created_time": gtime.Now(),
}
lastId, err := db.Model(table).Data(data).InsertAndGetId()
t.AssertNil(err)
t.AssertGT(lastId, 0)
})
}

View File

@ -6,7 +6,7 @@ replace github.com/gogf/gf/v2 => ../../../
require (
gitee.com/chunanyong/dm v1.8.12
github.com/gogf/gf/v2 v2.9.6
github.com/gogf/gf/v2 v2.9.7
)
require (

View File

@ -0,0 +1,50 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
// Package gaussdb implements gdb.Driver, which supports operations for database GaussDB.
package gaussdb
import (
_ "gitee.com/opengauss/openGauss-connector-go-pq"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/os/gctx"
)
// Driver is the driver for GaussDB database.
type Driver struct {
*gdb.Core
}
const (
internalPrimaryKeyInCtx gctx.StrKey = "primary_key"
defaultSchema string = "public"
quoteChar string = `"`
)
func init() {
if err := gdb.Register(`gaussdb`, New()); err != nil {
panic(err)
}
}
// New create and returns a driver that implements gdb.Driver, which supports operations for PostgreSql.
func New() gdb.Driver {
return &Driver{}
}
// New creates and returns a database object for postgresql.
// It implements the interface of gdb.Driver for extra database driver installation.
func (d *Driver) New(core *gdb.Core, node *gdb.ConfigNode) (gdb.DB, error) {
return &Driver{
Core: core,
}, nil
}
// GetChars returns the security char for this type of database.
func (d *Driver) GetChars() (charLeft string, charRight string) {
return quoteChar, quoteChar
}

View File

@ -0,0 +1,257 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gaussdb
import (
"context"
"reflect"
"strings"
"github.com/google/uuid"
"github.com/lib/pq"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
)
// ConvertValueForField converts value to database acceptable value.
func (d *Driver) ConvertValueForField(ctx context.Context, fieldType string, fieldValue any) (any, error) {
if g.IsNil(fieldValue) {
return d.Core.ConvertValueForField(ctx, fieldType, fieldValue)
}
var fieldValueKind = reflect.TypeOf(fieldValue).Kind()
if fieldValueKind == reflect.Slice {
// For pgsql, json or jsonb require '[]'
if !gstr.Contains(fieldType, "json") {
fieldValue = gstr.ReplaceByMap(gconv.String(fieldValue),
map[string]string{
"[": "{",
"]": "}",
},
)
}
}
return d.Core.ConvertValueForField(ctx, fieldType, fieldValue)
}
// CheckLocalTypeForField checks and returns corresponding local golang type for given db type.
// The parameter `fieldType` is in lower case, like:
// `int2`, `int4`, `int8`, `_int2`, `_int4`, `_int8`, `_float4`, `_float8`, etc.
//
// PostgreSQL type mapping:
//
// | PostgreSQL Type | Local Go Type |
// |------------------------------|---------------|
// | int2, int4 | int |
// | int8 | int64 |
// | uuid | uuid.UUID |
// | _int2, _int4 | []int32 | // Note: pq package does not provide Int16Array; int32 is used for compatibility
// | _int8 | []int64 |
// | _float4 | []float32 |
// | _float8 | []float64 |
// | _bool | []bool |
// | _varchar, _text | []string |
// | _char, _bpchar | []string |
// | _numeric, _decimal, _money | []float64 |
// | _bytea | [][]byte |
// | _uuid | []uuid.UUID |
func (d *Driver) CheckLocalTypeForField(ctx context.Context, fieldType string, fieldValue any) (gdb.LocalType, error) {
var typeName string
match, _ := gregex.MatchString(`(.+?)\((.+)\)`, fieldType)
if len(match) == 3 {
typeName = gstr.Trim(match[1])
} else {
typeName = fieldType
}
typeName = strings.ToLower(typeName)
switch typeName {
case "int2", "int4":
return gdb.LocalTypeInt, nil
case "int8":
return gdb.LocalTypeInt64, nil
case "uuid":
return gdb.LocalTypeUUID, nil
case "_int2", "_int4":
return gdb.LocalTypeInt32Slice, nil
case "_int8":
return gdb.LocalTypeInt64Slice, nil
case "_float4":
return gdb.LocalTypeFloat32Slice, nil
case "_float8":
return gdb.LocalTypeFloat64Slice, nil
case "_bool":
return gdb.LocalTypeBoolSlice, nil
case "_varchar", "_text", "_char", "_bpchar":
return gdb.LocalTypeStringSlice, nil
case "_uuid":
return gdb.LocalTypeUUIDSlice, nil
case "_numeric", "_decimal", "_money":
return gdb.LocalTypeFloat64Slice, nil
case "_bytea":
return gdb.LocalTypeBytesSlice, nil
default:
return d.Core.CheckLocalTypeForField(ctx, fieldType, fieldValue)
}
}
// ConvertValueForLocal converts value to local Golang type of value according field type name from database.
// The parameter `fieldType` is in lower case, like:
// `int2`, `int4`, `int8`, `_int2`, `_int4`, `_int8`, `uuid`, `_uuid`, etc.
//
// See: https://www.postgresql.org/docs/current/datatype.html
//
// PostgreSQL type mapping:
//
// | PostgreSQL Type | SQL Type | pq Type | Go Type |
// |-----------------|--------------------------------|-----------------|-------------|
// | int2 | int2, smallint | - | int |
// | int4 | int4, integer | - | int |
// | int8 | int8, bigint, bigserial | - | int64 |
// | uuid | uuid | - | uuid.UUID |
// | _int2 | int2[], smallint[] | pq.Int32Array | []int32 |
// | _int4 | int4[], integer[] | pq.Int32Array | []int32 |
// | _int8 | int8[], bigint[] | pq.Int64Array | []int64 |
// | _float4 | float4[], real[] | pq.Float32Array | []float32 |
// | _float8 | float8[], double precision[] | pq.Float64Array | []float64 |
// | _bool | boolean[], bool[] | pq.BoolArray | []bool |
// | _varchar | varchar[], character varying[] | pq.StringArray | []string |
// | _text | text[] | pq.StringArray | []string |
// | _char, _bpchar | char[], character[] | pq.StringArray | []string |
// | _numeric | numeric[] | pq.Float64Array | []float64 |
// | _decimal | decimal[] | pq.Float64Array | []float64 |
// | _money | money[] | pq.Float64Array | []float64 |
// | _bytea | bytea[] | pq.ByteaArray | [][]byte |
// | _uuid | uuid[] | pq.StringArray | []uuid.UUID |
//
// Note: PostgreSQL also supports these array types but they are not yet mapped:
// - _date (date[]), _timestamp (timestamp[]), _timestamptz (timestamptz[])
// - _jsonb (jsonb[]), _json (json[])
func (d *Driver) ConvertValueForLocal(ctx context.Context, fieldType string, fieldValue any) (any, error) {
typeName, _ := gregex.ReplaceString(`\(.+\)`, "", fieldType)
typeName = strings.ToLower(typeName)
// Basic types are mostly handled by Core layer, only handle array types here
switch typeName {
// []int32
case "_int2", "_int4":
var result pq.Int32Array
if err := result.Scan(fieldValue); err != nil {
return nil, err
}
return []int32(result), nil
// []int64
case "_int8":
var result pq.Int64Array
if err := result.Scan(fieldValue); err != nil {
return nil, err
}
return []int64(result), nil
// []float32
case "_float4":
var result pq.Float32Array
if err := result.Scan(fieldValue); err != nil {
return nil, err
}
return []float32(result), nil
// []float64
case "_float8":
var result pq.Float64Array
if err := result.Scan(fieldValue); err != nil {
return nil, err
}
return []float64(result), nil
// []bool
case "_bool":
var result pq.BoolArray
if err := result.Scan(fieldValue); err != nil {
return nil, err
}
return []bool(result), nil
// []string
case "_varchar", "_text", "_char", "_bpchar":
var result pq.StringArray
if err := result.Scan(fieldValue); err != nil {
return nil, err
}
return []string(result), nil
// uuid.UUID
case "uuid":
var uuidStr string
switch v := fieldValue.(type) {
case []byte:
uuidStr = string(v)
case string:
uuidStr = v
default:
uuidStr = gconv.String(fieldValue)
}
result, err := uuid.Parse(uuidStr)
if err != nil {
return nil, err
}
return result, nil
// []uuid.UUID
case "_uuid":
var strArray pq.StringArray
if err := strArray.Scan(fieldValue); err != nil {
return nil, err
}
result := make([]uuid.UUID, len(strArray))
for i, s := range strArray {
parsed, err := uuid.Parse(s)
if err != nil {
return nil, err
}
result[i] = parsed
}
return result, nil
// []float64
case "_numeric", "_decimal", "_money":
var result pq.Float64Array
if err := result.Scan(fieldValue); err != nil {
return nil, err
}
return []float64(result), nil
// [][]byte
case "_bytea":
var result pq.ByteaArray
if err := result.Scan(fieldValue); err != nil {
return nil, err
}
return [][]byte(result), nil
default:
return d.Core.ConvertValueForLocal(ctx, fieldType, fieldValue)
}
}

View File

@ -0,0 +1,110 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gaussdb
import (
"context"
"database/sql"
"fmt"
"strings"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
)
// DoExec commits the sql string and its arguments to underlying driver
// through given link object and returns the execution result.
func (d *Driver) DoExec(ctx context.Context, link gdb.Link, sql string, args ...any) (result sql.Result, err error) {
var (
isUseCoreDoExec bool = false // Check whether the default method needs to be used
primaryKey string = ""
pkField gdb.TableField
)
// Transaction checks.
if link == nil {
if tx := gdb.TXFromCtx(ctx, d.GetGroup()); tx != nil {
// Firstly, check and retrieve transaction link from context.
link = tx
} else if link, err = d.MasterLink(); err != nil {
// Or else it creates one from master node.
return nil, err
}
} else if !link.IsTransaction() {
// If current link is not transaction link, it checks and retrieves transaction from context.
if tx := gdb.TXFromCtx(ctx, d.GetGroup()); tx != nil {
link = tx
}
}
// Check if it is an insert operation with primary key.
if value := ctx.Value(internalPrimaryKeyInCtx); value != nil {
var ok bool
pkField, ok = value.(gdb.TableField)
if !ok {
isUseCoreDoExec = true
}
} else {
isUseCoreDoExec = true
}
// check if it is an insert operation.
if !isUseCoreDoExec && pkField.Name != "" && strings.Contains(sql, "INSERT INTO") {
primaryKey = pkField.Name
sql += fmt.Sprintf(` RETURNING "%s"`, primaryKey)
} else {
// use default DoExec
return d.Core.DoExec(ctx, link, sql, args...)
}
// Only the insert operation with primary key can execute the following code
// Sql filtering.
sql, args = d.FormatSqlBeforeExecuting(sql, args)
sql, args, err = d.DoFilter(ctx, link, sql, args)
if err != nil {
return nil, err
}
// Link execution.
var out gdb.DoCommitOutput
out, err = d.DoCommit(ctx, gdb.DoCommitInput{
Link: link,
Sql: sql,
Args: args,
Stmt: nil,
Type: gdb.SqlTypeQueryContext,
IsTransaction: link.IsTransaction(),
})
if err != nil {
return nil, err
}
affected := len(out.Records)
if affected > 0 {
if !strings.Contains(pkField.Type, "int") {
return Result{
affected: int64(affected),
lastInsertId: 0,
lastInsertIdError: gerror.NewCodef(
gcode.CodeNotSupported,
"LastInsertId is not supported by primary key type: %s", pkField.Type),
}, nil
}
if out.Records[affected-1][primaryKey] != nil {
lastInsertId := out.Records[affected-1][primaryKey].Int64()
return Result{
affected: int64(affected),
lastInsertId: lastInsertId,
}, nil
}
}
return Result{}, nil
}

View File

@ -0,0 +1,62 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gaussdb
import (
"context"
"fmt"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
)
// DoFilter deals with the sql string before commits it to underlying sql driver.
func (d *Driver) DoFilter(
ctx context.Context, link gdb.Link, sql string, args []any,
) (newSql string, newArgs []any, err error) {
var index int
// Convert placeholder char '?' to string "$x".
newSql, err = gregex.ReplaceStringFunc(`\?`, sql, func(s string) string {
index++
return fmt.Sprintf(`$%d`, index)
})
if err != nil {
return "", nil, err
}
// Handle pgsql jsonb feature support, which contains place-holder char '?'.
// Refer:
// https://github.com/gogf/gf/issues/1537
// https://www.postgresql.org/docs/12/functions-json.html
newSql, err = gregex.ReplaceStringFuncMatch(
`(::jsonb([^\w\d]*)\$\d)`,
newSql,
func(match []string) string {
return fmt.Sprintf(`::jsonb%s?`, match[2])
},
)
if err != nil {
return "", nil, err
}
newSql, err = gregex.ReplaceString(` LIMIT (\d+),\s*(\d+)`, ` LIMIT $2 OFFSET $1`, newSql)
if err != nil {
return "", nil, err
}
// Handle gaussdb INSERT IGNORE.
// The IGNORE keyword is removed here, converting the statement to a regular INSERT.
// The actual "ignore" behavior (i.e., skipping inserts that would violate constraints)
// is implemented at the DoInsert level by checking for existence before inserting.
if gstr.HasPrefix(newSql, gdb.InsertOperationIgnore) {
// Remove the IGNORE operation prefix and keep as regular INSERT
newSql = "INSERT" + newSql[len(gdb.InsertOperationIgnore):]
}
newArgs = args
return d.Core.DoFilter(ctx, link, newSql, newArgs)
}

View File

@ -0,0 +1,535 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gaussdb
import (
"context"
"database/sql"
"fmt"
"strings"
"github.com/gogf/gf/v2/container/gset"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
)
// DoInsert inserts or updates data for given table.
// The list parameter must contain at least one record, which was previously validated.
func (d *Driver) DoInsert(
ctx context.Context,
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
) (result sql.Result, err error) {
switch option.InsertOption {
case gdb.InsertOptionSave:
return d.doSave(ctx, link, table, list, option)
case gdb.InsertOptionReplace:
// Treat Replace as Save operation
return d.doSave(ctx, link, table, list, option)
// GaussDB does not support InsertIgnore with ON CONFLICT, use MERGE instead
case gdb.InsertOptionIgnore:
return d.doInsertIgnore(ctx, link, table, list, option)
case gdb.InsertOptionDefault:
// Get table fields to retrieve the primary key TableField object (not just the name)
// because DoExec needs the `TableField.Type` to determine if LastInsertId is supported.
tableFields, err := d.GetCore().GetDB().TableFields(ctx, table)
if err == nil {
for _, field := range tableFields {
if strings.EqualFold(field.Key, "pri") {
pkField := *field
ctx = context.WithValue(ctx, internalPrimaryKeyInCtx, pkField)
break
}
}
}
default:
}
return d.Core.DoInsert(ctx, link, table, list, option)
}
// doSave implements upsert operation using MERGE statement for GaussDB.
func (d *Driver) doSave(ctx context.Context,
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
) (result sql.Result, err error) {
return d.doMergeInsert(ctx, link, table, list, option, true)
}
// doInsertIgnore implements INSERT IGNORE operation using MERGE statement for GaussDB.
// It only inserts records when there's no conflict on primary/unique keys.
func (d *Driver) doInsertIgnore(ctx context.Context,
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
) (result sql.Result, err error) {
return d.doMergeInsert(ctx, link, table, list, option, false)
}
// doUpdateThenInsert handles upsert when conflict keys need to be updated.
// GaussDB MERGE cannot update columns in ON clause, so we use UPDATE + INSERT instead.
func (d *Driver) doUpdateThenInsert(ctx context.Context,
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
) (result sql.Result, err error) {
charL, charR := d.GetChars()
var (
batchResult = new(gdb.SqlResult)
totalAffected int64
)
for _, data := range list {
// Build UPDATE statement
var (
updateFields []string
updateValues []any
whereFields []string
whereValues []any
valueIndex = 1
)
// Process OnDuplicateMap to build UPDATE SET clause
for updateKey, updateValue := range option.OnDuplicateMap {
keyWithChar := charL + updateKey + charR
switch v := updateValue.(type) {
case gdb.Raw, *gdb.Raw:
rawStr := fmt.Sprintf("%v", v)
rawStr = strings.ReplaceAll(rawStr, "EXCLUDED.", "")
rawStr = strings.ReplaceAll(rawStr, "EXCLUDED ", "")
updateFields = append(updateFields, fmt.Sprintf("%s = %s", keyWithChar, rawStr))
case gdb.Counter, *gdb.Counter:
var counter gdb.Counter
if c, ok := v.(gdb.Counter); ok {
counter = c
} else if c, ok := v.(*gdb.Counter); ok {
counter = *c
}
operator := "+"
columnVal := counter.Value
if columnVal < 0 {
operator = "-"
columnVal = -columnVal
}
fieldWithChar := charL + counter.Field + charR
// For UPDATE statement, use the data value instead of referencing another column
if dataValue, ok := data[counter.Field]; ok {
updateFields = append(updateFields, fmt.Sprintf("%s = $%d %s %v", keyWithChar, valueIndex, operator, columnVal))
updateValues = append(updateValues, dataValue)
valueIndex++
} else {
updateFields = append(updateFields, fmt.Sprintf("%s = %s %s %v", keyWithChar, fieldWithChar, operator, columnVal))
}
default:
// Map value to another field name or use the value from data
valueStr := gconv.String(updateValue)
if dataValue, ok := data[valueStr]; ok {
updateFields = append(updateFields, fmt.Sprintf("%s = $%d", keyWithChar, valueIndex))
updateValues = append(updateValues, dataValue)
valueIndex++
} else {
updateFields = append(updateFields, fmt.Sprintf("%s = $%d", keyWithChar, valueIndex))
updateValues = append(updateValues, updateValue)
valueIndex++
}
}
}
// Build WHERE clause using OnConflict keys
for _, conflictKey := range option.OnConflict {
if dataValue, ok := data[conflictKey]; ok {
keyWithChar := charL + conflictKey + charR
whereFields = append(whereFields, fmt.Sprintf("%s = $%d", keyWithChar, valueIndex))
whereValues = append(whereValues, dataValue)
valueIndex++
}
}
if len(updateFields) > 0 && len(whereFields) > 0 {
updateSQL := fmt.Sprintf("UPDATE %s SET %s WHERE %s",
table,
strings.Join(updateFields, ", "),
strings.Join(whereFields, " AND "),
)
updateResult, updateErr := d.DoExec(ctx, link, updateSQL, append(updateValues, whereValues...)...)
if updateErr != nil {
return nil, updateErr
}
affected, _ := updateResult.RowsAffected()
if affected > 0 {
// UPDATE successful
totalAffected += affected
continue
}
}
// If UPDATE affected 0 rows, do INSERT
var (
insertKeys []string
insertHolders []string
insertValues []any
insertIndex = 1
)
for key, value := range data {
keyWithChar := charL + key + charR
insertKeys = append(insertKeys, keyWithChar)
insertHolders = append(insertHolders, fmt.Sprintf("$%d", insertIndex))
insertValues = append(insertValues, value)
insertIndex++
}
insertSQL := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)",
table,
strings.Join(insertKeys, ", "),
strings.Join(insertHolders, ", "),
)
insertResult, insertErr := d.DoExec(ctx, link, insertSQL, insertValues...)
if insertErr != nil {
// Ignore duplicate key errors (race condition: another transaction inserted between our UPDATE and INSERT)
if strings.Contains(insertErr.Error(), "duplicate key") ||
strings.Contains(insertErr.Error(), "unique constraint") {
continue
}
return nil, insertErr
}
affected, _ := insertResult.RowsAffected()
totalAffected += affected
}
batchResult.Result = &gdb.SqlResult{}
batchResult.Affected = totalAffected
return batchResult, nil
}
// doMergeInsert implements MERGE-based insert operations for GaussDB.
// When withUpdate is true, it performs upsert (insert or update).
// When withUpdate is false, it performs insert ignore (insert only when no conflict).
func (d *Driver) doMergeInsert(
ctx context.Context,
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption, withUpdate bool,
) (result sql.Result, err error) {
// For batch operations (multiple records), process each record individually
if len(list) > 1 {
var (
batchResult = new(gdb.SqlResult)
totalAffected int64
)
for _, record := range list {
singleResult, singleErr := d.doMergeInsert(ctx, link, table, gdb.List{record}, option, withUpdate)
if singleErr != nil {
return nil, singleErr
}
if n, _ := singleResult.RowsAffected(); n > 0 {
totalAffected += n
}
}
batchResult.Result = &gdb.SqlResult{}
batchResult.Affected = totalAffected
return batchResult, nil
}
// Check if OnDuplicateMap contains conflict keys
// GaussDB MERGE statement cannot update columns used in ON clause
// If user wants to update conflict keys, we need to use a different approach
if withUpdate && len(option.OnDuplicateMap) > 0 && len(option.OnConflict) > 0 {
conflictKeySet := gset.NewStrSetFrom(option.OnConflict)
hasConflictKeyUpdate := false
for updateKey := range option.OnDuplicateMap {
if conflictKeySet.Contains(strings.ToLower(updateKey)) ||
conflictKeySet.Contains(strings.ToUpper(updateKey)) ||
conflictKeySet.Contains(updateKey) {
hasConflictKeyUpdate = true
break
}
}
if hasConflictKeyUpdate {
// Use UPDATE + INSERT approach when conflict keys need to be updated
return d.doUpdateThenInsert(ctx, link, table, list, option)
}
}
// If OnConflict is not specified, automatically get the primary key of the table
conflictKeys := option.OnConflict
if len(conflictKeys) == 0 {
primaryKeys, err := d.Core.GetPrimaryKeys(ctx, table)
if err != nil {
return nil, gerror.WrapCode(
gcode.CodeInternalError,
err,
`failed to get primary keys for table`,
)
}
foundPrimaryKey := false
for _, primaryKey := range primaryKeys {
for dataKey := range list[0] {
if strings.EqualFold(dataKey, primaryKey) {
foundPrimaryKey = true
break
}
}
if foundPrimaryKey {
break
}
}
if !foundPrimaryKey {
// For InsertIgnore without primary key, try normal insert and ignore duplicate errors
// For Save/Replace, primary key is required
if !withUpdate {
result, err := d.Core.DoInsert(ctx, link, table, list, option)
if err != nil {
// Ignore duplicate key errors for InsertIgnore
if strings.Contains(err.Error(), "duplicate key") ||
strings.Contains(err.Error(), "unique constraint") {
return result, nil
}
return result, err
}
return result, nil
}
return nil, gerror.NewCodef(
gcode.CodeMissingParameter,
`Replace/Save operation requires conflict detection: `+
`either specify OnConflict() columns or ensure table '%s' has a primary key in the data`,
table,
)
}
// TODO consider composite primary keys.
conflictKeys = primaryKeys
}
var (
one = list[0]
oneLen = len(one)
charL, charR = d.GetChars()
conflictKeySet = gset.New(false)
// queryHolders: Handle data with Holder that need to be merged
// queryValues: Handle data that need to be merged
// insertKeys: Handle valid keys that need to be inserted
// insertValues: Handle values that need to be inserted
// updateValues: Handle values that need to be updated (only when withUpdate=true)
queryHolders = make([]string, oneLen)
queryValues = make([]any, oneLen)
insertKeys = make([]string, oneLen)
insertValues = make([]string, oneLen)
updateValues []string
)
// conflictKeys slice type conv to set type
for _, conflictKey := range conflictKeys {
conflictKeySet.Add(strings.ToUpper(conflictKey))
}
index := 0
for key, value := range one {
keyWithChar := charL + key + charR
queryHolders[index] = fmt.Sprintf("$%d AS %s", index+1, keyWithChar)
queryValues[index] = value
insertKeys[index] = keyWithChar
insertValues[index] = fmt.Sprintf("T2.%s", keyWithChar)
index++
}
// Build updateValues only when withUpdate is true
if withUpdate {
// Check if OnDuplicateStr or OnDuplicateMap is specified for custom update logic
if option.OnDuplicateStr != "" {
// Parse OnDuplicateStr (e.g., "field1,field2" or "field1, field2")
fields := gstr.SplitAndTrim(option.OnDuplicateStr, ",")
for _, field := range fields {
fieldWithChar := charL + field + charR
updateValues = append(
updateValues,
fmt.Sprintf(`T1.%s = T2.%s`, fieldWithChar, fieldWithChar),
)
}
} else if len(option.OnDuplicateMap) > 0 {
// Use OnDuplicateMap for custom update mapping
for updateKey, updateValue := range option.OnDuplicateMap {
// Skip conflict keys - they cannot be updated in MERGE
if conflictKeySet.Contains(strings.ToUpper(updateKey)) {
continue
}
keyWithChar := charL + updateKey + charR
switch v := updateValue.(type) {
case gdb.Raw, *gdb.Raw:
// Raw SQL expression
// Replace EXCLUDED (PostgreSQL ON CONFLICT syntax) with T2 (MERGE syntax)
rawStr := fmt.Sprintf("%v", v)
rawStr = strings.ReplaceAll(rawStr, "EXCLUDED.", "T2.")
rawStr = strings.ReplaceAll(rawStr, "EXCLUDED ", "T2 ")
updateValues = append(
updateValues,
fmt.Sprintf(`T1.%s = %s`, keyWithChar, rawStr),
)
case gdb.Counter, *gdb.Counter:
// Counter operation
var counter gdb.Counter
if c, ok := v.(gdb.Counter); ok {
counter = c
} else if c, ok := v.(*gdb.Counter); ok {
counter = *c
}
operator := "+"
columnVal := counter.Value
if columnVal < 0 {
operator = "-"
columnVal = -columnVal
}
fieldWithChar := charL + counter.Field + charR
updateValues = append(
updateValues,
fmt.Sprintf(`T1.%s = T2.%s %s %v`, keyWithChar, fieldWithChar, operator, columnVal),
)
default:
// Map value to another field name
valueStr := gconv.String(updateValue)
valueWithChar := charL + valueStr + charR
updateValues = append(
updateValues,
fmt.Sprintf(`T1.%s = T2.%s`, keyWithChar, valueWithChar),
)
}
}
} else {
// Default: update all fields except conflict keys and soft created fields
for key := range one {
if conflictKeySet.Contains(strings.ToUpper(key)) || d.Core.IsSoftCreatedFieldName(key) {
continue
}
keyWithChar := charL + key + charR
updateValues = append(
updateValues,
fmt.Sprintf(`T1.%s = T2.%s`, keyWithChar, keyWithChar),
)
}
}
}
var (
batchResult = new(gdb.SqlResult)
sqlStr string
)
// For InsertIgnore (withUpdate=false), we need to check if record exists first
if !withUpdate {
// Build WHERE clause to check if record exists
var whereConditions []string
var checkValues []any
checkIndex := 1
for _, key := range conflictKeys {
if value, ok := one[key]; ok {
keyWithChar := charL + key + charR
whereConditions = append(whereConditions, fmt.Sprintf("%s = $%d", keyWithChar, checkIndex))
checkValues = append(checkValues, value)
checkIndex++
}
}
whereClause := strings.Join(whereConditions, " AND ")
// Check if record exists
checkSQL := fmt.Sprintf("SELECT 1 FROM %s WHERE %s LIMIT 1", table, whereClause)
checkResult, checkErr := d.DoQuery(ctx, link, checkSQL, checkValues...)
if checkErr != nil {
return nil, checkErr
}
// If record exists, return result with 0 affected rows
if len(checkResult) > 0 {
batchResult.Result = &gdb.SqlResult{}
batchResult.Affected = 0
return batchResult, nil
}
// Record doesn't exist, proceed with insert
// For InsertIgnore, we just do a simple INSERT (no MERGE needed since we checked it doesn't exist)
var insertSQL strings.Builder
insertSQL.WriteString(fmt.Sprintf("INSERT INTO %s (", table))
insertSQL.WriteString(strings.Join(insertKeys, ","))
insertSQL.WriteString(") VALUES (")
for i := range insertKeys {
if i > 0 {
insertSQL.WriteString(",")
}
insertSQL.WriteString(fmt.Sprintf("$%d", i+1))
}
insertSQL.WriteString(")")
r, err := d.DoExec(ctx, link, insertSQL.String(), queryValues...)
if err != nil {
return r, err
}
if n, err := r.RowsAffected(); err != nil {
return r, err
} else {
batchResult.Result = r
batchResult.Affected = n
}
return batchResult, nil
}
// For Save/Replace (withUpdate=true), use MERGE
sqlStr = parseSqlForMerge(table, queryHolders, insertKeys, insertValues, updateValues, conflictKeys, charL, charR)
r, err := d.DoExec(ctx, link, sqlStr, queryValues...)
if err != nil {
return r, err
}
// GaussDB's MERGE statement may not return correct RowsAffected
// Workaround: If RowsAffected returns 0 despite a successful MERGE, we manually set it to 1.
if n, err := r.RowsAffected(); err != nil {
return r, err
} else {
batchResult.Result = r
// If RowsAffected returns 0, manually set to 1 for MERGE operations
if n == 0 {
batchResult.Affected = 1
} else {
batchResult.Affected += n
}
}
return batchResult, nil
}
// parseSqlForMerge generates MERGE statement for GaussDB.
// When updateValues is empty, it only inserts (INSERT IGNORE behavior).
// When updateValues is provided, it performs upsert (INSERT or UPDATE).
// Examples:
// - INSERT IGNORE: MERGE INTO table T1 USING (...) T2 ON (...) WHEN NOT MATCHED THEN INSERT(...) VALUES (...)
// - UPSERT: MERGE INTO table T1 USING (...) T2 ON (...) WHEN NOT MATCHED THEN INSERT(...) VALUES (...) WHEN MATCHED THEN UPDATE SET ...
func parseSqlForMerge(table string,
queryHolders, insertKeys, insertValues, updateValues, duplicateKey []string, charL, charR string,
) (sqlStr string) {
var (
intoStr = fmt.Sprintf("MERGE INTO %s AS T1", table)
usingStr = fmt.Sprintf("USING (SELECT %s) AS T2", strings.Join(queryHolders, ","))
onStr string
insertStr = fmt.Sprintf(
"WHEN NOT MATCHED THEN INSERT (%s) VALUES (%s)",
strings.Join(insertKeys, ","),
strings.Join(insertValues, ","),
)
updateStr string
)
// Build ON condition
var onConditions []string
for _, key := range duplicateKey {
keyWithChar := charL + key + charR
onConditions = append(onConditions, fmt.Sprintf("T1.%s = T2.%s", keyWithChar, keyWithChar))
}
onStr = "ON (" + strings.Join(onConditions, " AND ") + ")"
// Build UPDATE clause only when updateValues is provided
if len(updateValues) > 0 {
updateStr = fmt.Sprintf(" WHEN MATCHED THEN UPDATE SET %s", strings.Join(updateValues, ","))
}
sqlStr = fmt.Sprintf("%s %s %s %s%s", intoStr, usingStr, onStr, insertStr, updateStr)
return
}

View File

@ -0,0 +1,69 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gaussdb
import (
"database/sql"
"fmt"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/text/gstr"
)
// Open creates and returns an underlying sql.DB object for pgsql.
// https://pkg.go.dev/github.com/lib/pq
func (d *Driver) Open(config *gdb.ConfigNode) (db *sql.DB, err error) {
source, err := configNodeToSource(config)
if err != nil {
return nil, err
}
underlyingDriverName := "postgres"
if db, err = sql.Open(underlyingDriverName, source); err != nil {
err = gerror.WrapCodef(
gcode.CodeDbOperationError, err,
`sql.Open failed for driver "%s" by source "%s"`, underlyingDriverName, source,
)
return nil, err
}
return
}
func configNodeToSource(config *gdb.ConfigNode) (string, error) {
var source string
source = fmt.Sprintf(
"user=%s password='%s' host=%s sslmode=disable",
config.User, config.Pass, config.Host,
)
if config.Port != "" {
source = fmt.Sprintf("%s port=%s", source, config.Port)
}
if config.Name != "" {
source = fmt.Sprintf("%s dbname=%s", source, config.Name)
}
if config.Namespace != "" {
source = fmt.Sprintf("%s search_path=%s", source, config.Namespace)
}
if config.Timezone != "" {
source = fmt.Sprintf("%s timezone=%s", source, config.Timezone)
}
if config.Extra != "" {
extraMap, err := gstr.Parse(config.Extra)
if err != nil {
return "", gerror.WrapCodef(
gcode.CodeInvalidParameter,
err,
`invalid extra configuration: %s`, config.Extra,
)
}
for k, v := range extraMap {
source += fmt.Sprintf(` %s=%s`, k, v)
}
}
return source, nil
}

View File

@ -0,0 +1,12 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gaussdb
// OrderRandomFunction returns the SQL function for random ordering.
func (d *Driver) OrderRandomFunction() string {
return "RANDOM()"
}

View File

@ -0,0 +1,24 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gaussdb
import "database/sql"
type Result struct {
sql.Result
affected int64
lastInsertId int64
lastInsertIdError error
}
func (pgr Result) RowsAffected() (int64, error) {
return pgr.affected, nil
}
func (pgr Result) LastInsertId() (int64, error) {
return pgr.lastInsertId, pgr.lastInsertIdError
}

View File

@ -0,0 +1,108 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gaussdb
import (
"context"
"fmt"
"github.com/gogf/gf/v2/database/gdb"
)
var (
tableFieldsSqlTmp = `
SELECT
a.attname AS field,
t.typname AS type,
a.attnotnull AS null,
(CASE WHEN d.contype = 'p' THEN 'pri' WHEN d.contype = 'u' THEN 'uni' ELSE '' END) AS key,
ic.column_default AS default_value,
b.description AS comment,
COALESCE(character_maximum_length, numeric_precision, -1) AS length,
numeric_scale AS scale
FROM pg_attribute a
LEFT JOIN pg_class c ON a.attrelid = c.oid
LEFT JOIN pg_constraint d ON d.conrelid = c.oid AND a.attnum = d.conkey[1]
LEFT JOIN pg_description b ON a.attrelid = b.objoid AND a.attnum = b.objsubid
LEFT JOIN pg_type t ON a.atttypid = t.oid
LEFT JOIN information_schema.columns ic ON ic.column_name = a.attname AND ic.table_name = c.relname
WHERE c.oid = '%s'::regclass
AND a.attisdropped IS FALSE
AND a.attnum > 0
ORDER BY a.attnum`
)
func init() {
var err error
tableFieldsSqlTmp, err = gdb.FormatMultiLineSqlToSingle(tableFieldsSqlTmp)
if err != nil {
panic(err)
}
}
// TableFields retrieves and returns the fields' information of specified table of current schema.
func (d *Driver) TableFields(
ctx context.Context, table string, schema ...string,
) (fields map[string]*gdb.TableField, err error) {
var (
result gdb.Result
link gdb.Link
structureSql = fmt.Sprintf(tableFieldsSqlTmp, table)
)
// Schema parameter is not used for SlaveLink as it would attempt to switch database
// In GaussDB/PostgreSQL, schema is handled via search_path or table qualification
if link, err = d.SlaveLink(); err != nil {
return nil, err
}
result, err = d.DoSelect(ctx, link, structureSql)
if err != nil {
return nil, err
}
fields = make(map[string]*gdb.TableField)
var (
index = 0
name string
ok bool
existingField *gdb.TableField
)
for _, m := range result {
name = m["field"].String()
// Merge duplicated fields, especially for key constraints.
// Priority: pri > uni > others
if existingField, ok = fields[name]; ok {
currentKey := m["key"].String()
// Merge key information with priority: pri > uni
if currentKey == "pri" || (currentKey == "uni" && existingField.Key != "pri") {
existingField.Key = currentKey
}
continue
}
var (
fieldType string
dataType = m["type"].String()
dataLength = m["length"].Int()
)
if dataLength > 0 {
fieldType = fmt.Sprintf("%s(%d)", dataType, dataLength)
} else {
fieldType = dataType
}
fields[name] = &gdb.TableField{
Index: index,
Name: name,
Type: fieldType,
Null: !m["null"].Bool(),
Key: m["key"].String(),
Default: m["default_value"].Val(),
Comment: m["comment"].String(),
}
index++
}
return fields, nil
}

View File

@ -0,0 +1,103 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gaussdb
import (
"context"
"fmt"
"regexp"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gutil"
)
var (
tablesSqlTmp = `
SELECT
c.relname
FROM
pg_class c
INNER JOIN pg_namespace n ON
c.relnamespace = n.oid
WHERE
n.nspname = '%s'
AND c.relkind IN ('r', 'p')
%s
ORDER BY
c.relname
`
versionRegex = regexp.MustCompile(`PostgreSQL (\d+\.\d+)`)
)
func init() {
var err error
tablesSqlTmp, err = gdb.FormatMultiLineSqlToSingle(tablesSqlTmp)
if err != nil {
panic(err)
}
}
// Tables retrieves and returns the tables of current schema.
// It's mainly used in cli tool chain for automatically generating the models.
func (d *Driver) Tables(ctx context.Context, schema ...string) (tables []string, err error) {
var (
result gdb.Result
usedSchema = gutil.GetOrDefaultStr(d.GetConfig().Namespace, schema...)
)
if usedSchema == "" {
usedSchema = defaultSchema
}
// DO NOT use `usedSchema` as parameter for function `SlaveLink`.
// Schema is already handled in usedSchema variable above
link, err := d.SlaveLink()
if err != nil {
return nil, err
}
useRelpartbound := ""
if gstr.CompareVersion(d.version(ctx, link), "10") >= 0 {
useRelpartbound = "AND c.relpartbound IS NULL"
}
var query = fmt.Sprintf(
tablesSqlTmp,
usedSchema,
useRelpartbound,
)
query, _ = gregex.ReplaceString(`[\n\r\s]+`, " ", gstr.Trim(query))
result, err = d.DoSelect(ctx, link, query)
if err != nil {
return
}
for _, m := range result {
for _, v := range m {
tables = append(tables, v.String())
}
}
return
}
// version checks and returns the database version.
func (d *Driver) version(ctx context.Context, link gdb.Link) string {
result, err := d.DoSelect(ctx, link, "SELECT version();")
if err != nil {
return ""
}
if len(result) > 0 {
if v, ok := result[0]["version"]; ok {
matches := versionRegex.FindStringSubmatch(v.String())
if len(matches) >= 2 {
return matches[1]
}
}
}
return ""
}

View File

@ -0,0 +1,601 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gaussdb_test
import (
"fmt"
"strings"
"testing"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/test/gtest"
)
func Test_DB_Query(t *testing.T) {
table := createTable("name")
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
_, err := db.Query(ctx, fmt.Sprintf("select * from %s ", table))
t.AssertNil(err)
})
}
func Test_DB_Exec(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
_, err := db.Exec(ctx, fmt.Sprintf("select * from %s ", table))
t.AssertNil(err)
})
}
func Test_DB_Insert(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
_, err := db.Insert(ctx, table, g.Map{
"id": 1,
"passport": "t1",
"password": "25d55ad283aa400af464c76d713c07ad",
"nickname": "T1",
"create_time": gtime.Now().String(),
})
t.AssertNil(err)
answer, err := db.GetAll(ctx, fmt.Sprintf("SELECT * FROM %s WHERE id=?", table), 1)
t.AssertNil(err)
t.Assert(len(answer), 1)
t.Assert(answer[0]["passport"], "t1")
t.Assert(answer[0]["password"], "25d55ad283aa400af464c76d713c07ad")
t.Assert(answer[0]["nickname"], "T1")
// normal map
result, err := db.Insert(ctx, table, g.Map{
"id": "2",
"passport": "t2",
"password": "25d55ad283aa400af464c76d713c07ad",
"nickname": "name_2",
"create_time": gtime.Now().String(),
})
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 1)
answer, err = db.GetAll(ctx, fmt.Sprintf("SELECT * FROM %s WHERE id=?", table), 2)
t.AssertNil(err)
t.Assert(len(answer), 1)
t.Assert(answer[0]["passport"], "t2")
t.Assert(answer[0]["password"], "25d55ad283aa400af464c76d713c07ad")
t.Assert(answer[0]["nickname"], "name_2")
})
}
func Test_DB_Save(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
createTable("t_user")
defer dropTable("t_user")
i := 10
data := g.Map{
"id": i,
"passport": fmt.Sprintf(`t%d`, i),
"password": fmt.Sprintf(`p%d`, i),
"nickname": fmt.Sprintf(`T%d`, i),
"create_time": gtime.Now().String(),
}
_, err := db.Save(ctx, "t_user", data, 10)
gtest.AssertNil(err)
})
}
func Test_DB_Replace(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
createTable("t_user")
defer dropTable("t_user")
// Insert initial record
i := 10
data := g.Map{
"id": i,
"passport": fmt.Sprintf(`t%d`, i),
"password": fmt.Sprintf(`p%d`, i),
"nickname": fmt.Sprintf(`T%d`, i),
"create_time": gtime.Now().String(),
}
_, err := db.Insert(ctx, "t_user", data)
gtest.AssertNil(err)
// Replace with new data
data2 := g.Map{
"id": i,
"passport": fmt.Sprintf(`t%d_new`, i),
"password": fmt.Sprintf(`p%d_new`, i),
"nickname": fmt.Sprintf(`T%d_new`, i),
"create_time": gtime.Now().String(),
}
_, err = db.Replace(ctx, "t_user", data2)
gtest.AssertNil(err)
// Verify the data was replaced
one, err := db.GetOne(ctx, fmt.Sprintf("SELECT * FROM t_user WHERE id=?"), i)
gtest.AssertNil(err)
gtest.Assert(one["passport"].String(), fmt.Sprintf(`t%d_new`, i))
gtest.Assert(one["password"].String(), fmt.Sprintf(`p%d_new`, i))
gtest.Assert(one["nickname"].String(), fmt.Sprintf(`T%d_new`, i))
})
}
func Test_DB_GetAll(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
result, err := db.GetAll(ctx, fmt.Sprintf("SELECT * FROM %s WHERE id=?", table), 1)
t.AssertNil(err)
t.Assert(len(result), 1)
t.Assert(result[0]["id"].Int(), 1)
})
gtest.C(t, func(t *gtest.T) {
result, err := db.GetAll(ctx, fmt.Sprintf("SELECT * FROM %s WHERE id=?", table), g.Slice{1})
t.AssertNil(err)
t.Assert(len(result), 1)
t.Assert(result[0]["id"].Int(), 1)
})
gtest.C(t, func(t *gtest.T) {
result, err := db.GetAll(ctx, fmt.Sprintf("SELECT * FROM %s WHERE id in(?)", table), g.Slice{1, 2, 3})
t.AssertNil(err)
t.Assert(len(result), 3)
t.Assert(result[0]["id"].Int(), 1)
t.Assert(result[1]["id"].Int(), 2)
t.Assert(result[2]["id"].Int(), 3)
})
gtest.C(t, func(t *gtest.T) {
result, err := db.GetAll(ctx, fmt.Sprintf("SELECT * FROM %s WHERE id in(?,?,?)", table), g.Slice{1, 2, 3})
t.AssertNil(err)
t.Assert(len(result), 3)
t.Assert(result[0]["id"].Int(), 1)
t.Assert(result[1]["id"].Int(), 2)
t.Assert(result[2]["id"].Int(), 3)
})
gtest.C(t, func(t *gtest.T) {
result, err := db.GetAll(ctx, fmt.Sprintf("SELECT * FROM %s WHERE id in(?,?,?)", table), g.Slice{1, 2, 3}...)
t.AssertNil(err)
t.Assert(len(result), 3)
t.Assert(result[0]["id"].Int(), 1)
t.Assert(result[1]["id"].Int(), 2)
t.Assert(result[2]["id"].Int(), 3)
})
gtest.C(t, func(t *gtest.T) {
result, err := db.GetAll(ctx, fmt.Sprintf("SELECT * FROM %s WHERE id>=? AND id <=?", table), g.Slice{1, 3})
t.AssertNil(err)
t.Assert(len(result), 3)
t.Assert(result[0]["id"].Int(), 1)
t.Assert(result[1]["id"].Int(), 2)
t.Assert(result[2]["id"].Int(), 3)
})
}
func Test_DB_GetOne(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type User struct {
Id int
Passport string
Password string
Nickname string
CreateTime string
}
data := User{
Id: 1,
Passport: "user_1",
Password: "pass_1",
Nickname: "name_1",
CreateTime: "2020-10-10 12:00:01",
}
_, err := db.Insert(ctx, table, data)
t.AssertNil(err)
one, err := db.GetOne(ctx, fmt.Sprintf("SELECT * FROM %s WHERE id=?", table), 1)
t.AssertNil(err)
t.Assert(one["passport"], data.Passport)
t.Assert(one["create_time"], data.CreateTime)
t.Assert(one["nickname"], data.Nickname)
})
}
func Test_DB_GetValue(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
value, err := db.GetValue(ctx, fmt.Sprintf("SELECT id FROM %s WHERE passport=?", table), "user_3")
t.AssertNil(err)
t.Assert(value.Int(), 3)
})
}
func Test_DB_GetCount(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
count, err := db.GetCount(ctx, fmt.Sprintf("SELECT * FROM %s", table))
t.AssertNil(err)
t.Assert(count, TableSize)
})
}
func Test_DB_GetArray(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
array, err := db.GetArray(ctx, fmt.Sprintf("SELECT password FROM %s", table))
t.AssertNil(err)
arrays := make([]string, 0)
for i := 1; i <= TableSize; i++ {
arrays = append(arrays, fmt.Sprintf(`pass_%d`, i))
}
t.Assert(array, arrays)
})
}
func Test_DB_GetScan(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type User struct {
Id int
Passport string
Password string
NickName string
CreateTime gtime.Time
}
user := new(User)
err := db.GetScan(ctx, user, fmt.Sprintf("SELECT * FROM %s WHERE id=?", table), 3)
t.AssertNil(err)
t.Assert(user.NickName, "name_3")
})
}
func Test_DB_Update(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
result, err := db.Update(ctx, table, "password='987654321'", "id=3")
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 1)
one, err := db.Model(table).Where("id", 3).One()
t.AssertNil(err)
t.Assert(one["id"].Int(), 3)
t.Assert(one["passport"].String(), "user_3")
t.Assert(one["password"].String(), "987654321")
t.Assert(one["nickname"].String(), "name_3")
})
}
func Test_DB_Delete(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
result, err := db.Delete(ctx, table, "id>3")
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 7)
})
}
func Test_DB_Tables(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
tables := []string{"t_user1", "pop", "haha"}
for _, v := range tables {
createTable(v)
}
result, err := db.Tables(ctx)
gtest.AssertNil(err)
for i := 0; i < len(tables); i++ {
find := false
for j := 0; j < len(result); j++ {
if tables[i] == result[j] {
find = true
break
}
}
gtest.AssertEQ(find, true)
}
})
}
func Test_DB_TableFields(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
table := createTable()
defer dropTable(table)
var expect = map[string][]any{
// []string: Index Type Null Key Default Comment
// id is bigserial so the default is a pgsql function
"id": {0, "int8(64)", false, "pri", fmt.Sprintf("nextval('%s_id_seq'::regclass)", table), ""},
"passport": {1, "varchar(45)", false, "", nil, ""},
"password": {2, "varchar(32)", false, "", nil, ""},
"nickname": {3, "varchar(45)", false, "", nil, ""},
"create_time": {4, "timestamp", false, "", nil, ""},
}
res, err := db.TableFields(ctx, table)
gtest.AssertNil(err)
for k, v := range expect {
_, ok := res[k]
gtest.AssertEQ(ok, true)
gtest.AssertEQ(res[k].Index, v[0])
gtest.AssertEQ(res[k].Name, k)
gtest.AssertEQ(res[k].Type, v[1])
gtest.AssertEQ(res[k].Null, v[2])
gtest.AssertEQ(res[k].Key, v[3])
gtest.AssertEQ(res[k].Default, v[4])
gtest.AssertEQ(res[k].Comment, v[5])
}
})
}
func Test_NoFields_Error(t *testing.T) {
createSql := `CREATE TABLE IF NOT EXISTS %s (
id bigint PRIMARY KEY,
int_col INT);`
type Data struct {
Id int64
IntCol int64
}
// pgsql converts table names to lowercase
// mark: [c.oid = '%s'::regclass] is not case-sensitive
tableName := "Error_table"
_, err := db.Exec(ctx, fmt.Sprintf(createSql, tableName))
gtest.AssertNil(err)
defer dropTable(tableName)
gtest.C(t, func(t *gtest.T) {
var data = Data{
Id: 2,
IntCol: 2,
}
_, err = db.Model(tableName).Data(data).Insert()
t.AssertNE(err, nil)
// Insert a piece of test data using lowercase
_, err = db.Model(strings.ToLower(tableName)).Data(data).Insert()
t.AssertNil(err)
_, err = db.Model(tableName).Where("id", 1).Data(g.Map{
"int_col": 9999,
}).Update()
t.AssertNE(err, nil)
})
// The inserted field does not exist in the table
gtest.C(t, func(t *gtest.T) {
data := map[string]any{
"id1": 22,
"int_col_22": 11111,
}
_, err = db.Model(tableName).Data(data).Insert()
t.Assert(err, fmt.Errorf(`input data match no fields in table "%s"`, tableName))
lowerTableName := strings.ToLower(tableName)
_, err = db.Model(lowerTableName).Data(data).Insert()
t.Assert(err, fmt.Errorf(`input data match no fields in table "%s"`, lowerTableName))
_, err = db.Model(lowerTableName).Where("id", 1).Data(g.Map{
"int_col-2": 9999,
}).Update()
t.Assert(err, fmt.Errorf(`input data match no fields in table "%s"`, lowerTableName))
})
}
func Test_DB_TableFields_DuplicateConstraints(t *testing.T) {
// Test for the fix of duplicate field results with multiple constraints
// This test verifies that when a field has multiple constraints (e.g., both primary key and unique),
// the TableFields method correctly merges the results with proper priority (pri > uni > others)
gtest.C(t, func(t *gtest.T) {
tableName := "test_multi_constraint"
createSql := fmt.Sprintf(`
CREATE TABLE %s (
id bigserial NOT NULL PRIMARY KEY,
email varchar(100) NOT NULL UNIQUE,
username varchar(50) NOT NULL,
status int NOT NULL DEFAULT 1
)`, tableName)
_, err := db.Exec(ctx, createSql)
t.AssertNil(err)
defer dropTable(tableName)
// Get table fields
fields, err := db.TableFields(ctx, tableName)
t.AssertNil(err)
// Verify id field has primary key constraint
t.AssertNE(fields["id"], nil)
t.Assert(fields["id"].Key, "pri")
t.Assert(fields["id"].Name, "id")
t.Assert(fields["id"].Type, "int8(64)")
// Verify email field has unique constraint
t.AssertNE(fields["email"], nil)
t.Assert(fields["email"].Key, "uni")
t.Assert(fields["email"].Name, "email")
t.Assert(fields["email"].Type, "varchar(100)")
// Verify username field has no constraint
t.AssertNE(fields["username"], nil)
t.Assert(fields["username"].Key, "")
t.Assert(fields["username"].Name, "username")
// Verify status field has no constraint and has default value
t.AssertNE(fields["status"], nil)
t.Assert(fields["status"].Key, "")
t.Assert(fields["status"].Name, "status")
t.Assert(fields["status"].Default, 1)
// Verify field count is correct (no duplicates)
t.Assert(len(fields), 4)
})
// Test table with composite constraints
gtest.C(t, func(t *gtest.T) {
tableName := "test_composite_constraint"
createSql := fmt.Sprintf(`
CREATE TABLE %s (
user_id bigint NOT NULL,
project_id bigint NOT NULL,
role varchar(50) NOT NULL,
PRIMARY KEY (user_id, project_id)
)`, tableName)
_, err := db.Exec(ctx, createSql)
t.AssertNil(err)
defer dropTable(tableName)
// Get table fields
fields, err := db.TableFields(ctx, tableName)
t.AssertNil(err)
// In PostgreSQL, composite primary keys may appear in query results
// The first field in the composite key should be marked as 'pri'
t.AssertNE(fields["user_id"], nil)
t.Assert(fields["user_id"].Name, "user_id")
t.AssertNE(fields["project_id"], nil)
t.Assert(fields["project_id"].Name, "project_id")
t.AssertNE(fields["role"], nil)
t.Assert(fields["role"].Name, "role")
t.Assert(fields["role"].Key, "")
// Verify field count is correct (no duplicates)
t.Assert(len(fields), 3)
})
}
func Test_DB_InsertIgnore(t *testing.T) {
table := createTable()
defer dropTable(table)
// Insert test record
gtest.C(t, func(t *gtest.T) {
_, err := db.Insert(ctx, table, g.Map{
"id": 1,
"passport": "t1",
"password": "25d55ad283aa400af464c76d713c07ad",
"nickname": "T1",
"create_time": gtime.Now().String(),
})
t.AssertNil(err)
answer, err := db.GetAll(ctx, fmt.Sprintf("SELECT * FROM %s WHERE id=?", table), 1)
t.AssertNil(err)
t.Assert(len(answer), 1)
t.Assert(answer[0]["passport"], "t1")
t.Assert(answer[0]["password"], "25d55ad283aa400af464c76d713c07ad")
t.Assert(answer[0]["nickname"], "T1")
// Ignore Duplicate record
result, err := db.InsertIgnore(ctx, table, g.Map{
"id": 1,
"passport": "t1_duplicate",
"password": "duplicate_password",
"nickname": "Duplicate",
"create_time": gtime.Now().String(),
})
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 0)
answer, err = db.GetAll(ctx, fmt.Sprintf("SELECT * FROM %s WHERE id=?", table), 1)
t.AssertNil(err)
t.Assert(len(answer), 1)
t.Assert(answer[0]["passport"], "t1")
t.Assert(answer[0]["password"], "25d55ad283aa400af464c76d713c07ad")
t.Assert(answer[0]["nickname"], "T1")
// Insert Correct Record
result, err = db.Insert(ctx, table, g.Map{
"id": 2,
"passport": "t2",
"password": "25d55ad283aa400af464c76d713c07ad",
"nickname": "name_2",
"create_time": gtime.Now().String(),
})
t.AssertNil(err)
n, _ = result.RowsAffected()
t.Assert(n, 1)
answer, err = db.GetAll(ctx, fmt.Sprintf("SELECT * FROM %s WHERE id=?", table), 2)
t.AssertNil(err)
t.Assert(len(answer), 1)
t.Assert(answer[0]["passport"], "t2")
t.Assert(answer[0]["password"], "25d55ad283aa400af464c76d713c07ad")
t.Assert(answer[0]["nickname"], "name_2")
// Insert Multiple Records Using g.Map Array
data := g.List{
{
"id": 3,
"passport": "t3",
"password": "25d55ad283aa400af464c76d713c07ad",
"nickname": "name_3",
"create_time": gtime.Now().String(),
},
{
"id": 4,
"passport": "t4",
"password": "25d55ad283aa400af464c76d713c07ad",
"nickname": "name_4",
"create_time": gtime.Now().String(),
},
{
"id": 1,
"passport": "t1_conflict",
"password": "conflict_password",
"nickname": "conflict_name",
"create_time": gtime.Now().String(),
},
{
"id": 2,
"passport": "t2_conflict",
"password": "conflict_password",
"nickname": "conflict_name",
"create_time": gtime.Now().String(),
},
}
// Insert Multiple Records with Ignore
result, err = db.InsertIgnore(ctx, table, data)
t.AssertNil(err)
n, _ = result.RowsAffected()
t.Assert(n, 2)
answer, err = db.GetAll(ctx, fmt.Sprintf("SELECT * FROM %s", table))
t.AssertNil(err)
t.Assert(len(answer), 4)
// Should have four records in total (ID 1, 2, 3, 4)
t.Assert(answer[0]["passport"], "t1")
t.Assert(answer[1]["passport"], "t2")
t.Assert(answer[2]["passport"], "t3")
t.Assert(answer[3]["passport"], "t4")
})
}

View File

@ -0,0 +1,955 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gaussdb_test
import (
"fmt"
"testing"
"github.com/google/uuid"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/test/gtest"
)
// Test_TableFields tests the TableFields method for retrieving table field information
func Test_TableFields(t *testing.T) {
table := createAllTypesTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
fields, err := db.TableFields(ctx, table)
t.AssertNil(err)
t.Assert(len(fields) > 0, true)
// Test primary key field
t.Assert(fields["id"].Name, "id")
t.Assert(fields["id"].Key, "pri")
// Test integer types
t.Assert(fields["col_int2"].Name, "col_int2")
t.Assert(fields["col_int4"].Name, "col_int4")
t.Assert(fields["col_int8"].Name, "col_int8")
// Test float types
t.Assert(fields["col_float4"].Name, "col_float4")
t.Assert(fields["col_float8"].Name, "col_float8")
t.Assert(fields["col_numeric"].Name, "col_numeric")
// Test character types
t.Assert(fields["col_char"].Name, "col_char")
t.Assert(fields["col_varchar"].Name, "col_varchar")
t.Assert(fields["col_text"].Name, "col_text")
// Test boolean type
t.Assert(fields["col_bool"].Name, "col_bool")
// Test date/time types
t.Assert(fields["col_date"].Name, "col_date")
t.Assert(fields["col_timestamp"].Name, "col_timestamp")
// Test JSON types
t.Assert(fields["col_json"].Name, "col_json")
t.Assert(fields["col_jsonb"].Name, "col_jsonb")
// Test array types
t.Assert(fields["col_int2_arr"].Name, "col_int2_arr")
t.Assert(fields["col_int4_arr"].Name, "col_int4_arr")
t.Assert(fields["col_varchar_arr"].Name, "col_varchar_arr")
})
}
// Test_TableFields_Types tests field type information
func Test_TableFields_Types(t *testing.T) {
table := createAllTypesTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
fields, err := db.TableFields(ctx, table)
t.AssertNil(err)
// Test integer type names
t.Assert(fields["col_int2"].Type, "int2(16)")
t.Assert(fields["col_int4"].Type, "int4(32)")
t.Assert(fields["col_int8"].Type, "int8(64)")
// Test float type names
t.Assert(fields["col_float4"].Type, "float4(24)")
t.Assert(fields["col_float8"].Type, "float8(53)")
t.Assert(fields["col_numeric"].Type, "numeric(10)")
// Test character type names
t.Assert(fields["col_char"].Type, "bpchar(10)")
t.Assert(fields["col_varchar"].Type, "varchar(100)")
t.Assert(fields["col_text"].Type, "text")
// Test boolean type name
t.Assert(fields["col_bool"].Type, "bool")
// Test date/time type names
// Note: GaussDB internally represents date as timestamp in pg_type
t.Assert(fields["col_date"].Type, "timestamp")
t.Assert(fields["col_timestamp"].Type, "timestamp")
t.Assert(fields["col_timestamptz"].Type, "timestamptz")
// Test JSON type names
t.Assert(fields["col_json"].Type, "json")
t.Assert(fields["col_jsonb"].Type, "jsonb")
// Test array type names (PostgreSQL uses _ prefix for array types)
t.Assert(fields["col_int2_arr"].Type, "_int2")
t.Assert(fields["col_int4_arr"].Type, "_int4")
t.Assert(fields["col_int8_arr"].Type, "_int8")
t.Assert(fields["col_float4_arr"].Type, "_float4")
t.Assert(fields["col_float8_arr"].Type, "_float8")
t.Assert(fields["col_numeric_arr"].Type, "_numeric")
t.Assert(fields["col_varchar_arr"].Type, "_varchar")
t.Assert(fields["col_text_arr"].Type, "_text")
t.Assert(fields["col_bool_arr"].Type, "_bool")
})
}
// Test_TableFields_Nullable tests field nullable information
func Test_TableFields_Nullable(t *testing.T) {
table := createAllTypesTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
fields, err := db.TableFields(ctx, table)
t.AssertNil(err)
// NOT NULL fields should have Null = false
t.Assert(fields["col_int2"].Null, false)
t.Assert(fields["col_int4"].Null, false)
t.Assert(fields["col_numeric"].Null, false)
t.Assert(fields["col_varchar"].Null, false)
t.Assert(fields["col_bool"].Null, false)
t.Assert(fields["col_varchar_arr"].Null, false)
// Nullable fields should have Null = true
t.Assert(fields["col_int8"].Null, true)
t.Assert(fields["col_text"].Null, true)
t.Assert(fields["col_json"].Null, true)
})
}
// Test_TableFields_Comments tests field comment information
func Test_TableFields_Comments(t *testing.T) {
table := createAllTypesTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
fields, err := db.TableFields(ctx, table)
t.AssertNil(err)
// Test fields with comments
t.Assert(fields["id"].Comment, "Primary key ID")
t.Assert(fields["col_int2"].Comment, "int2 type (smallint)")
t.Assert(fields["col_int4"].Comment, "int4 type (integer)")
t.Assert(fields["col_int8"].Comment, "int8 type (bigint)")
t.Assert(fields["col_numeric"].Comment, "numeric type with precision")
t.Assert(fields["col_varchar"].Comment, "varchar type")
t.Assert(fields["col_bool"].Comment, "boolean type")
t.Assert(fields["col_timestamp"].Comment, "timestamp type")
t.Assert(fields["col_json"].Comment, "json type")
t.Assert(fields["col_jsonb"].Comment, "jsonb type")
// Test array field comments
t.Assert(fields["col_int2_arr"].Comment, "int2 array type (_int2)")
t.Assert(fields["col_int4_arr"].Comment, "int4 array type (_int4)")
t.Assert(fields["col_int8_arr"].Comment, "int8 array type (_int8)")
t.Assert(fields["col_numeric_arr"].Comment, "numeric array type (_numeric)")
t.Assert(fields["col_varchar_arr"].Comment, "varchar array type (_varchar)")
t.Assert(fields["col_text_arr"].Comment, "text array type (_text)")
})
}
// Test_Field_Type_Conversion tests type conversion for various PostgreSQL types
func Test_Field_Type_Conversion(t *testing.T) {
table := createInitAllTypesTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Query a single record
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
t.Assert(one.IsEmpty(), false)
// Test integer type conversions
t.Assert(one["col_int2"].Int(), 1)
t.Assert(one["col_int4"].Int(), 10)
t.Assert(one["col_int8"].Int64(), int64(100))
// Test float type conversions
t.Assert(one["col_float4"].Float32() > 0, true)
t.Assert(one["col_float8"].Float64() > 0, true)
// Test string type conversions
t.AssertNE(one["col_varchar"].String(), "")
t.AssertNE(one["col_text"].String(), "")
// Test boolean type conversion
t.Assert(one["col_bool"].Bool(), false) // i=1, 1%2==0 is false
})
}
// Test_Field_Array_Type_Conversion tests array type conversion
func Test_Field_Array_Type_Conversion(t *testing.T) {
table := createInitAllTypesTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Query a single record
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
t.Assert(one.IsEmpty(), false)
// Test integer array type conversions
int2Arr := one["col_int2_arr"].Ints()
t.Assert(len(int2Arr), 3)
t.Assert(int2Arr[0], 1)
t.Assert(int2Arr[1], 2)
t.Assert(int2Arr[2], 1)
int4Arr := one["col_int4_arr"].Ints()
t.Assert(len(int4Arr), 3)
t.Assert(int4Arr[0], 10)
t.Assert(int4Arr[1], 20)
t.Assert(int4Arr[2], 1)
int8Arr := one["col_int8_arr"].Int64s()
t.Assert(len(int8Arr), 3)
t.Assert(int8Arr[0], int64(100))
t.Assert(int8Arr[1], int64(200))
t.Assert(int8Arr[2], int64(1))
// Test string array type conversions
varcharArr := one["col_varchar_arr"].Strings()
t.Assert(len(varcharArr), 3)
t.Assert(varcharArr[0], "a")
t.Assert(varcharArr[1], "b")
t.Assert(varcharArr[2], "c1")
textArr := one["col_text_arr"].Strings()
t.Assert(len(textArr), 3)
t.Assert(textArr[0], "x")
t.Assert(textArr[1], "y")
t.Assert(textArr[2], "z1")
// Test boolean array type conversions
// col_bool_arr is '{true, false, %t}' where %t = i%2==0, for i=1 it's false
boolArr := one["col_bool_arr"].Bools()
t.Assert(len(boolArr), 3)
t.Assert(boolArr[0], true) // literal true
t.Assert(boolArr[1], false) // literal false
t.Assert(boolArr[2], false) // i=1, 1%2==0 is false
})
}
// Test_Field_Array_Insert tests inserting array data
func Test_Field_Array_Insert(t *testing.T) {
table := createAllTypesTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert with array values
_, err := db.Model(table).Data(g.Map{
"col_int2": 1,
"col_int4": 10,
"col_numeric": 99.99,
"col_varchar": "test",
"col_bool": true,
"col_int2_arr": []int{1, 2, 3},
"col_int4_arr": []int{10, 20, 30},
"col_varchar_arr": []string{"a", "b", "c"},
}).Insert()
t.AssertNil(err)
// Query and verify
one, err := db.Model(table).OrderDesc("id").One()
t.AssertNil(err)
t.Assert(one["col_int2"].Int(), 1)
t.Assert(one["col_varchar"].String(), "test")
t.Assert(one["col_bool"].Bool(), true)
int2Arr := one["col_int2_arr"].Ints()
t.Assert(len(int2Arr), 3)
t.Assert(int2Arr[0], 1)
t.Assert(int2Arr[1], 2)
t.Assert(int2Arr[2], 3)
varcharArr := one["col_varchar_arr"].Strings()
t.Assert(len(varcharArr), 3)
t.Assert(varcharArr[0], "a")
t.Assert(varcharArr[1], "b")
t.Assert(varcharArr[2], "c")
})
}
// Test_Field_Array_Update tests updating array data
func Test_Field_Array_Update(t *testing.T) {
table := createInitAllTypesTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Update array values
_, err := db.Model(table).Where("id", 1).Data(g.Map{
"col_int2_arr": []int{100, 200, 300},
"col_varchar_arr": []string{"x", "y", "z"},
}).Update()
t.AssertNil(err)
// Query and verify
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
int2Arr := one["col_int2_arr"].Ints()
t.Assert(len(int2Arr), 3)
t.Assert(int2Arr[0], 100)
t.Assert(int2Arr[1], 200)
t.Assert(int2Arr[2], 300)
varcharArr := one["col_varchar_arr"].Strings()
t.Assert(len(varcharArr), 3)
t.Assert(varcharArr[0], "x")
t.Assert(varcharArr[1], "y")
t.Assert(varcharArr[2], "z")
})
}
// Test_Field_JSON_Type tests JSON/JSONB type handling
func Test_Field_JSON_Type(t *testing.T) {
table := createAllTypesTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert with JSON values
testData := g.Map{
"name": "test",
"value": 123,
"items": []string{"a", "b", "c"},
}
_, err := db.Model(table).Data(g.Map{
"col_int2": 1,
"col_int4": 10,
"col_numeric": 99.99,
"col_varchar": "test",
"col_bool": true,
"col_json": testData,
"col_jsonb": testData,
}).Insert()
t.AssertNil(err)
// Query and verify
one, err := db.Model(table).OrderDesc("id").One()
t.AssertNil(err)
// Test JSON field
jsonMap := one["col_json"].Map()
t.Assert(jsonMap["name"], "test")
t.Assert(jsonMap["value"], 123)
// Test JSONB field
jsonbMap := one["col_jsonb"].Map()
t.Assert(jsonbMap["name"], "test")
t.Assert(jsonbMap["value"], 123)
})
}
// Test_Field_Scan_To_Struct tests scanning results to struct
func Test_Field_Scan_To_Struct(t *testing.T) {
table := createInitAllTypesTable()
defer dropTable(table)
type TestRecord struct {
Id int64 `json:"id"`
ColInt2 int16 `json:"col_int2"`
ColInt4 int32 `json:"col_int4"`
ColInt8 int64 `json:"col_int8"`
ColVarchar string `json:"col_varchar"`
ColBool bool `json:"col_bool"`
ColInt2Arr []int `json:"col_int2_arr"`
ColInt4Arr []int `json:"col_int4_arr"`
ColInt8Arr []int64 `json:"col_int8_arr"`
ColTextArr []string `json:"col_text_arr"`
}
gtest.C(t, func(t *gtest.T) {
var record TestRecord
err := db.Model(table).Where("id", 1).Scan(&record)
t.AssertNil(err)
t.Assert(record.Id, int64(1))
t.Assert(record.ColInt2, int16(1))
t.Assert(record.ColInt4, int32(10))
t.Assert(record.ColInt8, int64(100))
t.AssertNE(record.ColVarchar, "")
t.Assert(record.ColBool, false)
// Test array fields scanned to struct
t.Assert(len(record.ColInt2Arr), 3)
t.Assert(record.ColInt2Arr[0], 1)
t.Assert(record.ColInt2Arr[1], 2)
t.Assert(record.ColInt2Arr[2], 1)
t.Assert(len(record.ColTextArr), 3)
t.Assert(record.ColTextArr[0], "x")
t.Assert(record.ColTextArr[1], "y")
t.Assert(record.ColTextArr[2], "z1")
})
}
// Test_Field_Scan_To_Struct_Slice tests scanning multiple results to struct slice
func Test_Field_Scan_To_Struct_Slice(t *testing.T) {
table := createInitAllTypesTable()
defer dropTable(table)
type TestRecord struct {
Id int64 `json:"id"`
ColInt2 int16 `json:"col_int2"`
ColVarchar string `json:"col_varchar"`
ColInt2Arr []int `json:"col_int2_arr"`
ColTextArr []string `json:"col_text_arr"`
}
gtest.C(t, func(t *gtest.T) {
var records []TestRecord
err := db.Model(table).OrderAsc("id").Limit(5).Scan(&records)
t.AssertNil(err)
t.Assert(len(records), 5)
// Verify first record
t.Assert(records[0].Id, int64(1))
t.Assert(records[0].ColInt2, int16(1))
t.Assert(len(records[0].ColInt2Arr), 3)
// Verify last record
t.Assert(records[4].Id, int64(5))
t.Assert(records[4].ColInt2, int16(5))
})
}
// Test_Field_Empty_Array tests handling empty arrays
func Test_Field_Empty_Array(t *testing.T) {
table := createAllTypesTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert with empty array values (using default)
_, err := db.Model(table).Data(g.Map{
"col_int2": 1,
"col_int4": 10,
"col_numeric": 99.99,
"col_varchar": "test",
"col_bool": true,
}).Insert()
t.AssertNil(err)
// Query and verify empty arrays
one, err := db.Model(table).OrderDesc("id").One()
t.AssertNil(err)
// Default empty arrays
int2Arr := one["col_int2_arr"].Ints()
t.Assert(len(int2Arr), 0)
varcharArr := one["col_varchar_arr"].Strings()
t.Assert(len(varcharArr), 0)
})
}
// Test_Field_Null_Values tests handling NULL values
func Test_Field_Null_Values(t *testing.T) {
table := createAllTypesTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert minimal required fields, leaving nullable fields as NULL
_, err := db.Model(table).Data(g.Map{
"col_int2": 1,
"col_int4": 10,
"col_numeric": 99.99,
"col_varchar": "test",
"col_bool": true,
"col_varchar_arr": []string{},
}).Insert()
t.AssertNil(err)
// Query and verify NULL handling
one, err := db.Model(table).OrderDesc("id").One()
t.AssertNil(err)
// Nullable fields should return appropriate zero values
t.Assert(one["col_text"].IsNil() || one["col_text"].IsEmpty(), true)
t.Assert(one["col_int8_arr"].IsNil() || one["col_int8_arr"].IsEmpty(), true)
})
}
// Test_Field_Float_Array_Type_Conversion tests float array type conversion (_float4, _float8)
func Test_Field_Float_Array_Type_Conversion(t *testing.T) {
table := createInitAllTypesTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Query a single record
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
t.Assert(one.IsEmpty(), false)
// Test float4 array type conversions
float4Arr := one["col_float4_arr"].Float32s()
t.Assert(len(float4Arr), 3)
t.Assert(float4Arr[0] > 0, true)
t.Assert(float4Arr[1] > 0, true)
// Test float8 array type conversions
float8Arr := one["col_float8_arr"].Float64s()
t.Assert(len(float8Arr), 3)
t.Assert(float8Arr[0] > 0, true)
t.Assert(float8Arr[1] > 0, true)
})
}
// Test_Field_Numeric_Array_Type_Conversion tests numeric/decimal array type conversion
func Test_Field_Numeric_Array_Type_Conversion(t *testing.T) {
table := createInitAllTypesTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Query a single record
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
t.Assert(one.IsEmpty(), false)
// Test numeric array type conversions
numericArr := one["col_numeric_arr"].Float64s()
t.Assert(len(numericArr), 3)
t.Assert(numericArr[0] > 0, true)
t.Assert(numericArr[1] > 0, true)
// Test decimal array type conversions
decimalArr := one["col_decimal_arr"].Float64s()
if !one["col_decimal_arr"].IsNil() {
t.Assert(len(decimalArr) > 0, true)
}
})
}
// Test_Field_Bool_Array_Type_Conversion tests bool array type conversion more thoroughly
func Test_Field_Bool_Array_Type_Conversion(t *testing.T) {
table := createAllTypesTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert with specific bool array values
_, err := db.Model(table).Data(g.Map{
"col_int2": 1,
"col_int4": 10,
"col_numeric": 99.99,
"col_varchar": "test",
"col_bool": true,
"col_bool_arr": []bool{true, false, true},
}).Insert()
t.AssertNil(err)
// Query and verify
one, err := db.Model(table).OrderDesc("id").One()
t.AssertNil(err)
// Test bool array
boolArr := one["col_bool_arr"].Bools()
t.Assert(len(boolArr), 3)
t.Assert(boolArr[0], true)
t.Assert(boolArr[1], false)
t.Assert(boolArr[2], true)
})
}
// Test_Field_Char_Array_Type tests char array type (_char)
func Test_Field_Char_Array_Type(t *testing.T) {
table := createAllTypesTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert with char array values
_, err := db.Model(table).Data(g.Map{
"col_int2": 1,
"col_int4": 10,
"col_numeric": 99.99,
"col_varchar": "test",
"col_bool": true,
"col_char_arr": []string{"a", "b", "c"},
"col_varchar_arr": []string{},
}).Insert()
t.AssertNil(err)
// Query and verify
one, err := db.Model(table).OrderDesc("id").One()
t.AssertNil(err)
// Test char array
charArr := one["col_char_arr"].Strings()
t.Assert(len(charArr), 3)
})
}
// Test_Field_Bytea_Type tests bytea (binary) type conversion
func Test_Field_Bytea_Type(t *testing.T) {
table := createAllTypesTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert with binary data
binaryData := []byte{0x48, 0x65, 0x6c, 0x6c, 0x6f} // "Hello" in hex
_, err := db.Model(table).Data(g.Map{
"col_int2": 1,
"col_int4": 10,
"col_numeric": 99.99,
"col_varchar": "test",
"col_bool": true,
"col_bytea": binaryData,
"col_varchar_arr": []string{},
}).Insert()
t.AssertNil(err)
// Query and verify
one, err := db.Model(table).OrderDesc("id").One()
t.AssertNil(err)
// Test bytea field
result := one["col_bytea"].Bytes()
t.Assert(len(result), 5)
t.Assert(result[0], 0x48) // 'H'
})
}
// Test_Field_Bytea_Array_Type tests bytea array type (_bytea)
func Test_Field_Bytea_Array_Type(t *testing.T) {
table := createAllTypesTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert with bytea array values using raw SQL
// PostgreSQL bytea array literal format: ARRAY[E'\\x010203', E'\\x040506']::bytea[]
_, err := db.Exec(ctx, fmt.Sprintf(`
INSERT INTO %s (col_int2, col_int4, col_numeric, col_varchar, col_bool, col_varchar_arr, col_bytea_arr)
VALUES (1, 10, 99.99, 'test', true, '{}', ARRAY[E'\\x010203', E'\\x040506']::bytea[])
`, table))
t.AssertNil(err)
// Query and verify bytea array
one, err := db.Model(table).OrderDesc("id").One()
t.AssertNil(err)
// Test bytea array field - should be converted to [][]byte
byteaArrVal := one["col_bytea_arr"]
t.Assert(byteaArrVal.IsNil(), false)
// Verify the array contains the expected data
byteaArr := byteaArrVal.Interfaces()
t.Assert(len(byteaArr), 2)
})
}
// Test_Field_Date_Array_Type tests date array type (_date)
func Test_Field_Date_Array_Type(t *testing.T) {
table := createAllTypesTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Note: PostgreSQL _date array is not yet mapped in the driver
// This test documents the limitation but can be extended when support is added
_, err := db.Model(table).Data(g.Map{
"col_int2": 1,
"col_int4": 10,
"col_numeric": 99.99,
"col_varchar": "test",
"col_bool": true,
"col_varchar_arr": []string{},
}).Insert()
t.AssertNil(err)
// Query and verify NULL date array is handled gracefully
one, err := db.Model(table).OrderDesc("id").One()
t.AssertNil(err)
// date array should be nil or empty
t.Assert(one["col_date_arr"].IsNil() || one["col_date_arr"].IsEmpty(), true)
})
}
// Test_Field_Timestamp_Array_Type tests timestamp array type (_timestamp)
func Test_Field_Timestamp_Array_Type(t *testing.T) {
table := createAllTypesTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Note: PostgreSQL _timestamp array is not yet mapped in the driver
// This test documents the limitation but can be extended when support is added
_, err := db.Model(table).Data(g.Map{
"col_int2": 1,
"col_int4": 10,
"col_numeric": 99.99,
"col_varchar": "test",
"col_bool": true,
"col_varchar_arr": []string{},
}).Insert()
t.AssertNil(err)
// Query and verify NULL timestamp array is handled gracefully
one, err := db.Model(table).OrderDesc("id").One()
t.AssertNil(err)
// timestamp array should be nil or empty
t.Assert(one["col_timestamp_arr"].IsNil() || one["col_timestamp_arr"].IsEmpty(), true)
})
}
// Test_Field_JSONB_Array_Type tests JSONB array type (_jsonb)
func Test_Field_JSONB_Array_Type(t *testing.T) {
table := createAllTypesTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Note: PostgreSQL _jsonb array is not yet mapped in the driver
// This test documents the limitation but can be extended when support is added
_, err := db.Model(table).Data(g.Map{
"col_int2": 1,
"col_int4": 10,
"col_numeric": 99.99,
"col_varchar": "test",
"col_bool": true,
"col_varchar_arr": []string{},
}).Insert()
t.AssertNil(err)
// Query and verify NULL jsonb array is handled gracefully
one, err := db.Model(table).OrderDesc("id").One()
t.AssertNil(err)
// jsonb array should be nil or empty
t.Assert(one["col_jsonb_arr"].IsNil() || one["col_jsonb_arr"].IsEmpty(), true)
})
}
// Test_Field_UUID_Array_Type tests UUID array type (_uuid)
func Test_Field_UUID_Array_Type(t *testing.T) {
table := createAllTypesTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert with UUID array values using raw SQL
// PostgreSQL uuid array literal format: ARRAY['uuid1', 'uuid2']::uuid[]
uuid1 := "550e8400-e29b-41d4-a716-446655440000"
uuid2 := "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
uuid3 := "6ba7b811-9dad-11d1-80b4-00c04fd430c8"
_, err := db.Exec(ctx, fmt.Sprintf(`
INSERT INTO %s (col_int2, col_int4, col_numeric, col_varchar, col_bool, col_varchar_arr, col_uuid_arr)
VALUES (1, 10, 99.99, 'test', true, '{}', ARRAY['%s', '%s', '%s']::uuid[])
`, table, uuid1, uuid2, uuid3))
t.AssertNil(err)
// Query and verify UUID array
one, err := db.Model(table).OrderDesc("id").One()
t.AssertNil(err)
// Test UUID array field - should be converted to []uuid.UUID
uuidArrVal := one["col_uuid_arr"]
t.Assert(uuidArrVal.IsNil(), false)
// Verify the array contains the expected data as []uuid.UUID
uuidArr := uuidArrVal.Interfaces()
t.Assert(len(uuidArr), 3)
// Verify each element is uuid.UUID type
u1, ok := uuidArr[0].(uuid.UUID)
t.Assert(ok, true)
t.Assert(u1.String(), uuid1)
u2, ok := uuidArr[1].(uuid.UUID)
t.Assert(ok, true)
t.Assert(u2.String(), uuid2)
u3, ok := uuidArr[2].(uuid.UUID)
t.Assert(ok, true)
t.Assert(u3.String(), uuid3)
})
}
// Test_Field_UUID_Type tests UUID type
func Test_Field_UUID_Type(t *testing.T) {
table := createInitAllTypesTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Query and verify UUID field
one, err := db.Model(table).OrderAsc("id").One()
t.AssertNil(err)
// Test UUID field - should be converted to uuid.UUID
uuidVal := one["col_uuid"]
t.Assert(uuidVal.IsNil(), false)
// Verify the value is uuid.UUID type
uuidObj, ok := uuidVal.Val().(uuid.UUID)
t.Assert(ok, true)
// Verify the UUID format
uuidStr := uuidObj.String()
t.Assert(len(uuidStr) > 0, true)
// UUID should contain the pattern from insert: 550e8400-e29b-41d4-a716-44665544000X
t.Assert(uuidStr, "550e8400-e29b-41d4-a716-446655440001")
// Also verify we can still get string representation via .String()
t.Assert(uuidVal.String(), "550e8400-e29b-41d4-a716-446655440001")
})
}
// Test_Field_Bytea_Array_Type_Scan tests bytea array type and scanning
func Test_Field_Bytea_Array_Type_Scan(t *testing.T) {
table := createInitAllTypesTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Query and verify bytea array field
one, err := db.Model(table).OrderAsc("id").One()
t.AssertNil(err)
// Test bytea array field
byteaArrVal := one["col_bytea_arr"]
// bytea array should not be nil since we inserted data
t.Assert(byteaArrVal.IsNil(), false)
})
}
// Test_Field_Date_Array_Type_Scan tests date array type and scanning
func Test_Field_Date_Array_Type_Scan(t *testing.T) {
table := createInitAllTypesTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Query and verify date array field
one, err := db.Model(table).OrderAsc("id").One()
t.AssertNil(err)
// Test date array field
dateArrVal := one["col_date_arr"]
t.Assert(dateArrVal.IsNil(), false)
// Verify the array contains the expected data
dateArr := dateArrVal.Strings()
t.Assert(len(dateArr) > 0, true)
})
}
// Test_Field_Timestamp_Array_Type_Scan tests timestamp array type and scanning
func Test_Field_Timestamp_Array_Type_Scan(t *testing.T) {
table := createInitAllTypesTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Query and verify timestamp array field
one, err := db.Model(table).OrderAsc("id").One()
t.AssertNil(err)
// Test timestamp array field
timestampArrVal := one["col_timestamp_arr"]
t.Assert(timestampArrVal.IsNil(), false)
// Verify the array contains the expected data
timestampArr := timestampArrVal.Strings()
t.Assert(len(timestampArr) > 0, true)
})
}
// Test_Field_JSONB_Array_Type_Scan tests JSONB array type and scanning
func Test_Field_JSONB_Array_Type_Scan(t *testing.T) {
table := createInitAllTypesTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Query and verify JSONB array field
one, err := db.Model(table).OrderAsc("id").One()
t.AssertNil(err)
// Test JSONB array field
jsonbArrVal := one["col_jsonb_arr"]
t.Assert(jsonbArrVal.IsNil(), false)
})
}
// Test_Field_UUID_Query tests querying by UUID field
func Test_Field_UUID_Query(t *testing.T) {
table := createInitAllTypesTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Test 1: Query by UUID string
uuidStr := "550e8400-e29b-41d4-a716-446655440001"
one, err := db.Model(table).Where("col_uuid", uuidStr).One()
t.AssertNil(err)
t.Assert(one.IsEmpty(), false)
t.Assert(one["id"].Int(), 1)
// Verify the returned UUID is correct
uuidObj, ok := one["col_uuid"].Val().(uuid.UUID)
t.Assert(ok, true)
t.Assert(uuidObj.String(), uuidStr)
// Test 2: Query by uuid.UUID type directly
uuidVal, err := uuid.Parse("550e8400-e29b-41d4-a716-446655440002")
t.AssertNil(err)
one, err = db.Model(table).Where("col_uuid", uuidVal).One()
t.AssertNil(err)
t.Assert(one.IsEmpty(), false)
t.Assert(one["id"].Int(), 2)
// Test 3: Query by UUID string using g.Map
one, err = db.Model(table).Where(g.Map{
"col_uuid": "550e8400-e29b-41d4-a716-446655440003",
}).One()
t.AssertNil(err)
t.Assert(one.IsEmpty(), false)
t.Assert(one["id"].Int(), 3)
// Test 4: Query by uuid.UUID type using g.Map
uuidVal, err = uuid.Parse("550e8400-e29b-41d4-a716-446655440004")
t.AssertNil(err)
one, err = db.Model(table).Where(g.Map{
"col_uuid": uuidVal,
}).One()
t.AssertNil(err)
t.Assert(one.IsEmpty(), false)
t.Assert(one["id"].Int(), 4)
// Test 5: Query non-existent UUID
one, err = db.Model(table).Where("col_uuid", "00000000-0000-0000-0000-000000000000").One()
t.AssertNil(err)
t.Assert(one.IsEmpty(), true)
// Test 6: Query multiple records by UUID IN clause with strings
all, err := db.Model(table).WhereIn("col_uuid", g.Slice{
"550e8400-e29b-41d4-a716-446655440001",
"550e8400-e29b-41d4-a716-446655440002",
}).OrderAsc("id").All()
t.AssertNil(err)
t.Assert(len(all), 2)
t.Assert(all[0]["id"].Int(), 1)
t.Assert(all[1]["id"].Int(), 2)
// Test 7: Query multiple records by UUID IN clause with uuid.UUID types
uuid1, _ := uuid.Parse("550e8400-e29b-41d4-a716-446655440003")
uuid2, _ := uuid.Parse("550e8400-e29b-41d4-a716-446655440004")
all, err = db.Model(table).WhereIn("col_uuid", g.Slice{uuid1, uuid2}).OrderAsc("id").All()
t.AssertNil(err)
t.Assert(len(all), 2)
t.Assert(all[0]["id"].Int(), 3)
t.Assert(all[1]["id"].Int(), 4)
})
}

View File

@ -0,0 +1,277 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gaussdb_test
import (
"testing"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/test/gtest"
"github.com/gogf/gf/contrib/drivers/gaussdb/v2"
)
// Test_DoFilter_LimitOffset tests LIMIT OFFSET conversion
func Test_DoFilter_LimitOffset(t *testing.T) {
var (
ctx = gctx.New()
driver = gaussdb.Driver{}
)
gtest.C(t, func(t *gtest.T) {
// Test MySQL style LIMIT x,y to PostgreSQL style LIMIT y OFFSET x
sql := "SELECT * FROM users LIMIT 10, 20"
newSql, _, err := driver.DoFilter(ctx, nil, sql, nil)
t.AssertNil(err)
t.Assert(newSql, "SELECT * FROM users LIMIT 20 OFFSET 10")
})
gtest.C(t, func(t *gtest.T) {
// Test with different numbers
sql := "SELECT * FROM users LIMIT 0, 100"
newSql, _, err := driver.DoFilter(ctx, nil, sql, nil)
t.AssertNil(err)
t.Assert(newSql, "SELECT * FROM users LIMIT 100 OFFSET 0")
})
gtest.C(t, func(t *gtest.T) {
// Test no conversion needed
sql := "SELECT * FROM users LIMIT 50"
newSql, _, err := driver.DoFilter(ctx, nil, sql, nil)
t.AssertNil(err)
t.Assert(newSql, "SELECT * FROM users LIMIT 50")
})
}
// Test_DoFilter_InsertIgnore tests INSERT IGNORE conversion
func Test_DoFilter_InsertIgnore(t *testing.T) {
var (
ctx = gctx.New()
driver = gaussdb.Driver{}
)
gtest.C(t, func(t *gtest.T) {
// Test INSERT IGNORE conversion
// Note: GaussDB (PostgreSQL 9.2) does not support ON CONFLICT syntax (added in PG 9.5)
// GaussDB handles InsertIgnore at DoInsert level using MERGE statement
sql := "INSERT IGNORE INTO users (name) VALUES ($1)"
newSql, _, err := driver.DoFilter(ctx, nil, sql, nil)
t.AssertNil(err)
// GaussDB removes IGNORE keyword but doesn't add ON CONFLICT (not supported)
t.Assert(newSql, "INSERT INTO users (name) VALUES ($1)")
})
}
// Test_DoFilter_PlaceholderConversion tests placeholder conversion
func Test_DoFilter_PlaceholderConversion(t *testing.T) {
var (
ctx = gctx.New()
driver = gaussdb.Driver{}
)
gtest.C(t, func(t *gtest.T) {
// Test ? placeholder conversion to $n
sql := "SELECT * FROM users WHERE id = ? AND name = ?"
newSql, _, err := driver.DoFilter(ctx, nil, sql, nil)
t.AssertNil(err)
t.Assert(newSql, "SELECT * FROM users WHERE id = $1 AND name = $2")
})
gtest.C(t, func(t *gtest.T) {
// Test multiple placeholders
sql := "INSERT INTO users (a, b, c, d, e) VALUES (?, ?, ?, ?, ?)"
newSql, _, err := driver.DoFilter(ctx, nil, sql, nil)
t.AssertNil(err)
t.Assert(newSql, "INSERT INTO users (a, b, c, d, e) VALUES ($1, $2, $3, $4, $5)")
})
}
// Test_DoFilter_JsonbOperator tests JSONB operator handling
func Test_DoFilter_JsonbOperator(t *testing.T) {
var (
ctx = gctx.New()
driver = gaussdb.Driver{}
)
gtest.C(t, func(t *gtest.T) {
// Test jsonb ?| operator
// The jsonb ? is first converted to $1, then restored to ?
// So the next placeholder becomes $2
sql := "SELECT * FROM users WHERE (data)::jsonb ?| ?"
newSql, _, err := driver.DoFilter(ctx, nil, sql, nil)
t.AssertNil(err)
// After placeholder conversion, the ? in jsonb should be preserved
t.Assert(newSql, "SELECT * FROM users WHERE (data)::jsonb ?| $2")
})
gtest.C(t, func(t *gtest.T) {
// Test jsonb ?& operator
sql := "SELECT * FROM users WHERE (data)::jsonb &? ?"
newSql, _, err := driver.DoFilter(ctx, nil, sql, nil)
t.AssertNil(err)
t.Assert(newSql, "SELECT * FROM users WHERE (data)::jsonb &? $2")
})
gtest.C(t, func(t *gtest.T) {
// Test jsonb ? operator
sql := "SELECT * FROM users WHERE (data)::jsonb ? ?"
newSql, _, err := driver.DoFilter(ctx, nil, sql, nil)
t.AssertNil(err)
t.Assert(newSql, "SELECT * FROM users WHERE (data)::jsonb ? $2")
})
gtest.C(t, func(t *gtest.T) {
// Test combination of jsonb and regular placeholders
sql := "SELECT * FROM users WHERE id = ? AND (data)::jsonb ?| ?"
newSql, _, err := driver.DoFilter(ctx, nil, sql, nil)
t.AssertNil(err)
t.Assert(newSql, "SELECT * FROM users WHERE id = $1 AND (data)::jsonb ?| $3")
})
}
// Test_DoFilter_ComplexQuery tests complex queries with multiple features
func Test_DoFilter_ComplexQuery(t *testing.T) {
var (
ctx = gctx.New()
driver = gaussdb.Driver{}
)
gtest.C(t, func(t *gtest.T) {
// Test complex query with LIMIT and placeholders
sql := "SELECT * FROM users WHERE status = ? AND age > ? LIMIT 5, 10"
newSql, _, err := driver.DoFilter(ctx, nil, sql, nil)
t.AssertNil(err)
t.Assert(newSql, "SELECT * FROM users WHERE status = $1 AND age > $2 LIMIT 10 OFFSET 5")
})
}
// Test_Tables tests the Tables method
func Test_Tables_Method(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
tables, err := db.Tables(ctx)
t.AssertNil(err)
t.Assert(len(tables) >= 0, true)
})
gtest.C(t, func(t *gtest.T) {
// Test with specific schema - use the test schema
tables, err := db.Tables(ctx, "test")
t.AssertNil(err)
t.Assert(len(tables) >= 0, true)
})
}
// Test_OrderRandomFunction tests the OrderRandomFunction method
func Test_OrderRandomFunction(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Test ORDER BY RANDOM()
all, err := db.Model(table).OrderRandom().All()
t.AssertNil(err)
t.Assert(len(all), TableSize)
})
}
// Test_GetChars tests the GetChars method
func Test_GetChars(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
driver := gaussdb.Driver{}
left, right := driver.GetChars()
t.Assert(left, `"`)
t.Assert(right, `"`)
})
}
// Test_New tests the New method
func Test_New(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
driver := gaussdb.New()
t.AssertNE(driver, nil)
})
}
// Test_DoExec_NonIntPrimaryKey tests DoExec with non-integer primary key
func Test_DoExec_NonIntPrimaryKey(t *testing.T) {
// Create a table with UUID primary key
tableName := "t_uuid_pk_test"
_, err := db.Exec(ctx, `
CREATE TABLE IF NOT EXISTS `+tableName+` (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name varchar(100)
)
`)
if err != nil {
// If gen_random_uuid is not available, skip this test
t.Log("Skipping UUID test:", err)
return
}
defer db.Exec(ctx, "DROP TABLE IF EXISTS "+tableName)
gtest.C(t, func(t *gtest.T) {
// Insert with UUID primary key
result, err := db.Model(tableName).Data(g.Map{
"name": "test_user",
}).Insert()
t.AssertNil(err)
// LastInsertId should return error for non-integer primary key
_, err = result.LastInsertId()
// For UUID, LastInsertId is not supported
t.AssertNE(err, nil)
// RowsAffected should still work
affected, err := result.RowsAffected()
t.AssertNil(err)
t.Assert(affected, int64(1))
})
}
// Test_TableFields_WithSchema tests TableFields with specific schema
func Test_TableFields_WithSchema(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Test with schema parameter
fields, err := db.TableFields(ctx, table, "test")
t.AssertNil(err)
t.Assert(len(fields) > 0, true)
})
}
// Test_TableFields_UniqueKey tests TableFields with unique key constraint
func Test_TableFields_UniqueKey(t *testing.T) {
tableName := "t_unique_test"
// Create table with unique constraint
_, err := db.Exec(ctx, `
CREATE TABLE IF NOT EXISTS `+tableName+` (
id bigserial PRIMARY KEY,
email varchar(100) UNIQUE NOT NULL,
name varchar(100)
)
`)
if err != nil {
t.Error(err)
return
}
defer db.Exec(ctx, "DROP TABLE IF EXISTS "+tableName)
gtest.C(t, func(t *gtest.T) {
fields, err := db.TableFields(ctx, tableName)
t.AssertNil(err)
// Check primary key
t.Assert(fields["id"].Key, "pri")
// Check unique key
t.Assert(fields["email"].Key, "uni")
})
}

View File

@ -0,0 +1,339 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gaussdb_test
import (
"context"
"fmt"
"strings"
_ "github.com/gogf/gf/contrib/drivers/gaussdb/v2"
"github.com/gogf/gf/v2/container/garray"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/test/gtest"
)
const (
TableSize = 10
TablePrefix = "t_"
SchemaName = "test"
CreateTime = "2018-10-24 10:00:00"
)
var (
db gdb.DB
configNode gdb.ConfigNode
ctx = context.TODO()
)
func init() {
configNode = gdb.ConfigNode{
Link: `gaussdb:gaussdb:UTpass@1234@tcp(127.0.0.1:9950)/postgres`,
Namespace: SchemaName, // Set the schema namespace
}
// gaussdb only permit to connect to the designation database.
// so you need to create the gaussdb database before you use orm
gdb.AddConfigNode(gdb.DefaultGroupName, configNode)
if r, err := gdb.New(configNode); err != nil {
gtest.Fatal(err)
} else {
db = r
}
// Create schema if not exists
schemaTemplate := "CREATE SCHEMA IF NOT EXISTS %s"
if _, err := db.Exec(ctx, fmt.Sprintf(schemaTemplate, SchemaName)); err != nil {
gtest.Error(err)
}
}
func createTable(table ...string) string {
return createTableWithDb(db, table...)
}
func createInitTable(table ...string) string {
return createInitTableWithDb(db, table...)
}
func createTableWithDb(db gdb.DB, table ...string) (name string) {
if len(table) > 0 {
name = table[0]
} else {
name = fmt.Sprintf(`%s_%d`, TablePrefix+"test", gtime.TimestampNano())
}
dropTableWithDb(db, name)
if _, err := db.Exec(ctx, fmt.Sprintf(`
CREATE TABLE %s (
id bigserial NOT NULL,
passport varchar(45) NOT NULL,
password varchar(32) NOT NULL,
nickname varchar(45) NOT NULL,
create_time timestamp NOT NULL,
favorite_movie varchar[],
favorite_music text[],
numeric_values numeric[],
decimal_values decimal[],
PRIMARY KEY (id)
) ;`, name,
)); err != nil {
gtest.Fatal(err)
}
return
}
func dropTable(table string) {
dropTableWithDb(db, table)
}
func createInitTableWithDb(db gdb.DB, table ...string) (name string) {
name = createTableWithDb(db, table...)
array := garray.New(true)
for i := 1; i <= TableSize; i++ {
array.Append(g.Map{
"id": i,
"passport": fmt.Sprintf(`user_%d`, i),
"password": fmt.Sprintf(`pass_%d`, i),
"nickname": fmt.Sprintf(`name_%d`, i),
"create_time": gtime.NewFromStr(CreateTime).String(),
})
}
result, err := db.Insert(ctx, name, array.Slice())
gtest.AssertNil(err)
n, e := result.RowsAffected()
gtest.Assert(e, nil)
gtest.Assert(n, TableSize)
return
}
func dropTableWithDb(db gdb.DB, table string) {
if _, err := db.Exec(ctx, fmt.Sprintf("DROP TABLE IF EXISTS %s", table)); err != nil {
gtest.Error(err)
}
}
// createAllTypesTable creates a table with all common PostgreSQL types for testing
func createAllTypesTable(table ...string) string {
return createAllTypesTableWithDb(db, table...)
}
func createAllTypesTableWithDb(db gdb.DB, table ...string) (name string) {
if len(table) > 0 {
name = table[0]
} else {
name = fmt.Sprintf(`%s_%d`, TablePrefix+"all_types", gtime.TimestampNano())
}
dropTableWithDb(db, name)
if _, err := db.Exec(ctx, fmt.Sprintf(`
CREATE TABLE %s (
-- Basic integer types
id bigserial PRIMARY KEY,
col_int2 int2 NOT NULL DEFAULT 0,
col_int4 int4 NOT NULL DEFAULT 0,
col_int8 int8 DEFAULT 0,
col_smallint smallint,
col_integer integer,
col_bigint bigint,
-- Float types
col_float4 float4 DEFAULT 0.0,
col_float8 float8 DEFAULT 0.0,
col_real real,
col_double double precision,
col_numeric numeric(10,2) NOT NULL DEFAULT 0.00,
col_decimal decimal(10,2),
-- Character types
col_char char(10) DEFAULT '',
col_varchar varchar(100) NOT NULL DEFAULT '',
col_text text,
-- Boolean type
col_bool boolean NOT NULL DEFAULT false,
-- Date/Time types
col_date date DEFAULT CURRENT_DATE,
col_time time,
col_timetz timetz,
col_timestamp timestamp DEFAULT CURRENT_TIMESTAMP,
col_timestamptz timestamptz,
col_interval interval,
-- Binary type
col_bytea bytea,
-- JSON types
col_json json DEFAULT '{}',
col_jsonb jsonb DEFAULT '{}',
-- UUID type
col_uuid uuid,
-- Network types
col_inet inet,
col_cidr cidr,
col_macaddr macaddr,
-- Array types - integers
col_int2_arr int2[] DEFAULT '{}',
col_int4_arr int4[] DEFAULT '{}',
col_int8_arr int8[],
-- Array types - floats
col_float4_arr float4[],
col_float8_arr float8[],
col_numeric_arr numeric[] DEFAULT '{}',
col_decimal_arr decimal[],
-- Array types - characters
col_varchar_arr varchar[] NOT NULL DEFAULT '{}',
col_text_arr text[],
col_char_arr char(10)[],
-- Array types - boolean
col_bool_arr boolean[],
-- Array types - bytea
col_bytea_arr bytea[],
-- Array types - date/time
col_date_arr date[],
col_timestamp_arr timestamp[],
-- Array types - JSON
col_jsonb_arr jsonb[],
-- Array types - UUID
col_uuid_arr uuid[]
);
-- Add comments for columns
COMMENT ON TABLE %s IS 'Test table with all PostgreSQL types';
COMMENT ON COLUMN %s.id IS 'Primary key ID';
COMMENT ON COLUMN %s.col_int2 IS 'int2 type (smallint)';
COMMENT ON COLUMN %s.col_int4 IS 'int4 type (integer)';
COMMENT ON COLUMN %s.col_int8 IS 'int8 type (bigint)';
COMMENT ON COLUMN %s.col_numeric IS 'numeric type with precision';
COMMENT ON COLUMN %s.col_varchar IS 'varchar type';
COMMENT ON COLUMN %s.col_bool IS 'boolean type';
COMMENT ON COLUMN %s.col_timestamp IS 'timestamp type';
COMMENT ON COLUMN %s.col_json IS 'json type';
COMMENT ON COLUMN %s.col_jsonb IS 'jsonb type';
COMMENT ON COLUMN %s.col_int2_arr IS 'int2 array type (_int2)';
COMMENT ON COLUMN %s.col_int4_arr IS 'int4 array type (_int4)';
COMMENT ON COLUMN %s.col_int8_arr IS 'int8 array type (_int8)';
COMMENT ON COLUMN %s.col_numeric_arr IS 'numeric array type (_numeric)';
COMMENT ON COLUMN %s.col_varchar_arr IS 'varchar array type (_varchar)';
COMMENT ON COLUMN %s.col_text_arr IS 'text array type (_text)';
`, name,
name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name)); err != nil {
gtest.Fatal(err)
}
return
}
// createInitAllTypesTable creates and initializes a table with all common PostgreSQL types
func createInitAllTypesTable(table ...string) string {
return createInitAllTypesTableWithDb(db, table...)
}
func createInitAllTypesTableWithDb(db gdb.DB, table ...string) (name string) {
name = createAllTypesTableWithDb(db, table...)
// Insert test data
for i := 1; i <= TableSize; i++ {
var sql strings.Builder
// Write INSERT statement header
sql.WriteString(fmt.Sprintf(`INSERT INTO %s (
col_int2, col_int4, col_int8, col_smallint, col_integer, col_bigint,
col_float4, col_float8, col_real, col_double, col_numeric, col_decimal,
col_char, col_varchar, col_text, col_bool,
col_date, col_time, col_timestamp,
col_json, col_jsonb,
col_bytea,
col_uuid,
col_int2_arr, col_int4_arr, col_int8_arr,
col_float4_arr, col_float8_arr, col_numeric_arr, col_decimal_arr,
col_varchar_arr, col_text_arr, col_bool_arr, col_bytea_arr, col_date_arr, col_timestamp_arr, col_jsonb_arr, col_uuid_arr
) VALUES (`, name))
// Integer types: col_int2, col_int4, col_int8, col_smallint, col_integer, col_bigint
sql.WriteString(fmt.Sprintf("%d, %d, %d, %d, %d, %d, ",
i, i*10, i*100, i, i*10, i*100))
// Float types: col_float4, col_float8, col_real, col_double, col_numeric, col_decimal
sql.WriteString(fmt.Sprintf("%d.5, %d.5, %d.5, %d.5, %d.99, %d.99, ",
i, i, i, i, i, i))
// Character types: col_char, col_varchar, col_text, col_bool
sql.WriteString(fmt.Sprintf("'char_%d', 'varchar_%d', 'text_%d', %t, ",
i, i, i, i%2 == 0))
// Date/Time types: col_date, col_time, col_timestamp
// Calculate day as integer in range 1-28; 28 is used because it is the maximum day value safe for all months to avoid date validity issues.
// %02d in fmt.Sprintf ensures two-digit zero-padded format
dayOfMonth := (i-1)%28 + 1
sql.WriteString(fmt.Sprintf("'2024-01-%02d', '10:00:%02d', '2024-01-%02d 10:00:00', ",
dayOfMonth, (i-1)%60, dayOfMonth))
// JSON types: col_json, col_jsonb
sql.WriteString(fmt.Sprintf(`'{"key": "value%d"}', '{"key": "value%d"}', `, i, i))
// Bytea type: col_bytea
sql.WriteString(`E'\\xDEADBEEF', `)
// UUID type: col_uuid (use %x for hex representation, padded to ensure valid UUID)
sql.WriteString(fmt.Sprintf("'550e8400-e29b-41d4-a716-4466554400%02x', ", i))
// Integer array types: col_int2_arr, col_int4_arr, col_int8_arr
sql.WriteString(fmt.Sprintf("'{1, 2, %d}', '{10, 20, %d}', '{100, 200, %d}', ",
i, i, i))
// Float array types: col_float4_arr, col_float8_arr, col_numeric_arr, col_decimal_arr
sql.WriteString(fmt.Sprintf("'{1.1, 2.2, %d.3}', '{1.1, 2.2, %d.3}', '{1.11, 2.22, %d.33}', '{1.11, 2.22, %d.33}', ",
i, i, i, i))
// Character array types: col_varchar_arr, col_text_arr
sql.WriteString(fmt.Sprintf(`'{"a", "b", "c%d"}', '{"x", "y", "z%d"}', `, i, i))
// Boolean array type: col_bool_arr
sql.WriteString(fmt.Sprintf("'{true, false, %t}', ", i%2 == 0))
// Bytea array type: col_bytea_arr (use ARRAY syntax for bytea)
sql.WriteString(`ARRAY[E'\\xDEADBEEF', E'\\xCAFEBABE']::bytea[], `)
// Date array type: col_date_arr
sql.WriteString(fmt.Sprintf(`'{"2024-01-%02d", "2024-01-%02d"}', `, dayOfMonth, (dayOfMonth%28)+1))
// Timestamp array type: col_timestamp_arr
sql.WriteString(fmt.Sprintf(`'{"2024-01-%02d 10:00:00", "2024-01-%02d 11:00:00"}', `, dayOfMonth, dayOfMonth))
// JSONB array type: col_jsonb_arr (store as text array first, then cast to jsonb array)
sql.WriteString(`ARRAY['{"key": "value1"}', '{"key": "value2"}']::jsonb[], `)
// UUID array type: col_uuid_arr
sql.WriteString(fmt.Sprintf("ARRAY['550e8400-e29b-41d4-a716-4466554400%02x'::uuid, '6ba7b810-9dad-11d1-80b4-00c04fd430c8'::uuid]", i))
// Close VALUES
sql.WriteString(")")
if _, err := db.Exec(ctx, sql.String()); err != nil {
gtest.Fatal(err)
}
}
return
}

View File

@ -0,0 +1,864 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gaussdb_test
import (
"database/sql"
"fmt"
"testing"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/test/gtest"
)
func Test_Model_Insert(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
user := db.Model(table)
result, err := user.Data(g.Map{
"id": 1,
"uid": 1,
"passport": "t1",
"password": "25d55ad283aa400af464c76d713c07ad",
"nickname": "name_1",
"create_time": gtime.Now().String(),
}).Insert()
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 1)
result, err = db.Model(table).Data(g.Map{
"id": "2",
"uid": "2",
"passport": "t2",
"password": "25d55ad283aa400af464c76d713c07ad",
"nickname": "name_2",
"create_time": gtime.Now().String(),
}).Insert()
t.AssertNil(err)
n, _ = result.RowsAffected()
t.Assert(n, 1)
type User struct {
Id int `gconv:"id"`
Uid int `gconv:"uid"`
Passport string `json:"passport"`
Password string `gconv:"password"`
Nickname string `gconv:"nickname"`
CreateTime *gtime.Time `json:"create_time"`
}
// Model inserting.
result, err = db.Model(table).Data(User{
Id: 3,
Uid: 3,
Passport: "t3",
Password: "25d55ad283aa400af464c76d713c07ad",
Nickname: "name_3",
CreateTime: gtime.Now(),
}).Insert()
t.AssertNil(err)
n, _ = result.RowsAffected()
t.Assert(n, 1)
value, err := db.Model(table).Fields("passport").Where("id=3").Value() // model value
t.AssertNil(err)
t.Assert(value.String(), "t3")
result, err = db.Model(table).Data(&User{
Id: 4,
Uid: 4,
Passport: "t4",
Password: "25d55ad283aa400af464c76d713c07ad",
Nickname: "T4",
CreateTime: gtime.Now(),
}).Insert()
t.AssertNil(err)
n, _ = result.RowsAffected()
t.Assert(n, 1)
value, err = db.Model(table).Fields("passport").Where("id=4").Value()
t.AssertNil(err)
t.Assert(value.String(), "t4")
result, err = db.Model(table).Where("id>?", 1).Delete() // model delete
t.AssertNil(err)
n, _ = result.RowsAffected()
t.Assert(n, 3)
})
}
func Test_Model_One(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type User struct {
Id int
Passport string
Password string
Nickname string
CreateTime string
}
data := User{
Id: 1,
Passport: "user_1",
Password: "pass_1",
Nickname: "name_1",
CreateTime: "2020-10-10 12:00:01",
}
_, err := db.Model(table).Data(data).Insert()
t.AssertNil(err)
one, err := db.Model(table).WherePri(1).One() // model one
t.AssertNil(err)
t.Assert(one["passport"], data.Passport)
t.Assert(one["create_time"], data.CreateTime)
t.Assert(one["nickname"], data.Nickname)
})
}
func Test_Model_All(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
result, err := db.Model(table).All()
t.AssertNil(err)
t.Assert(len(result), TableSize)
})
}
func Test_Model_Delete(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
result, err := db.Model(table).Where("id", "2").Delete()
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 1)
})
}
func Test_Model_Update(t *testing.T) {
table := createInitTable()
defer dropTable(table)
// Update + Data(string)
gtest.C(t, func(t *gtest.T) {
result, err := db.Model(table).Data("passport='user_33'").Where("passport='user_3'").Update()
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 1)
})
// Update + Fields(string)
gtest.C(t, func(t *gtest.T) {
result, err := db.Model(table).Fields("passport").Data(g.Map{
"passport": "user_44",
"none": "none",
}).Where("passport='user_4'").Update()
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 1)
})
}
func Test_Model_Array(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
all, err := db.Model(table).Where("id", g.Slice{1, 2, 3}).All()
t.AssertNil(err)
t.Assert(all.Array("id"), g.Slice{1, 2, 3})
t.Assert(all.Array("nickname"), g.Slice{"name_1", "name_2", "name_3"})
})
gtest.C(t, func(t *gtest.T) {
array, err := db.Model(table).Fields("nickname").Where("id", g.Slice{1, 2, 3}).Array()
t.AssertNil(err)
t.Assert(array, g.Slice{"name_1", "name_2", "name_3"})
})
gtest.C(t, func(t *gtest.T) {
array, err := db.Model(table).Array("nickname", "id", g.Slice{1, 2, 3})
t.AssertNil(err)
t.Assert(array, g.Slice{"name_1", "name_2", "name_3"})
})
}
func Test_Model_Scan(t *testing.T) {
table := createInitTable()
defer dropTable(table)
type User struct {
Id int
Passport string
Password string
NickName string
CreateTime gtime.Time
}
gtest.C(t, func(t *gtest.T) {
var users []User
err := db.Model(table).Scan(&users)
t.AssertNil(err)
t.Assert(len(users), TableSize)
})
}
func Test_Model_Count(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
count, err := db.Model(table).Count()
t.AssertNil(err)
t.Assert(count, int64(TableSize))
})
gtest.C(t, func(t *gtest.T) {
count, err := db.Model(table).FieldsEx("id").Where("id>8").Count()
t.AssertNil(err)
t.Assert(count, int64(2))
})
}
func Test_Model_Exist(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
exist, err := db.Model(table).Exist()
t.AssertNil(err)
t.Assert(exist, TableSize > 0)
exist, err = db.Model(table).Where("id", -1).Exist()
t.AssertNil(err)
t.Assert(exist, false)
})
}
func Test_Model_Where(t *testing.T) {
table := createInitTable()
defer dropTable(table)
// map + slice parameter
gtest.C(t, func(t *gtest.T) {
result, err := db.Model(table).Where(g.Map{
"id": g.Slice{1, 2, 3},
"passport": g.Slice{"user_2", "user_3"},
}).Where("id=? and nickname=?", g.Slice{3, "name_3"}).One()
t.AssertNil(err)
t.AssertGT(len(result), 0)
t.Assert(result["id"].Int(), 3)
})
// struct, automatic mapping and filtering.
gtest.C(t, func(t *gtest.T) {
type User struct {
Id int
Nickname string
}
result, err := db.Model(table).Where(User{3, "name_3"}).One()
t.AssertNil(err)
t.Assert(result["id"].Int(), 3)
result, err = db.Model(table).Where(&User{3, "name_3"}).One()
t.AssertNil(err)
t.Assert(result["id"].Int(), 3)
})
}
func Test_Model_Save(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type User struct {
Id int
Passport string
Password string
NickName string
CreateTime *gtime.Time
}
var (
user User
count int
result sql.Result
err error
)
result, err = db.Model(table).Data(g.Map{
"id": 1,
"passport": "p1",
"password": "pw1",
"nickname": "n1",
"create_time": CreateTime,
}).OnConflict("id").Save()
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 1)
err = db.Model(table).Scan(&user)
t.AssertNil(err)
t.Assert(user.Id, 1)
t.Assert(user.Passport, "p1")
t.Assert(user.Password, "pw1")
t.Assert(user.NickName, "n1")
t.Assert(user.CreateTime.String(), CreateTime)
_, err = db.Model(table).Data(g.Map{
"id": 1,
"passport": "p1",
"password": "pw2",
"nickname": "n2",
"create_time": CreateTime,
}).OnConflict("id").Save()
t.AssertNil(err)
err = db.Model(table).Scan(&user)
t.AssertNil(err)
t.Assert(user.Passport, "p1")
t.Assert(user.Password, "pw2")
t.Assert(user.NickName, "n2")
t.Assert(user.CreateTime.String(), CreateTime)
count, err = db.Model(table).Count()
t.AssertNil(err)
t.Assert(count, 1)
})
}
func Test_Model_Replace(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert initial record
result, err := db.Model(table).Data(g.Map{
"id": 1,
"passport": "t1",
"password": "pass1",
"nickname": "T1",
"create_time": "2018-10-24 10:00:00",
}).Insert()
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 1)
// Replace with new data
result, err = db.Model(table).Data(g.Map{
"id": 1,
"passport": "t11",
"password": "25d55ad283aa400af464c76d713c07ad",
"nickname": "T11",
"create_time": "2018-10-24 10:00:00",
}).Replace()
t.AssertNil(err)
n, _ = result.RowsAffected()
t.Assert(n, 1)
// Verify the data was replaced
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
t.Assert(one["passport"].String(), "t11")
t.Assert(one["password"].String(), "25d55ad283aa400af464c76d713c07ad")
t.Assert(one["nickname"].String(), "T11")
// Replace with new ID (insert new record)
result, err = db.Model(table).Data(g.Map{
"id": 2,
"passport": "t22",
"password": "pass22",
"nickname": "T22",
"create_time": "2018-10-24 11:00:00",
}).Replace()
t.AssertNil(err)
n, _ = result.RowsAffected()
t.Assert(n, 1)
// Verify new record was inserted
count, err := db.Model(table).Count()
t.AssertNil(err)
t.Assert(count, 2)
})
}
func Test_Model_OnConflict(t *testing.T) {
var (
table = fmt.Sprintf(`%s_%d`, TablePrefix+"test", gtime.TimestampNano())
uniqueName = fmt.Sprintf(`%s_%d`, TablePrefix+"test_unique", gtime.TimestampNano())
)
if _, err := db.Exec(ctx, fmt.Sprintf(`
CREATE TABLE %s (
id bigserial NOT NULL,
passport varchar(45) NOT NULL,
password varchar(32) NOT NULL,
nickname varchar(45) NOT NULL,
create_time timestamp NOT NULL,
PRIMARY KEY (id),
CONSTRAINT %s UNIQUE ("passport", "password")
) ;`, table, uniqueName,
)); err != nil {
gtest.Fatal(err)
}
defer dropTable(table)
// string type 1.
gtest.C(t, func(t *gtest.T) {
data := g.Map{
"id": 1,
"passport": "pp1",
"password": "pw1",
"nickname": "n1",
"create_time": "2016-06-06",
}
_, err := db.Model(table).OnConflict("passport,password").Data(data).Save()
t.AssertNil(err)
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
t.Assert(one["passport"], data["passport"])
t.Assert(one["password"], data["password"])
t.Assert(one["nickname"], "n1")
})
// string type 2.
gtest.C(t, func(t *gtest.T) {
data := g.Map{
"id": 1,
"passport": "pp1",
"password": "pw1",
"nickname": "n1",
"create_time": "2016-06-06",
}
_, err := db.Model(table).OnConflict("passport", "password").Data(data).Save()
t.AssertNil(err)
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
t.Assert(one["passport"], data["passport"])
t.Assert(one["password"], data["password"])
t.Assert(one["nickname"], "n1")
})
// slice.
gtest.C(t, func(t *gtest.T) {
data := g.Map{
"id": 1,
"passport": "pp1",
"password": "pw1",
"nickname": "n1",
"create_time": "2016-06-06",
}
_, err := db.Model(table).OnConflict(g.Slice{"passport", "password"}).Data(data).Save()
t.AssertNil(err)
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
t.Assert(one["passport"], data["passport"])
t.Assert(one["password"], data["password"])
t.Assert(one["nickname"], "n1")
})
}
func Test_Model_OnDuplicate(t *testing.T) {
table := createInitTable()
defer dropTable(table)
// string type 1.
gtest.C(t, func(t *gtest.T) {
data := g.Map{
"id": 1,
"passport": "pp1",
"password": "pw1",
"nickname": "n1",
"create_time": "2016-06-06",
}
_, err := db.Model(table).OnConflict("id").OnDuplicate("passport,password").Data(data).Save()
t.AssertNil(err)
one, err := db.Model(table).WherePri(1).One()
t.AssertNil(err)
t.Assert(one["passport"], data["passport"])
t.Assert(one["password"], data["password"])
t.Assert(one["nickname"], "name_1")
})
// string type 2.
gtest.C(t, func(t *gtest.T) {
data := g.Map{
"id": 1,
"passport": "pp1",
"password": "pw1",
"nickname": "n1",
"create_time": "2016-06-06",
}
_, err := db.Model(table).OnConflict("id").OnDuplicate("passport", "password").Data(data).Save()
t.AssertNil(err)
one, err := db.Model(table).WherePri(1).One()
t.AssertNil(err)
t.Assert(one["passport"], data["passport"])
t.Assert(one["password"], data["password"])
t.Assert(one["nickname"], "name_1")
})
// slice.
gtest.C(t, func(t *gtest.T) {
data := g.Map{
"id": 1,
"passport": "pp1",
"password": "pw1",
"nickname": "n1",
"create_time": "2016-06-06",
}
_, err := db.Model(table).OnConflict("id").OnDuplicate(g.Slice{"passport", "password"}).Data(data).Save()
t.AssertNil(err)
one, err := db.Model(table).WherePri(1).One()
t.AssertNil(err)
t.Assert(one["passport"], data["passport"])
t.Assert(one["password"], data["password"])
t.Assert(one["nickname"], "name_1")
})
// map.
gtest.C(t, func(t *gtest.T) {
data := g.Map{
"id": 1,
"passport": "pp1",
"password": "pw1",
"nickname": "n1",
"create_time": "2016-06-06",
}
_, err := db.Model(table).OnConflict("id").OnDuplicate(g.Map{
"passport": "nickname",
"password": "nickname",
}).Data(data).Save()
t.AssertNil(err)
one, err := db.Model(table).WherePri(1).One()
t.AssertNil(err)
t.Assert(one["passport"], data["nickname"])
t.Assert(one["password"], data["nickname"])
t.Assert(one["nickname"], "name_1")
})
// map+raw.
gtest.C(t, func(t *gtest.T) {
data := g.MapStrStr{
"id": "1",
"passport": "pp1",
"password": "pw1",
"nickname": "n1",
"create_time": "2016-06-06",
}
_, err := db.Model(table).OnConflict("id").OnDuplicate(g.Map{
"passport": gdb.Raw("CONCAT(EXCLUDED.passport, '1')"),
"password": gdb.Raw("CONCAT(EXCLUDED.password, '2')"),
}).Data(data).Save()
t.AssertNil(err)
one, err := db.Model(table).WherePri(1).One()
t.AssertNil(err)
t.Assert(one["passport"], data["passport"]+"1")
t.Assert(one["password"], data["password"]+"2")
t.Assert(one["nickname"], "name_1")
})
}
func Test_Model_OnDuplicateWithCounter(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
data := g.Map{
"id": 1,
"passport": "pp1",
"password": "pw1",
"nickname": "n1",
"create_time": "2016-06-06",
}
_, err := db.Model(table).OnConflict("id").OnDuplicate(g.Map{
"id": gdb.Counter{Field: "id", Value: 999999},
}).Data(data).Save()
t.AssertNil(err)
one, err := db.Model(table).WherePri(1).One()
t.AssertNil(err)
t.AssertNil(one)
})
}
func Test_Model_OnDuplicateEx(t *testing.T) {
table := createInitTable()
defer dropTable(table)
// string type 1.
gtest.C(t, func(t *gtest.T) {
data := g.Map{
"id": 1,
"passport": "pp1",
"password": "pw1",
"nickname": "n1",
"create_time": "2016-06-06",
}
_, err := db.Model(table).OnConflict("id").OnDuplicateEx("nickname,create_time").Data(data).Save()
t.AssertNil(err)
one, err := db.Model(table).WherePri(1).One()
t.AssertNil(err)
t.Assert(one["passport"], data["passport"])
t.Assert(one["password"], data["password"])
t.Assert(one["nickname"], "name_1")
})
// string type 2.
gtest.C(t, func(t *gtest.T) {
data := g.Map{
"id": 1,
"passport": "pp1",
"password": "pw1",
"nickname": "n1",
"create_time": "2016-06-06",
}
_, err := db.Model(table).OnConflict("id").OnDuplicateEx("nickname", "create_time").Data(data).Save()
t.AssertNil(err)
one, err := db.Model(table).WherePri(1).One()
t.AssertNil(err)
t.Assert(one["passport"], data["passport"])
t.Assert(one["password"], data["password"])
t.Assert(one["nickname"], "name_1")
})
// slice.
gtest.C(t, func(t *gtest.T) {
data := g.Map{
"id": 1,
"passport": "pp1",
"password": "pw1",
"nickname": "n1",
"create_time": "2016-06-06",
}
_, err := db.Model(table).OnConflict("id").OnDuplicateEx(g.Slice{"nickname", "create_time"}).Data(data).Save()
t.AssertNil(err)
one, err := db.Model(table).WherePri(1).One()
t.AssertNil(err)
t.Assert(one["passport"], data["passport"])
t.Assert(one["password"], data["password"])
t.Assert(one["nickname"], "name_1")
})
// map.
gtest.C(t, func(t *gtest.T) {
data := g.Map{
"id": 1,
"passport": "pp1",
"password": "pw1",
"nickname": "n1",
"create_time": "2016-06-06",
}
_, err := db.Model(table).OnConflict("id").OnDuplicateEx(g.Map{
"nickname": "nickname",
"create_time": "nickname",
}).Data(data).Save()
t.AssertNil(err)
one, err := db.Model(table).WherePri(1).One()
t.AssertNil(err)
t.Assert(one["passport"], data["passport"])
t.Assert(one["password"], data["password"])
t.Assert(one["nickname"], "name_1")
})
}
func Test_OrderRandom(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
result, err := db.Model(table).OrderRandom().All()
t.AssertNil(err)
t.Assert(len(result), TableSize)
})
}
func Test_ConvertSliceString(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type User struct {
Id int
Passport string
Password string
NickName string
CreateTime *gtime.Time
FavoriteMovie []string
FavoriteMusic []string
}
var (
user User
user2 User
err error
)
// slice string not null
_, err = db.Model(table).Data(g.Map{
"id": 1,
"passport": "p1",
"password": "pw1",
"nickname": "n1",
"create_time": CreateTime,
"favorite_movie": g.Slice{"Iron-Man", "Spider-Man"},
"favorite_music": g.Slice{"Hey jude", "Let it be"},
}).Insert()
t.AssertNil(err)
err = db.Model(table).Where("id", 1).Scan(&user)
t.AssertNil(err)
t.Assert(len(user.FavoriteMusic), 2)
t.Assert(user.FavoriteMusic[0], "Hey jude")
t.Assert(user.FavoriteMusic[1], "Let it be")
t.Assert(len(user.FavoriteMovie), 2)
t.Assert(user.FavoriteMovie[0], "Iron-Man")
t.Assert(user.FavoriteMovie[1], "Spider-Man")
// slice string null
_, err = db.Model(table).Data(g.Map{
"id": 2,
"passport": "p1",
"password": "pw1",
"nickname": "n1",
"create_time": CreateTime,
}).Insert()
t.AssertNil(err)
err = db.Model(table).Where("id", 2).Scan(&user2)
t.AssertNil(err)
t.Assert(user2.FavoriteMusic, nil)
t.Assert(len(user2.FavoriteMovie), 0)
})
}
func Test_ConvertSliceFloat64(t *testing.T) {
table := createTable()
defer dropTable(table)
type Args struct {
NumericValues []float64 `orm:"numeric_values"`
DecimalValues []float64 `orm:"decimal_values"`
}
type User struct {
Id int `orm:"id"`
Passport string `orm:"passport"`
Password string `json:"password"`
NickName string `json:"nickname"`
CreateTime *gtime.Time `json:"create_time"`
Args
}
tests := []struct {
name string
args Args
}{
{
name: "nil",
args: Args{
NumericValues: nil,
DecimalValues: nil,
},
},
{
name: "not nil",
args: Args{
NumericValues: []float64{1.1, 2.2, 3.3},
DecimalValues: []float64{1.1, 2.2, 3.3},
},
},
{
name: "not empty",
args: Args{
NumericValues: []float64{},
DecimalValues: []float64{},
},
},
}
now := gtime.New(CreateTime)
for i, tt := range tests {
gtest.C(t, func(t *gtest.T) {
user := User{
Id: i + 1,
Passport: fmt.Sprintf("test_%d", i+1),
Password: fmt.Sprintf("pass_%d", i+1),
NickName: fmt.Sprintf("name_%d", i+1),
CreateTime: now,
Args: tt.args,
}
_, err := db.Model(table).OmitNilData().Insert(user)
t.AssertNil(err)
var got Args
err = db.Model(table).Where("id", user.Id).Limit(1).Scan(&got)
t.AssertNil(err)
t.AssertEQ(tt.args, got)
})
}
}
func Test_Model_InsertIgnore(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
user := db.Model(table)
result, err := user.Data(g.Map{
"id": 1,
"uid": 1,
"passport": "t1",
"password": "25d55ad283aa400af464c76d713c07ad",
"nickname": "name_1",
"create_time": gtime.Now().String(),
}).Insert()
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 1)
result, err = db.Model(table).Data(g.Map{
"id": 1,
"uid": 1,
"passport": "t1",
"password": "25d55ad283aa400af464c76d713c07ad",
"nickname": "name_1",
"create_time": gtime.Now().String(),
}).Insert()
t.AssertNE(err, nil)
result, err = db.Model(table).Data(g.Map{
"id": 1,
"uid": 1,
"passport": "t2",
"password": "25d55ad283aa400af464c76d713c07ad",
"nickname": "name_2",
"create_time": gtime.Now().String(),
}).InsertIgnore()
t.AssertNil(err)
n, _ = result.RowsAffected()
t.Assert(n, 0)
value, err := db.Model(table).Fields("passport").WherePri(1).Value()
t.AssertNil(err)
t.Assert(value.String(), "t1")
count, err := db.Model(table).Count()
t.AssertNil(err)
t.Assert(count, 1)
// pgsql support ignore without primary key
result, err = db.Model(table).Data(g.Map{
// "id": 1,
"uid": 1,
"passport": "t2",
"password": "25d55ad283aa400af464c76d713c07ad",
"nickname": "name_2",
"create_time": gtime.Now().String(),
}).InsertIgnore()
t.AssertNil(err)
count, err = db.Model(table).Count()
t.AssertNil(err)
t.Assert(count, 1)
})
}

View File

@ -0,0 +1,179 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gaussdb_test
import (
"testing"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/test/gtest"
"github.com/gogf/gf/contrib/drivers/gaussdb/v2"
)
// Test_Open tests the Open method with various configurations
func Test_Open_WithNamespace(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
driver := gaussdb.Driver{}
config := &gdb.ConfigNode{
User: "postgres",
Pass: "12345678",
Host: "127.0.0.1",
Port: "5432",
Name: "test",
Namespace: "public",
}
db, err := driver.Open(config)
t.AssertNil(err)
t.AssertNE(db, nil)
if db != nil {
db.Close()
}
})
}
// Test_Open_WithTimezone tests Open with timezone configuration
func Test_Open_WithTimezone(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
driver := gaussdb.Driver{}
config := &gdb.ConfigNode{
User: "postgres",
Pass: "12345678",
Host: "127.0.0.1",
Port: "5432",
Name: "test",
Timezone: "Asia/Shanghai",
}
db, err := driver.Open(config)
t.AssertNil(err)
t.AssertNE(db, nil)
if db != nil {
db.Close()
}
})
}
// Test_Open_WithExtra tests Open with extra configuration
func Test_Open_WithExtra(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
driver := gaussdb.Driver{}
config := &gdb.ConfigNode{
User: "postgres",
Pass: "12345678",
Host: "127.0.0.1",
Port: "5432",
Name: "test",
Extra: "connect_timeout=10",
}
db, err := driver.Open(config)
t.AssertNil(err)
t.AssertNE(db, nil)
if db != nil {
db.Close()
}
})
}
// Test_Open_WithInvalidExtra tests Open with invalid extra configuration
func Test_Open_WithInvalidExtra(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
driver := gaussdb.Driver{}
config := &gdb.ConfigNode{
User: "postgres",
Pass: "12345678",
Host: "127.0.0.1",
Port: "5432",
Name: "test",
// Invalid extra format with invalid URL encoding that will cause parse error
Extra: "%Q=%Q&b",
}
_, err := driver.Open(config)
t.AssertNE(err, nil)
})
}
// Test_Open_WithFullConfig tests Open with all configuration options
func Test_Open_WithFullConfig(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
driver := gaussdb.Driver{}
config := &gdb.ConfigNode{
User: "postgres",
Pass: "12345678",
Host: "127.0.0.1",
Port: "5432",
Name: "test",
Namespace: "public",
Timezone: "UTC",
Extra: "connect_timeout=10",
}
db, err := driver.Open(config)
t.AssertNil(err)
t.AssertNE(db, nil)
if db != nil {
db.Close()
}
})
}
// Test_Open_WithoutPort tests Open without port
func Test_Open_WithoutPort(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
driver := gaussdb.Driver{}
config := &gdb.ConfigNode{
User: "postgres",
Pass: "12345678",
Host: "127.0.0.1",
Name: "test",
}
db, err := driver.Open(config)
t.AssertNil(err)
t.AssertNE(db, nil)
if db != nil {
db.Close()
}
})
}
// Test_Open_WithoutName tests Open without database name
func Test_Open_WithoutName(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
driver := gaussdb.Driver{}
config := &gdb.ConfigNode{
User: "postgres",
Pass: "12345678",
Host: "127.0.0.1",
Port: "5432",
}
db, err := driver.Open(config)
t.AssertNil(err)
t.AssertNE(db, nil)
if db != nil {
db.Close()
}
})
}
// Test_Open_InvalidHost tests Open with invalid host
func Test_Open_InvalidHost(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
driver := gaussdb.Driver{}
config := &gdb.ConfigNode{
User: "postgres",
Pass: "12345678",
Host: "invalid_host_that_does_not_exist",
Port: "5432",
Name: "test",
}
// Note: sql.Open doesn't actually connect, so no error here
// The error would occur when actually using the connection
db, err := driver.Open(config)
t.AssertNil(err)
if db != nil {
db.Close()
}
})
}

View File

@ -0,0 +1,99 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gaussdb_test
import (
"testing"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/test/gtest"
)
func Test_Raw_Insert(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
user := db.Model(table)
result, err := user.Data(g.Map{
"passport": "port_1",
"password": "pass_1",
"nickname": "name_1",
"create_time": gdb.Raw("now()"),
}).Insert()
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 1)
})
}
func Test_Raw_BatchInsert(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
user := db.Model(table)
result, err := user.Data(
g.List{
g.Map{
"passport": "port_2",
"password": "pass_2",
"nickname": "name_2",
"create_time": gdb.Raw("now()"),
},
g.Map{
"passport": "port_4",
"password": "pass_4",
"nickname": "name_4",
"create_time": gdb.Raw("now()"),
},
},
).Insert()
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 2)
})
}
func Test_Raw_Delete(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
user := db.Model(table)
result, err := user.Data(g.Map{
"id": gdb.Raw("id"),
}).Where("id", 1).Delete()
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 1)
})
}
func Test_Raw_Update(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
user := db.Model(table)
result, err := user.Data(g.Map{
"id": gdb.Raw("id+100"),
"create_time": gdb.Raw("now()"),
}).Where("id", 1).Update()
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 1)
})
gtest.C(t, func(t *gtest.T) {
user := db.Model(table)
n, err := user.Where("id", 101).Count()
t.AssertNil(err)
t.Assert(n, int64(1))
})
}

View File

@ -0,0 +1,106 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gaussdb_test
import (
"context"
"testing"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/test/gtest"
"github.com/gogf/gf/contrib/drivers/gaussdb/v2"
)
func Test_LastInsertId(t *testing.T) {
// err not nil
gtest.C(t, func(t *gtest.T) {
_, err := db.Model("notexist").Insert(g.List{
{"name": "user1"},
{"name": "user2"},
{"name": "user3"},
})
t.AssertNE(err, nil)
})
gtest.C(t, func(t *gtest.T) {
tableName := createTable()
defer dropTable(tableName)
res, err := db.Model(tableName).Insert(g.List{
{"passport": "user1", "password": "pwd", "nickname": "nickname", "create_time": CreateTime},
{"passport": "user2", "password": "pwd", "nickname": "nickname", "create_time": CreateTime},
{"passport": "user3", "password": "pwd", "nickname": "nickname", "create_time": CreateTime},
})
t.AssertNil(err)
lastInsertId, err := res.LastInsertId()
t.AssertNil(err)
t.Assert(lastInsertId, int64(3))
rowsAffected, err := res.RowsAffected()
t.AssertNil(err)
t.Assert(rowsAffected, int64(3))
})
}
func Test_TxLastInsertId(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
tableName := createTable()
defer dropTable(tableName)
err := db.Transaction(context.TODO(), func(ctx context.Context, tx gdb.TX) error {
// user
res, err := tx.Model(tableName).Insert(g.List{
{"passport": "user1", "password": "pwd", "nickname": "nickname", "create_time": CreateTime},
{"passport": "user2", "password": "pwd", "nickname": "nickname", "create_time": CreateTime},
{"passport": "user3", "password": "pwd", "nickname": "nickname", "create_time": CreateTime},
})
t.AssertNil(err)
lastInsertId, err := res.LastInsertId()
t.AssertNil(err)
t.AssertEQ(lastInsertId, int64(3))
rowsAffected, err := res.RowsAffected()
t.AssertNil(err)
t.AssertEQ(rowsAffected, int64(3))
res1, err := tx.Model(tableName).Insert(g.List{
{"passport": "user4", "password": "pwd", "nickname": "nickname", "create_time": CreateTime},
{"passport": "user5", "password": "pwd", "nickname": "nickname", "create_time": CreateTime},
})
t.AssertNil(err)
lastInsertId1, err := res1.LastInsertId()
t.AssertNil(err)
t.AssertEQ(lastInsertId1, int64(5))
rowsAffected1, err := res1.RowsAffected()
t.AssertNil(err)
t.AssertEQ(rowsAffected1, int64(2))
return nil
})
t.AssertNil(err)
})
}
func Test_Driver_DoFilter(t *testing.T) {
var (
ctx = gctx.New()
driver = gaussdb.Driver{}
)
gtest.C(t, func(t *gtest.T) {
var data = g.Map{
`select * from user where (role)::jsonb ?| 'admin'`: `select * from user where (role)::jsonb ?| 'admin'`,
`select * from user where (role)::jsonb ?| '?'`: `select * from user where (role)::jsonb ?| '$2'`,
`select * from user where (role)::jsonb &? '?'`: `select * from user where (role)::jsonb &? '$2'`,
`select * from user where (role)::jsonb ? '?'`: `select * from user where (role)::jsonb ? '$2'`,
`select * from user where '?'`: `select * from user where '$1'`,
}
for k, v := range data {
newSql, _, err := driver.DoFilter(ctx, nil, k, nil)
t.AssertNil(err)
t.Assert(newSql, v)
}
})
}

View File

@ -0,0 +1,267 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gaussdb_test
import (
"testing"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/test/gtest"
)
// Test_FormatUpsert_WithOnDuplicateStr tests FormatUpsert with OnDuplicateStr
func Test_FormatUpsert_WithOnDuplicateStr(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert initial data
_, err := db.Model(table).Data(g.Map{
"passport": "user1",
"password": "pwd",
"nickname": "nick1",
"create_time": CreateTime,
}).Insert()
t.AssertNil(err)
// Test Save with OnConflict (upsert)
_, err = db.Model(table).Data(g.Map{
"id": 1,
"passport": "user1",
"password": "newpwd",
"nickname": "newnick",
"create_time": CreateTime,
}).OnConflict("id").Save()
t.AssertNil(err)
// Verify the update
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
t.Assert(one["password"].String(), "newpwd")
t.Assert(one["nickname"].String(), "newnick")
})
}
// Test_FormatUpsert_WithOnDuplicateMap tests FormatUpsert with OnDuplicateMap
func Test_FormatUpsert_WithOnDuplicateMap(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert initial data
_, err := db.Model(table).Data(g.Map{
"passport": "user2",
"password": "pwd",
"nickname": "nick2",
"create_time": CreateTime,
}).Insert()
t.AssertNil(err)
// Test OnDuplicate with map - values should be column names to use EXCLUDED.column
_, err = db.Model(table).Data(g.Map{
"id": 1,
"passport": "user2",
"password": "newpwd2",
"nickname": "newnick2",
"create_time": CreateTime,
}).OnConflict("id").OnDuplicate(g.Map{
"password": "password",
"nickname": "nickname",
}).Save()
t.AssertNil(err)
// Verify - values should be from the inserted data
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
t.Assert(one["password"].String(), "newpwd2")
t.Assert(one["nickname"].String(), "newnick2")
})
}
// Test_FormatUpsert_WithCounter tests FormatUpsert with Counter type on numeric column.
// Note: In PostgreSQL, Counter uses EXCLUDED.column which references the NEW value being inserted,
// not the current table value. This differs from MySQL's ON DUPLICATE KEY UPDATE behavior.
func Test_FormatUpsert_WithCounter(t *testing.T) {
// Create a special table with numeric id for counter test
tableName := "t_counter_test"
dropTable(tableName)
_, err := db.Exec(ctx, `
CREATE TABLE `+tableName+` (
id bigserial PRIMARY KEY,
counter_value int NOT NULL DEFAULT 0,
name varchar(45)
)
`)
if err != nil {
t.Error(err)
return
}
defer dropTable(tableName)
gtest.C(t, func(t *gtest.T) {
// Insert initial data
_, err := db.Model(tableName).Data(g.Map{
"counter_value": 10,
"name": "counter_test",
}).Insert()
t.AssertNil(err)
// Get initial ID
one, err := db.Model(tableName).Where("name", "counter_test").One()
t.AssertNil(err)
initialId := one["id"].Int64()
// Test OnDuplicate with Counter
// In PostgreSQL: counter_value = EXCLUDED.counter_value + 5
// EXCLUDED.counter_value is the value we're trying to insert (20)
// So result = 20 + 5 = 25
_, err = db.Model(tableName).Data(g.Map{
"id": initialId,
"counter_value": 20, // This is the EXCLUDED value
"name": "counter_test",
}).OnConflict("id").OnDuplicate(g.Map{
"counter_value": &gdb.Counter{
Field: "counter_value",
Value: 5,
},
}).Save()
t.AssertNil(err)
// Verify: EXCLUDED.counter_value(20) + 5 = 25
one, err = db.Model(tableName).Where("id", initialId).One()
t.AssertNil(err)
t.Assert(one["counter_value"].Int(), 25)
})
gtest.C(t, func(t *gtest.T) {
// Test Counter with negative value (decrement)
one, err := db.Model(tableName).Where("name", "counter_test").One()
t.AssertNil(err)
initialId := one["id"].Int64()
// In PostgreSQL: counter_value = EXCLUDED.counter_value - 3
// EXCLUDED.counter_value is 100, so result = 100 - 3 = 97
_, err = db.Model(tableName).Data(g.Map{
"id": initialId,
"counter_value": 100, // This is the EXCLUDED value
"name": "counter_test",
}).OnConflict("id").OnDuplicate(g.Map{
"counter_value": &gdb.Counter{
Field: "counter_value",
Value: -3,
},
}).Save()
t.AssertNil(err)
// Verify: EXCLUDED.counter_value(100) - 3 = 97
one, err = db.Model(tableName).Where("id", initialId).One()
t.AssertNil(err)
t.Assert(one["counter_value"].Int(), 97)
})
}
// Test_FormatUpsert_WithRaw tests FormatUpsert with Raw type
func Test_FormatUpsert_WithRaw(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert initial data
_, err := db.Model(table).Data(g.Map{
"passport": "raw_user",
"password": "pwd",
"nickname": "nick",
"create_time": CreateTime,
}).Insert()
t.AssertNil(err)
// Get initial ID
one, err := db.Model(table).Where("passport", "raw_user").One()
t.AssertNil(err)
initialId := one["id"].Int64()
// Test OnDuplicate with Raw SQL
_, err = db.Model(table).Data(g.Map{
"id": initialId,
"passport": "raw_user",
"password": "pwd",
"nickname": "nick",
"create_time": CreateTime,
}).OnConflict("id").OnDuplicate(g.Map{
"password": gdb.Raw("'raw_password'"),
}).Save()
t.AssertNil(err)
// Verify
one, err = db.Model(table).Where("id", initialId).One()
t.AssertNil(err)
t.Assert(one["password"].String(), "raw_password")
})
}
// Test_FormatUpsert_NoOnConflict tests FormatUpsert without OnConflict (should fail)
func Test_FormatUpsert_NoOnConflict(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert initial data
_, err := db.Model(table).Data(g.Map{
"passport": "no_conflict_user",
"password": "pwd",
"nickname": "nick",
"create_time": CreateTime,
}).Insert()
t.AssertNil(err)
// Try Save without OnConflict and without primary key in data - should fail
// because driver cannot auto-detect conflict columns when primary key is missing
_, err = db.Model(table).Data(g.Map{
// "id": 1,
"passport": "no_conflict_user",
"password": "newpwd",
"nickname": "newnick",
"create_time": CreateTime,
}).Save()
t.AssertNE(err, nil)
})
}
// Test_FormatUpsert_MultipleConflictKeys tests FormatUpsert with multiple conflict keys
func Test_FormatUpsert_MultipleConflictKeys(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
// Insert initial data
_, err := db.Model(table).Data(g.Map{
"passport": "multi_key_user",
"password": "pwd",
"nickname": "nick",
"create_time": CreateTime,
}).Insert()
t.AssertNil(err)
// Test with multiple conflict keys using only "id" which has a unique constraint
// Note: Using multiple keys requires a composite unique constraint to exist
_, err = db.Model(table).Data(g.Map{
"id": 1,
"passport": "multi_key_user",
"password": "newpwd",
"nickname": "newnick",
"create_time": CreateTime,
}).OnConflict("id").Save()
t.AssertNil(err)
// Verify the update
one, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)
t.Assert(one["password"].String(), "newpwd")
t.Assert(one["nickname"].String(), "newnick")
})
}

View File

@ -0,0 +1,47 @@
module github.com/gogf/gf/contrib/drivers/gaussdb/v2
go 1.23.0
require (
gitee.com/opengauss/openGauss-connector-go-pq v1.0.7
github.com/gogf/gf/v2 v2.9.7
github.com/google/uuid v1.6.0
github.com/lib/pq v1.10.9
)
require (
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/grokify/html-strip-tags-go v0.1.0 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.0.9 // indirect
github.com/olekukonko/tablewriter v1.1.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace (
github.com/gogf/gf/contrib/drivers/mysql/v2 => ../mysql
github.com/gogf/gf/v2 => ../../../
)

View File

@ -0,0 +1,171 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
gitee.com/opengauss/openGauss-connector-go-pq v1.0.7 h1:plLidoldV5RfMU6i/I+tvRKtP3sfDyUzQ//HGXLLsZo=
gitee.com/opengauss/openGauss-connector-go-pq v1.0.7/go.mod h1:2UEp+ug6ls6C0pLfZgBn7VBzBntFUzxJuy+6FlQ7qyI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU=
github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4=
github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY=
github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -0,0 +1,44 @@
module github.com/gogf/gf/contrib/drivers/mariadb/v2
go 1.23.0
require (
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.7
github.com/gogf/gf/v2 v2.9.7
)
require (
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/grokify/html-strip-tags-go v0.1.0 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.0.9 // indirect
github.com/olekukonko/tablewriter v1.1.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.25.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace (
github.com/gogf/gf/contrib/drivers/mysql/v2 => ../mysql
github.com/gogf/gf/v2 => ../../../
)

View File

@ -0,0 +1,81 @@
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU=
github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4=
github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY=
github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -0,0 +1,49 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
// Package mariadb implements gdb.Driver, which supports operations for database MariaDB.
package mariadb
import (
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/contrib/drivers/mysql/v2"
)
// Driver is the driver for MariaDB database.
//
// MariaDB is a community-developed, commercially supported fork of the MySQL relational database.
// This driver uses the MySQL protocol to communicate with MariaDB database, as MariaDB maintains
// high compatibility with MySQL protocol.
//
// Although MariaDB is compatible with MySQL protocol, it is packaged as a separate driver component
// rather than reusing the mysql adapter directly. This design allows for future extensibility,
// such as implementing MariaDB-specific features or optimizations.
type Driver struct {
*mysql.Driver
}
func init() {
var (
err error
driverObj = New()
driverNames = g.SliceStr{"mariadb"}
)
for _, driverName := range driverNames {
if err = gdb.Register(driverName, driverObj); err != nil {
panic(err)
}
}
}
// New creates and returns a driver that implements gdb.Driver, which supports operations for MariaDB.
func New() gdb.Driver {
mysqlDriver := mysql.New().(*mysql.Driver)
return &Driver{
Driver: mysqlDriver,
}
}

View File

@ -0,0 +1,82 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package mariadb
import (
"context"
"fmt"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/util/gutil"
)
var (
tableFieldsSqlByMariadb = `
SELECT
c.COLUMN_NAME AS 'Field',
( CASE WHEN ch.CHECK_CLAUSE LIKE 'json_valid%%' THEN 'json' ELSE c.COLUMN_TYPE END ) AS 'Type',
c.COLLATION_NAME AS 'Collation',
c.IS_NULLABLE AS 'Null',
c.COLUMN_KEY AS 'Key',
( CASE WHEN c.COLUMN_DEFAULT = 'NULL' OR c.COLUMN_DEFAULT IS NULL THEN NULL ELSE c.COLUMN_DEFAULT END) AS 'Default',
c.EXTRA AS 'Extra',
c.PRIVILEGES AS 'Privileges',
c.COLUMN_COMMENT AS 'Comment'
FROM
information_schema.COLUMNS AS c
LEFT JOIN information_schema.CHECK_CONSTRAINTS AS ch ON c.TABLE_NAME = ch.TABLE_NAME
AND c.COLUMN_NAME = ch.CONSTRAINT_NAME
WHERE
c.TABLE_SCHEMA = '%s'
AND c.TABLE_NAME = '%s'
ORDER BY c.ORDINAL_POSITION`
)
func init() {
var err error
tableFieldsSqlByMariadb, err = gdb.FormatMultiLineSqlToSingle(tableFieldsSqlByMariadb)
if err != nil {
panic(err)
}
}
// TableFields retrieves and returns the fields' information of specified table of current
// schema.
func (d *Driver) TableFields(
ctx context.Context, table string, schema ...string,
) (fields map[string]*gdb.TableField, err error) {
var (
result gdb.Result
link gdb.Link
usedSchema = gutil.GetOrDefaultStr(d.GetSchema(), schema...)
)
if link, err = d.SlaveLink(usedSchema); err != nil {
return nil, err
}
result, err = d.DoSelect(
ctx, link,
fmt.Sprintf(tableFieldsSqlByMariadb, usedSchema, table),
)
if err != nil {
return nil, err
}
fields = make(map[string]*gdb.TableField)
for i, m := range result {
fields[m["Field"].String()] = &gdb.TableField{
Index: i,
Name: m["Field"].String(),
Type: m["Type"].String(),
Null: m["Null"].Bool(),
Key: m["Key"].String(),
Default: m["Default"].Val(),
Extra: m["Extra"].String(),
Comment: m["Comment"].String(),
}
}
return fields, nil
}

View File

@ -0,0 +1,126 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package mariadb_test
import (
"context"
"fmt"
"time"
_ "github.com/gogf/gf/contrib/drivers/mariadb/v2"
"github.com/gogf/gf/v2/container/garray"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/test/gtest"
)
const (
TableSize = 10
TableName = "user"
TestSchema1 = "test1"
TestSchema2 = "test2"
TestDbPass = "12345678"
CreateTime = "2018-10-24 10:00:00"
)
var (
db gdb.DB
ctx = context.TODO()
)
func init() {
nodeDefault := gdb.ConfigNode{
ExecTimeout: time.Second * 2,
Link: fmt.Sprintf("mariadb:root:%s@tcp(127.0.0.1:3307)/?loc=Local&parseTime=true", TestDbPass),
TranTimeout: time.Second * 3,
}
err := gdb.AddConfigNode(gdb.DefaultGroupName, nodeDefault)
if err != nil {
panic(err)
}
// Default db.
if r, err := gdb.NewByGroup(); err != nil {
gtest.Error(err)
} else {
db = r
}
schemaTemplate := "CREATE DATABASE IF NOT EXISTS `%s` CHARACTER SET UTF8"
if _, err := db.Exec(ctx, fmt.Sprintf(schemaTemplate, TestSchema1)); err != nil {
gtest.Error(err)
}
if _, err := db.Exec(ctx, fmt.Sprintf(schemaTemplate, TestSchema2)); err != nil {
gtest.Error(err)
}
db = db.Schema(TestSchema1)
}
func createTable(table ...string) string {
return createTableWithDb(db, table...)
}
func createInitTable(table ...string) string {
return createInitTableWithDb(db, table...)
}
func dropTable(table string) {
dropTableWithDb(db, table)
}
func createTableWithDb(db gdb.DB, table ...string) (name string) {
if len(table) > 0 {
name = table[0]
} else {
name = fmt.Sprintf(`%s_%d`, TableName, gtime.TimestampNano())
}
dropTableWithDb(db, name)
if _, err := db.Exec(ctx, fmt.Sprintf(`
CREATE TABLE %s (
id int(10) unsigned NOT NULL AUTO_INCREMENT,
passport varchar(45) NULL,
password char(32) NULL,
nickname varchar(45) NULL,
create_time timestamp(6) NULL,
create_date date NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
`, name,
)); err != nil {
gtest.Fatal(err)
}
return name
}
func createInitTableWithDb(db gdb.DB, table ...string) (name string) {
name = createTableWithDb(db, table...)
array := garray.New(true)
for i := 1; i <= TableSize; i++ {
array.Append(g.Map{
"id": i,
"passport": fmt.Sprintf(`user_%d`, i),
"password": fmt.Sprintf(`pass_%d`, i),
"nickname": fmt.Sprintf(`name_%d`, i),
"create_time": gtime.NewFromStr(CreateTime).String(),
})
}
result, err := db.Insert(ctx, name, array.Slice())
gtest.AssertNil(err)
n, e := result.RowsAffected()
gtest.Assert(e, nil)
gtest.Assert(n, TableSize)
return
}
func dropTableWithDb(db gdb.DB, table string) {
if _, err := db.Exec(ctx, fmt.Sprintf("DROP TABLE IF EXISTS `%s`", table)); err != nil {
gtest.Error(err)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/drivers/mssql/v2
go 1.23.0
require (
github.com/gogf/gf/v2 v2.9.6
github.com/gogf/gf/v2 v2.9.7
github.com/microsoft/go-mssqldb v1.7.1
)

View File

@ -4,11 +4,7 @@
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
// Package mssql implements gdb.Driver, which supports operations for database MSSql.
//
// Note:
// 1. It does not support Replace features.
// 2. It does not support LastInsertId.
// Package mssql implements gdb.Driver, which supports operations for MSSQL.
package mssql
import (

View File

@ -1,3 +1,9 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package mssql
import (
@ -87,12 +93,16 @@ func (d *Driver) DoExec(ctx context.Context, link gdb.Link, sqlStr string, args
IsTransaction: link.IsTransaction(),
})
if err != nil {
return &InsertResult{lastInsertId: 0, rowsAffected: 0, err: err}, err
return &Result{lastInsertId: 0, rowsAffected: 0, err: err}, err
}
stdSqlResult := out.Records
if len(stdSqlResult) == 0 {
err = gerror.WrapCode(gcode.CodeDbOperationError, gerror.New("affectcount is zero"), `sql.Result.RowsAffected failed`)
return &InsertResult{lastInsertId: 0, rowsAffected: 0, err: err}, err
err = gerror.WrapCode(
gcode.CodeDbOperationError,
gerror.New("affected count is zero"),
`sql.Result.RowsAffected failed`,
)
return &Result{lastInsertId: 0, rowsAffected: 0, err: err}, err
}
// For batch insert, OUTPUT clause returns one row per inserted row.
// So the rowsAffected should be the count of returned records.
@ -100,7 +110,7 @@ func (d *Driver) DoExec(ctx context.Context, link gdb.Link, sqlStr string, args
// get last_insert_id from the first returned row
lastInsertId := stdSqlResult[0].GMap().GetVar(lastInsertIdFieldAlias).Int64()
return &InsertResult{lastInsertId: lastInsertId, rowsAffected: rowsAffected}, err
return &Result{lastInsertId: lastInsertId, rowsAffected: rowsAffected}, err
}
// GetTableNameFromSql get table name from sql statement
@ -111,17 +121,19 @@ func (d *Driver) DoExec(ctx context.Context, link gdb.Link, sqlStr string, args
// "user as u".
func (d *Driver) GetTableNameFromSql(sqlStr string) (table string) {
// INSERT INTO "ip_to_id"("ip") OUTPUT 1 as AffectCount,INSERTED.id as ID VALUES(?)
leftChars, rightChars := d.GetChars()
trimStr := leftChars + rightChars + "[] "
pattern := "INTO(.+?)\\("
regCompile := regexp.MustCompile(pattern)
tableInfo := regCompile.FindStringSubmatch(sqlStr)
var (
leftChars, rightChars = d.GetChars()
trimStr = leftChars + rightChars + "[] "
pattern = "INTO(.+?)\\("
regCompile = regexp.MustCompile(pattern)
tableInfo = regCompile.FindStringSubmatch(sqlStr)
)
// get the first one. after the first it may be content of the value, it's not table name.
table = tableInfo[1]
table = strings.Trim(table, " ")
if strings.Contains(table, ".") {
tmpAry := strings.Split(table, ".")
// the last one is tablename
// the last one is table name
table = tmpAry[len(tmpAry)-1]
} else if strings.Contains(table, "as") || strings.Contains(table, " ") {
tmpAry := strings.Split(table, "as")
@ -151,21 +163,6 @@ func (l *txLinkMssql) IsOnMaster() bool {
return true
}
// InsertResult instance of sql.Result
type InsertResult struct {
lastInsertId int64
rowsAffected int64
err error
}
func (r *InsertResult) LastInsertId() (int64, error) {
return r.lastInsertId, r.err
}
func (r *InsertResult) RowsAffected() (int64, error) {
return r.rowsAffected, r.err
}
// GetInsertOutputSql gen get last_insert_id code
func (d *Driver) GetInsertOutputSql(ctx context.Context, table string) string {
fds, errFd := d.GetDB().TableFields(ctx, table)

View File

@ -25,67 +25,90 @@ func (d *Driver) DoInsert(
ctx context.Context, link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
) (result sql.Result, err error) {
switch option.InsertOption {
case
gdb.InsertOptionSave,
gdb.InsertOptionReplace:
// MSSQL does not support REPLACE INTO syntax.
// Convert Replace to Save operation, using MERGE statement.
// Auto-detect primary keys if OnConflict is not specified.
if len(option.OnConflict) == 0 {
primaryKeys, err := d.Core.GetPrimaryKeys(ctx, table)
if err != nil {
return nil, gerror.WrapCode(
gcode.CodeInternalError,
err,
`failed to get primary keys for Save/Replace operation`,
)
}
foundPrimaryKey := false
for _, primaryKey := range primaryKeys {
for dataKey := range list[0] {
if strings.EqualFold(dataKey, primaryKey) {
foundPrimaryKey = true
break
}
}
if foundPrimaryKey {
break
}
}
if !foundPrimaryKey {
return nil, gerror.NewCodef(
gcode.CodeMissingParameter,
`Save/Replace operation requires conflict detection: `+
`either specify OnConflict() columns or ensure table '%s' has a primary key in the data`,
table,
)
}
option.OnConflict = primaryKeys
}
// Convert to Save operation
case gdb.InsertOptionSave:
return d.doSave(ctx, link, table, list, option)
case gdb.InsertOptionReplace:
// MSSQL does not support REPLACE INTO syntax, use SAVE instead.
return d.doSave(ctx, link, table, list, option)
case gdb.InsertOptionIgnore:
// MSSQL does not support INSERT IGNORE syntax, use MERGE instead.
return d.doInsertIgnore(ctx, link, table, list, option)
default:
return d.Core.DoInsert(ctx, link, table, list, option)
}
}
// doSave support upsert for SQL server
// doSave support upsert for MSSQL
func (d *Driver) doSave(ctx context.Context,
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
) (result sql.Result, err error) {
return d.doMergeInsert(ctx, link, table, list, option, true)
}
// doInsertIgnore implements INSERT IGNORE operation using MERGE statement for MSSQL database.
// It only inserts records when there's no conflict on primary/unique keys.
func (d *Driver) doInsertIgnore(ctx context.Context,
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
) (result sql.Result, err error) {
return d.doMergeInsert(ctx, link, table, list, option, false)
}
// doMergeInsert implements MERGE-based insert operations for MSSQL database.
// When withUpdate is true, it performs upsert (insert or update).
// When withUpdate is false, it performs insert ignore (insert only when no conflict).
func (d *Driver) doMergeInsert(
ctx context.Context,
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption, withUpdate bool,
) (result sql.Result, err error) {
// If OnConflict is not specified, automatically get the primary key of the table
conflictKeys := option.OnConflict
if len(conflictKeys) == 0 {
primaryKeys, err := d.Core.GetPrimaryKeys(ctx, table)
if err != nil {
return nil, gerror.WrapCode(
gcode.CodeInternalError,
err,
`failed to get primary keys for table`,
)
}
foundPrimaryKey := false
for _, primaryKey := range primaryKeys {
for dataKey := range list[0] {
if strings.EqualFold(dataKey, primaryKey) {
foundPrimaryKey = true
break
}
}
if foundPrimaryKey {
break
}
}
if !foundPrimaryKey {
return nil, gerror.NewCodef(
gcode.CodeMissingParameter,
`Replace/Save/InsertIgnore operation requires conflict detection: `+
`either specify OnConflict() columns or ensure table '%s' has a primary key in the data`,
table,
)
}
// TODO consider composite primary keys.
conflictKeys = primaryKeys
}
var (
one = list[0]
oneLen = len(one)
charL, charR = d.GetChars()
conflictKeys = option.OnConflict
conflictKeySet = gset.New(false)
// queryHolders: Handle data with Holder that need to be upsert
// queryValues: Handle data that need to be upsert
// queryHolders: Handle data with Holder that need to be merged
// queryValues: Handle data that need to be merged
// insertKeys: Handle valid keys that need to be inserted
// insertValues: Handle values that need to be inserted
// updateValues: Handle values that need to be updated
// updateValues: Handle values that need to be updated (only when withUpdate=true)
queryHolders = make([]string, oneLen)
queryValues = make([]any, oneLen)
insertKeys = make([]string, oneLen)
@ -105,9 +128,9 @@ func (d *Driver) doSave(ctx context.Context,
insertKeys[index] = charL + key + charR
insertValues[index] = "T2." + charL + key + charR
// filter conflict keys in updateValues.
// And the key is not a soft created field.
if !(conflictKeySet.Contains(key) || d.Core.IsSoftCreatedFieldName(key)) {
// Build updateValues only when withUpdate is true
// Filter conflict keys and soft created fields from updateValues
if withUpdate && !(conflictKeySet.Contains(key) || d.Core.IsSoftCreatedFieldName(key)) {
updateValues = append(
updateValues,
fmt.Sprintf(`T1.%s = T2.%s`, charL+key+charR, charL+key+charR),
@ -116,8 +139,10 @@ func (d *Driver) doSave(ctx context.Context,
index++
}
batchResult := new(gdb.SqlResult)
sqlStr := parseSqlForUpsert(table, queryHolders, insertKeys, insertValues, updateValues, conflictKeys)
var (
batchResult = new(gdb.SqlResult)
sqlStr = parseSqlForMerge(table, queryHolders, insertKeys, insertValues, updateValues, conflictKeys)
)
r, err := d.DoExec(ctx, link, sqlStr, queryValues...)
if err != nil {
return r, err
@ -131,44 +156,48 @@ func (d *Driver) doSave(ctx context.Context,
return batchResult, nil
}
// parseSqlForUpsert
// MERGE INTO {{table}} T1
// USING ( VALUES( {{queryHolders}}) T2 ({{insertKeyStr}})
// ON (T1.{{duplicateKey}} = T2.{{duplicateKey}} AND ...)
// WHEN NOT MATCHED THEN
// INSERT {{insertKeys}} VALUES {{insertValues}}
// WHEN MATCHED THEN
// UPDATE SET {{updateValues}}
func parseSqlForUpsert(table string,
// parseSqlForMerge generates MERGE statement for MSSQL database.
// When updateValues is empty, it only inserts (INSERT IGNORE behavior).
// When updateValues is provided, it performs upsert (INSERT or UPDATE).
// Examples:
// - INSERT IGNORE: MERGE INTO table T1 USING (...) T2 ON (...) WHEN NOT MATCHED THEN INSERT(...) VALUES (...)
// - UPSERT: MERGE INTO table T1 USING (...) T2 ON (...) WHEN NOT MATCHED THEN INSERT(...) VALUES (...) WHEN MATCHED THEN UPDATE SET ...
func parseSqlForMerge(table string,
queryHolders, insertKeys, insertValues, updateValues, duplicateKey []string,
) (sqlStr string) {
var (
queryHolderStr = strings.Join(queryHolders, ",")
insertKeyStr = strings.Join(insertKeys, ",")
insertValueStr = strings.Join(insertValues, ",")
updateValueStr = strings.Join(updateValues, ",")
duplicateKeyStr string
pattern = gstr.Trim(
`MERGE INTO %s T1 USING (VALUES(%s)) T2 (%s) ON (%s) WHEN NOT MATCHED ` +
`THEN INSERT(%s) VALUES (%s) WHEN MATCHED THEN UPDATE SET %s;`,
)
)
// Build ON condition
for index, keys := range duplicateKey {
if index != 0 {
duplicateKeyStr += " AND "
}
duplicateTmp := fmt.Sprintf("T1.%s = T2.%s", keys, keys)
duplicateKeyStr += duplicateTmp
duplicateKeyStr += fmt.Sprintf("T1.%s = T2.%s", keys, keys)
}
return fmt.Sprintf(pattern,
table,
queryHolderStr,
insertKeyStr,
duplicateKeyStr,
insertKeyStr,
insertValueStr,
updateValueStr,
// Build SQL based on whether UPDATE is needed
pattern := gstr.Trim(
`MERGE INTO %s T1 USING (VALUES(%s)) T2 (%s) ON (%s) WHEN NOT MATCHED THEN INSERT(%s) VALUES (%s)`,
)
if len(updateValues) > 0 {
// Upsert: INSERT or UPDATE
pattern += gstr.Trim(` WHEN MATCHED THEN UPDATE SET %s`)
return fmt.Sprintf(
pattern+";",
table,
queryHolderStr,
insertKeyStr,
duplicateKeyStr,
insertKeyStr,
insertValueStr,
strings.Join(updateValues, ","),
)
}
// Insert Ignore: INSERT only
return fmt.Sprintf(pattern+";", table, queryHolderStr, insertKeyStr, duplicateKeyStr, insertKeyStr, insertValueStr)
}

View File

@ -0,0 +1,22 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package mssql
// Result instance of sql.Result
type Result struct {
lastInsertId int64
rowsAffected int64
err error
}
func (r *Result) LastInsertId() (int64, error) {
return r.lastInsertId, r.err
}
func (r *Result) RowsAffected() (int64, error) {
return r.rowsAffected, r.err
}

View File

@ -117,6 +117,48 @@ func Test_Model_Insert(t *testing.T) {
})
}
func Test_Model_InsertIgnore(t *testing.T) {
table := createInitTable()
defer dropTable(table)
// db.SetDebug(true)
gtest.C(t, func(t *gtest.T) {
data := g.Map{
"id": 1,
"passport": fmt.Sprintf(`t%d`, 777),
"password": fmt.Sprintf(`p%d`, 777),
"nickname": fmt.Sprintf(`T%d`, 777),
"create_time": gtime.Now(),
}
_, err := db.Model(table).Data(data).InsertIgnore()
t.AssertNil(err)
one, err := db.Model(table).WherePri(1).One()
t.AssertNil(err)
t.Assert(one["PASSPORT"].String(), "user_1")
count, err := db.Model(table).Count()
t.AssertNil(err)
t.Assert(count, TableSize)
})
gtest.C(t, func(t *gtest.T) {
data := g.Map{
"passport": fmt.Sprintf(`t%d`, 777),
"password": fmt.Sprintf(`p%d`, 777),
"nickname": fmt.Sprintf(`T%d`, 777),
"create_time": gtime.Now(),
}
_, err := db.Model(table).Data(data).InsertIgnore()
t.AssertNE(err, nil)
count, err := db.Model(table).Count()
t.AssertNil(err)
t.Assert(count, TableSize)
})
}
func Test_Model_Insert_KeyFieldNameMapping(t *testing.T) {
table := createTable()
defer dropTable(table)

View File

@ -4,7 +4,7 @@ go 1.23.0
require (
github.com/go-sql-driver/mysql v1.7.1
github.com/gogf/gf/v2 v2.9.6
github.com/gogf/gf/v2 v2.9.7
)
require (

View File

@ -27,7 +27,7 @@ func init() {
var (
err error
driverObj = New()
driverNames = g.SliceStr{"mysql", "mariadb", "tidb"}
driverNames = g.SliceStr{"mysql", "mariadb", "tidb"} // TODO remove mariadb and tidb in future versions.
)
for _, driverName := range driverNames {
if err = gdb.Register(driverName, driverObj); err != nil {

View File

@ -15,6 +15,9 @@ import (
)
var (
// tableFieldsSqlByMariadb is the query statement for retrieving table fields' information in MariaDB.
// Deprecated: Use package `contrib/drivers/mariadb` instead.
// TODO remove in next version.
tableFieldsSqlByMariadb = `
SELECT
c.COLUMN_NAME AS 'Field',
@ -68,6 +71,8 @@ func (d *Driver) TableFields(ctx context.Context, table string, schema ...string
}
dbType := d.GetConfig().Type
switch dbType {
// Deprecated: Use package `contrib/drivers/mariadb` instead.
// TODO remove in next version.
case "mariadb":
tableFieldsSql = fmt.Sprintf(tableFieldsSqlByMariadb, usedSchema, table)
default:

View File

@ -1,236 +0,0 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package mysql_test
import (
"testing"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/test/gtest"
)
// Test_Model_Group_WithJoin tests GROUP BY with JOIN queries
func Test_Model_Group_WithJoin(t *testing.T) {
var (
table1 = gtime.TimestampNanoStr() + "_user"
table2 = gtime.TimestampNanoStr() + "_user_detail"
)
createInitTable(table1)
defer dropTable(table1)
createInitTable(table2)
defer dropTable(table2)
gtest.C(t, func(t *gtest.T) {
// Test basic GROUP BY with JOIN - unqualified column should be auto-prefixed
// This prevents "Column 'id' in group statement is ambiguous" error
r, err := db.Model(table1+" u").
Fields("u.id", "u.nickname", "COUNT(*) as count").
LeftJoin(table2+" ud", "u.id = ud.id").
Where("u.id", g.Slice{1, 2}).
Group("id").
Order("u.id asc").All()
t.AssertNil(err)
t.Assert(len(r), 2)
t.Assert(r[0]["id"], "1")
t.Assert(r[1]["id"], "2")
// Test GROUP BY with already qualified column
r, err = db.Model(table1+" u").
Fields("u.id", "u.nickname", "COUNT(*) as count").
LeftJoin(table2+" ud", "u.id = ud.id").
Where("u.id", g.Slice{1, 2}).
Group("u.id").
Order("u.id asc").All()
t.AssertNil(err)
t.Assert(len(r), 2)
t.Assert(r[0]["id"], "1")
t.Assert(r[1]["id"], "2")
// Test GROUP BY with multiple columns
r, err = db.Model(table1+" u").
Fields("u.id", "u.nickname", "COUNT(*) as count").
LeftJoin(table2+" ud", "u.id = ud.id").
Where("u.id", g.Slice{1, 2}).
Group("id", "nickname").
Order("u.id asc").All()
t.AssertNil(err)
t.Assert(len(r), 2)
// Test GROUP BY with Raw expression
r, err = db.Model(table1+" u").
Fields("u.id", "u.nickname", "COUNT(*) as count").
LeftJoin(table2+" ud", "u.id = ud.id").
Where("u.id", g.Slice{1, 2}).
Group(gdb.Raw("u.id")).
Order("u.id asc").All()
t.AssertNil(err)
t.Assert(len(r), 2)
t.Assert(r[0]["id"], "1")
t.Assert(r[1]["id"], "2")
// Test GROUP BY on non-primary table should work correctly
r, err = db.Model(table1+" u").
Fields("ud.id", "COUNT(*) as count").
LeftJoin(table2+" ud", "u.id = ud.id").
Where("u.id", g.Slice{1, 2}).
Group("ud.id").
Order("ud.id asc").All()
t.AssertNil(err)
// Should have results from the joined table
t.Assert(len(r) > 0, true)
})
}
// Test_Model_Order_WithJoin tests ORDER BY with JOIN queries
func Test_Model_Order_WithJoin(t *testing.T) {
var (
table1 = gtime.TimestampNanoStr() + "_user"
table2 = gtime.TimestampNanoStr() + "_user_detail"
)
createInitTable(table1)
defer dropTable(table1)
createInitTable(table2)
defer dropTable(table2)
gtest.C(t, func(t *gtest.T) {
// Test ORDER BY with JOIN - unqualified column should be auto-prefixed
r, err := db.Model(table1+" u").
LeftJoin(table2+" ud", "u.id = ud.id").
Where("u.id", g.Slice{1, 2}).
Order("id desc").All()
t.AssertNil(err)
t.Assert(len(r), 2)
t.Assert(r[0]["id"], "2")
t.Assert(r[1]["id"], "1")
// Test ORDER BY with already qualified column
r, err = db.Model(table1+" u").
LeftJoin(table2+" ud", "u.id = ud.id").
Where("u.id", g.Slice{1, 2}).
Order("u.id asc").All()
t.AssertNil(err)
t.Assert(len(r), 2)
t.Assert(r[0]["id"], "1")
t.Assert(r[1]["id"], "2")
// Test ORDER BY with Raw expression
r, err = db.Model(table1+" u").
LeftJoin(table2+" ud", "u.id = ud.id").
Where("u.id", g.Slice{1, 2}).
Order(gdb.Raw("u.id asc")).All()
t.AssertNil(err)
t.Assert(len(r), 2)
t.Assert(r[0]["id"], "1")
t.Assert(r[1]["id"], "2")
// Test multiple ORDER BY clauses with JOIN
r, err = db.Model(table1+" u").
LeftJoin(table2+" ud", "u.id = ud.id").
Order("id asc").Order("nickname asc").All()
t.AssertNil(err)
t.Assert(len(r) > 0, true)
// Test ORDER BY with asc/desc keywords
r, err = db.Model(table1+" u").
LeftJoin(table2+" ud", "u.id = ud.id").
Where("u.id", g.Slice{1, 2}).
Order("id asc").All()
t.AssertNil(err)
t.Assert(len(r), 2)
t.Assert(r[0]["id"], "1")
t.Assert(r[1]["id"], "2")
})
}
// Test_Model_Group_And_Order_WithJoin tests combined GROUP BY and ORDER BY with JOINs
func Test_Model_Group_And_Order_WithJoin(t *testing.T) {
var (
table1 = gtime.TimestampNanoStr() + "_user"
table2 = gtime.TimestampNanoStr() + "_user_detail"
)
createInitTable(table1)
defer dropTable(table1)
createInitTable(table2)
defer dropTable(table2)
gtest.C(t, func(t *gtest.T) {
// Test combined GROUP BY and ORDER BY with JOIN
r, err := db.Model(table1+" u").
Fields("u.id", "COUNT(*) as count").
LeftJoin(table2+" ud", "u.id = ud.id").
Where("u.id", g.Slice{1, 2}).
Group("id").
Order("id desc").All()
t.AssertNil(err)
t.Assert(len(r), 2)
t.Assert(r[0]["id"], "2")
t.Assert(r[1]["id"], "1")
// Test with already qualified GROUP BY and unqualified ORDER BY
r, err = db.Model(table1+" u").
Fields("u.id", "COUNT(*) as count").
LeftJoin(table2+" ud", "u.id = ud.id").
Where("u.id", g.Slice{1, 2}).
Group("u.id").
Order("id asc").All()
t.AssertNil(err)
t.Assert(len(r), 2)
t.Assert(r[0]["id"], "1")
t.Assert(r[1]["id"], "2")
// Test with unqualified GROUP BY and qualified ORDER BY
r, err = db.Model(table1+" u").
Fields("u.id", "COUNT(*) as count").
LeftJoin(table2+" ud", "u.id = ud.id").
Where("u.id", g.Slice{1, 2}).
Group("id").
Order("u.id desc").All()
t.AssertNil(err)
t.Assert(len(r), 2)
t.Assert(r[0]["id"], "2")
t.Assert(r[1]["id"], "1")
// Test with both unqualified
r, err = db.Model(table1+" u").
Fields("u.id", "COUNT(*) as count").
LeftJoin(table2+" ud", "u.id = ud.id").
Where("u.id", g.Slice{1, 2}).
Group("id").
Order("id").All()
t.AssertNil(err)
t.Assert(len(r), 2)
})
}
// Test_Model_Join_Without_Alias tests JOIN without table aliases
func Test_Model_Join_Without_Alias(t *testing.T) {
var (
table1 = gtime.TimestampNanoStr() + "_user"
table2 = gtime.TimestampNanoStr() + "_user_detail"
)
createInitTable(table1)
defer dropTable(table1)
createInitTable(table2)
defer dropTable(table2)
gtest.C(t, func(t *gtest.T) {
// Test GROUP BY and ORDER BY with JOIN but without aliases
// This should still work correctly
r, err := db.Model(table1).
Fields(table1+".id", "COUNT(*) as count").
LeftJoin(table2, table1+".id = "+table2+".id").
Where(table1+".id", g.Slice{1, 2}).
Group(table1 + ".id").
Order(table1 + ".id asc").All()
t.AssertNil(err)
t.Assert(len(r), 2)
t.Assert(r[0]["id"], "1")
t.Assert(r[1]["id"], "2")
})
}

View File

@ -178,7 +178,7 @@ func Test_PartitionTable(t *testing.T) {
createShopDBTable()
insertShopDBData()
//defer dropShopDBTable()
// defer dropShopDBTable()
gtest.C(t, func(t *gtest.T) {
data, err := db3.Ctx(ctx).Model("dbx_order").Partition("p3", "p4").All()
t.AssertNil(err)

View File

@ -0,0 +1,44 @@
module github.com/gogf/gf/contrib/drivers/oceanbase/v2
go 1.23.0
require (
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.7
github.com/gogf/gf/v2 v2.9.7
)
require (
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/grokify/html-strip-tags-go v0.1.0 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.0.9 // indirect
github.com/olekukonko/tablewriter v1.1.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.25.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace (
github.com/gogf/gf/contrib/drivers/mysql/v2 => ../mysql
github.com/gogf/gf/v2 => ../../../
)

View File

@ -0,0 +1,81 @@
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU=
github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4=
github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY=
github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -0,0 +1,49 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
// Package oceanbase implements gdb.Driver, which supports operations for database OceanBase.
package oceanbase
import (
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/contrib/drivers/mysql/v2"
)
// Driver is the driver for OceanBase database.
//
// OceanBase is a distributed relational database developed by Ant Group. It supports both MySQL and Oracle
// protocol modes. This driver uses the MySQL protocol to communicate with OceanBase database in MySQL
// compatibility mode.
//
// Although OceanBase is compatible with MySQL protocol, it is packaged as a separate driver component
// rather than reusing the mysql adapter directly. This design allows for future extensibility,
// such as implementing OceanBase-specific features like distributed transactions or Oracle mode support.
type Driver struct {
*mysql.Driver
}
func init() {
var (
err error
driverObj = New()
driverNames = g.SliceStr{"oceanbase"}
)
for _, driverName := range driverNames {
if err = gdb.Register(driverName, driverObj); err != nil {
panic(err)
}
}
}
// New creates and returns a driver that implements gdb.Driver, which supports operations for OceanBase.
func New() gdb.Driver {
mysqlDriver := mysql.New().(*mysql.Driver)
return &Driver{
Driver: mysqlDriver,
}
}

View File

@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/drivers/oracle/v2
go 1.23.0
require (
github.com/gogf/gf/v2 v2.9.6
github.com/gogf/gf/v2 v2.9.7
github.com/sijms/go-ora/v2 v2.7.10
)

View File

@ -5,10 +5,6 @@
// You can obtain one at https://github.com/gogf/gf.
// Package oracle implements gdb.Driver, which supports operations for database Oracle.
//
// Note:
// 1. It does not support Save/Replace features.
// 2. It does not support LastInsertId.
package oracle
import (

View File

@ -0,0 +1,120 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package oracle
import (
"context"
"database/sql"
"fmt"
"strings"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
)
const (
returningClause = " RETURNING %s INTO ?"
)
// DoExec commits the sql string and its arguments to underlying driver
// through given link object and returns the execution result.
// It handles INSERT statements specially to support LastInsertId.
func (d *Driver) DoExec(
ctx context.Context, link gdb.Link, sql string, args ...interface{},
) (result sql.Result, err error) {
var (
isUseCoreDoExec = true
primaryKey string
pkField gdb.TableField
)
// Transaction checks.
if link == nil {
if tx := gdb.TXFromCtx(ctx, d.GetGroup()); tx != nil {
link = tx
} else if link, err = d.MasterLink(); err != nil {
return nil, err
}
} else if !link.IsTransaction() {
if tx := gdb.TXFromCtx(ctx, d.GetGroup()); tx != nil {
link = tx
}
}
// Check if it is an insert operation with primary key from context.
if value := ctx.Value(internalPrimaryKeyInCtx); value != nil {
if field, ok := value.(gdb.TableField); ok {
pkField = field
isUseCoreDoExec = false
}
}
// Check if it is an INSERT statement with primary key.
if !isUseCoreDoExec && pkField.Name != "" && strings.Contains(strings.ToUpper(sql), "INSERT INTO") {
primaryKey = pkField.Name
// Oracle supports RETURNING clause to get the last inserted id
sql += fmt.Sprintf(returningClause, d.QuoteWord(primaryKey))
} else {
// Use default DoExec for non-INSERT or no primary key scenarios
return d.Core.DoExec(ctx, link, sql, args...)
}
// Only the insert operation with primary key can execute the following code
// SQL filtering.
sql, args = d.FormatSqlBeforeExecuting(sql, args)
sql, args, err = d.DoFilter(ctx, link, sql, args)
if err != nil {
return nil, err
}
// Prepare output variable for RETURNING clause
var lastInsertId int64
// Append the output parameter for the RETURNING clause
args = append(args, &lastInsertId)
// Link execution.
_, err = d.DoCommit(ctx, gdb.DoCommitInput{
Link: link,
Sql: sql,
Args: args,
Stmt: nil,
Type: gdb.SqlTypeExecContext,
IsTransaction: link.IsTransaction(),
})
if err != nil {
return &Result{
lastInsertId: 0,
rowsAffected: 0,
lastInsertIdError: err,
}, err
}
// Get rows affected from the result
// For single insert with RETURNING clause, affected is always 1
var affected int64 = 1
// Check if the primary key field type supports LastInsertId
if !strings.Contains(strings.ToLower(pkField.Type), "int") {
return &Result{
lastInsertId: 0,
rowsAffected: affected,
lastInsertIdError: gerror.NewCodef(
gcode.CodeNotSupported,
"LastInsertId is not supported by primary key type: %s",
pkField.Type,
),
}, nil
}
return &Result{
lastInsertId: lastInsertId,
rowsAffected: affected,
}, nil
}

View File

@ -16,10 +16,15 @@ import (
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
)
const (
internalPrimaryKeyInCtx gctx.StrKey = "primary_key_field"
)
// DoInsert inserts or updates data for given table.
// The list parameter must contain at least one record, which was previously validated.
func (d *Driver) DoInsert(
@ -30,10 +35,38 @@ func (d *Driver) DoInsert(
return d.doSave(ctx, link, table, list, option)
case gdb.InsertOptionReplace:
return nil, gerror.NewCode(
gcode.CodeNotSupported,
`Replace operation is not supported by oracle driver`,
)
// Oracle does not support REPLACE INTO syntax, use SAVE instead.
return d.doSave(ctx, link, table, list, option)
case gdb.InsertOptionIgnore:
// Oracle does not support INSERT IGNORE syntax, use MERGE instead.
return d.doInsertIgnore(ctx, link, table, list, option)
case gdb.InsertOptionDefault:
// For default insert, set primary key field in context to support LastInsertId.
// Only set it when the primary key is not provided in the data, for performance reason.
tableFields, err := d.GetCore().GetDB().TableFields(ctx, table)
if err == nil && len(list) > 0 {
for _, field := range tableFields {
if strings.EqualFold(field.Key, "pri") {
// Check if primary key is provided in the data.
pkProvided := false
for key := range list[0] {
if strings.EqualFold(key, field.Name) {
pkProvided = true
break
}
}
// Only use RETURNING when primary key is not provided, for performance reason.
if !pkProvided {
pkField := *field
ctx = context.WithValue(ctx, internalPrimaryKeyInCtx, pkField)
}
break
}
}
}
default:
}
var (
@ -57,8 +90,8 @@ func (d *Driver) DoInsert(
valueHolderStr = strings.Join(valueHolder, ",")
)
// Format "INSERT...INTO..." statement.
intoStrArray := make([]string, 0)
for i := 0; i < len(list); i++ {
// Note: Use standard INSERT INTO syntax instead of INSERT ALL to ensure triggers fire
for i := 0; i < listLength; i++ {
for _, k := range keys {
if s, ok := list[i][k].(gdb.Raw); ok {
params = append(params, gconv.String(s))
@ -67,49 +100,87 @@ func (d *Driver) DoInsert(
}
}
values = append(values, valueHolderStr)
intoStrArray = append(
intoStrArray,
fmt.Sprintf(
"INTO %s(%s) VALUES(%s)",
table, keyStr, valueHolderStr,
),
)
if len(intoStrArray) == option.BatchCount || (i == listLength-1 && len(valueHolder) > 0) {
r, err := d.DoExec(ctx, link, fmt.Sprintf(
"INSERT ALL %s SELECT * FROM DUAL",
strings.Join(intoStrArray, " "),
), params...)
if err != nil {
return r, err
}
if n, err := r.RowsAffected(); err != nil {
return r, err
} else {
batchResult.Result = r
batchResult.Affected += n
}
params = params[:0]
intoStrArray = intoStrArray[:0]
// Execute individual INSERT for each record to trigger row-level triggers
r, err := d.DoExec(ctx, link, fmt.Sprintf(
"INSERT INTO %s(%s) VALUES(%s)",
table, keyStr, valueHolderStr,
), params...)
if err != nil {
return r, err
}
if n, err := r.RowsAffected(); err != nil {
return r, err
} else {
batchResult.Result = r
batchResult.Affected += n
}
params = params[:0]
}
return batchResult, nil
}
// doSave support upsert for Oracle.
// doSave support upsert for Oracle
func (d *Driver) doSave(ctx context.Context,
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
) (result sql.Result, err error) {
if len(option.OnConflict) == 0 {
return nil, gerror.NewCode(
gcode.CodeMissingParameter, `Please specify conflict columns`,
)
return d.doMergeInsert(ctx, link, table, list, option, true)
}
// doInsertIgnore implements INSERT IGNORE operation using MERGE statement for Oracle database.
// It only inserts records when there's no conflict on primary/unique keys.
func (d *Driver) doInsertIgnore(ctx context.Context,
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption,
) (result sql.Result, err error) {
return d.doMergeInsert(ctx, link, table, list, option, false)
}
// doMergeInsert implements MERGE-based insert operations for Oracle database.
// When withUpdate is true, it performs upsert (insert or update).
// When withUpdate is false, it performs insert ignore (insert only when no conflict).
func (d *Driver) doMergeInsert(
ctx context.Context,
link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption, withUpdate bool,
) (result sql.Result, err error) {
// If OnConflict is not specified, automatically get the primary key of the table
conflictKeys := option.OnConflict
if len(conflictKeys) == 0 {
primaryKeys, err := d.Core.GetPrimaryKeys(ctx, table)
if err != nil {
return nil, gerror.WrapCode(
gcode.CodeInternalError,
err,
`failed to get primary keys for table`,
)
}
foundPrimaryKey := false
for _, primaryKey := range primaryKeys {
for dataKey := range list[0] {
if strings.EqualFold(dataKey, primaryKey) {
foundPrimaryKey = true
break
}
}
if foundPrimaryKey {
break
}
}
if !foundPrimaryKey {
return nil, gerror.NewCodef(
gcode.CodeMissingParameter,
`Replace/Save/InsertIgnore operation requires conflict detection: `+
`either specify OnConflict() columns or ensure table '%s' has a primary key in the data`,
table,
)
}
// TODO consider composite primary keys.
conflictKeys = primaryKeys
}
var (
one = list[0]
oneLen = len(one)
charL, charR = d.GetChars()
conflictKeys = option.OnConflict
conflictKeySet = gset.New(false)
// queryHolders: Handle data with Holder that need to be upsert
@ -137,9 +208,9 @@ func (d *Driver) doSave(ctx context.Context,
insertKeys[index] = keyWithChar
insertValues[index] = fmt.Sprintf("T2.%s", keyWithChar)
// filter conflict keys in updateValues.
// And the key is not a soft created field.
if !(conflictKeySet.Contains(key) || d.Core.IsSoftCreatedFieldName(key)) {
// Build updateValues only when withUpdate is true
// Filter conflict keys and soft created fields from updateValues
if withUpdate && !(conflictKeySet.Contains(key) || d.Core.IsSoftCreatedFieldName(key)) {
updateValues = append(
updateValues,
fmt.Sprintf(`T1.%s = T2.%s`, keyWithChar, keyWithChar),
@ -148,8 +219,10 @@ func (d *Driver) doSave(ctx context.Context,
index++
}
batchResult := new(gdb.SqlResult)
sqlStr := parseSqlForUpsert(table, queryHolders, insertKeys, insertValues, updateValues, conflictKeys)
var (
batchResult = new(gdb.SqlResult)
sqlStr = parseSqlForMerge(table, queryHolders, insertKeys, insertValues, updateValues, conflictKeys)
)
r, err := d.DoExec(ctx, link, sqlStr, queryValues...)
if err != nil {
return r, err
@ -163,40 +236,43 @@ func (d *Driver) doSave(ctx context.Context,
return batchResult, nil
}
// parseSqlForUpsert
// MERGE INTO {{table}} T1
// USING ( SELECT {{queryHolders}} FROM DUAL T2
// ON (T1.{{duplicateKey}} = T2.{{duplicateKey}} AND ...)
// WHEN NOT MATCHED THEN
// INSERT {{insertKeys}} VALUES {{insertValues}}
// WHEN MATCHED THEN
// UPDATE SET {{updateValues}}
func parseSqlForUpsert(table string,
// parseSqlForMerge generates MERGE statement for Oracle database.
// When updateValues is empty, it only inserts (INSERT IGNORE behavior).
// When updateValues is provided, it performs upsert (INSERT or UPDATE).
// Examples:
// - INSERT IGNORE: MERGE INTO table T1 USING (...) T2 ON (...) WHEN NOT MATCHED THEN INSERT(...) VALUES (...)
// - UPSERT: MERGE INTO table T1 USING (...) T2 ON (...) WHEN NOT MATCHED THEN INSERT(...) VALUES (...) WHEN MATCHED THEN UPDATE SET ...
func parseSqlForMerge(table string,
queryHolders, insertKeys, insertValues, updateValues, duplicateKey []string,
) (sqlStr string) {
var (
queryHolderStr = strings.Join(queryHolders, ",")
insertKeyStr = strings.Join(insertKeys, ",")
insertValueStr = strings.Join(insertValues, ",")
updateValueStr = strings.Join(updateValues, ",")
duplicateKeyStr string
pattern = gstr.Trim(`MERGE INTO %s T1 USING (SELECT %s FROM DUAL) T2 ON (%s) WHEN NOT MATCHED THEN INSERT(%s) VALUES (%s) WHEN MATCHED THEN UPDATE SET %s`)
)
// Build ON condition
for index, keys := range duplicateKey {
if index != 0 {
duplicateKeyStr += " AND "
}
duplicateTmp := fmt.Sprintf("T1.%s = T2.%s", keys, keys)
duplicateKeyStr += duplicateTmp
duplicateKeyStr += fmt.Sprintf("T1.%s = T2.%s", keys, keys)
}
return fmt.Sprintf(pattern,
table,
queryHolderStr,
duplicateKeyStr,
insertKeyStr,
insertValueStr,
updateValueStr,
// Build SQL based on whether UPDATE is needed
pattern := gstr.Trim(
`MERGE INTO %s T1 USING (SELECT %s FROM DUAL) T2 ON (%s) WHEN ` +
`NOT MATCHED THEN INSERT(%s) VALUES (%s)`,
)
if len(updateValues) > 0 {
// Upsert: INSERT or UPDATE
pattern += gstr.Trim(` WHEN MATCHED THEN UPDATE SET %s`)
return fmt.Sprintf(
pattern, table, queryHolderStr, duplicateKeyStr, insertKeyStr, insertValueStr,
strings.Join(updateValues, ","),
)
}
// Insert Ignore: INSERT only
return fmt.Sprintf(pattern, table, queryHolderStr, duplicateKeyStr, insertKeyStr, insertValueStr)
}

View File

@ -0,0 +1,24 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package oracle
// Result implements sql.Result interface for Oracle database.
type Result struct {
lastInsertId int64
rowsAffected int64
lastInsertIdError error
}
// LastInsertId returns the last insert id.
func (r *Result) LastInsertId() (int64, error) {
return r.lastInsertId, r.lastInsertIdError
}
// RowsAffected returns the rows affected.
func (r *Result) RowsAffected() (int64, error) {
return r.rowsAffected, nil
}

View File

@ -18,13 +18,23 @@ import (
var (
tableFieldsSqlTmp = `
SELECT
COLUMN_NAME AS FIELD,
c.COLUMN_NAME AS FIELD,
CASE
WHEN (DATA_TYPE='NUMBER' AND NVL(DATA_SCALE,0)=0) THEN 'INT'||'('||DATA_PRECISION||','||DATA_SCALE||')'
WHEN (DATA_TYPE='NUMBER' AND NVL(DATA_SCALE,0)>0) THEN 'FLOAT'||'('||DATA_PRECISION||','||DATA_SCALE||')'
WHEN DATA_TYPE='FLOAT' THEN DATA_TYPE||'('||DATA_PRECISION||','||DATA_SCALE||')'
ELSE DATA_TYPE||'('||DATA_LENGTH||')' END AS TYPE,NULLABLE
FROM USER_TAB_COLUMNS WHERE TABLE_NAME = '%s' ORDER BY COLUMN_ID
WHEN (c.DATA_TYPE='NUMBER' AND NVL(c.DATA_SCALE,0)=0) THEN 'INT'||'('||c.DATA_PRECISION||','||c.DATA_SCALE||')'
WHEN (c.DATA_TYPE='NUMBER' AND NVL(c.DATA_SCALE,0)>0) THEN 'FLOAT'||'('||c.DATA_PRECISION||','||c.DATA_SCALE||')'
WHEN c.DATA_TYPE='FLOAT' THEN c.DATA_TYPE||'('||c.DATA_PRECISION||','||c.DATA_SCALE||')'
ELSE c.DATA_TYPE||'('||c.DATA_LENGTH||')' END AS TYPE,
c.NULLABLE,
CASE WHEN pk.COLUMN_NAME IS NOT NULL THEN 'PRI' ELSE '' END AS KEY
FROM USER_TAB_COLUMNS c
LEFT JOIN (
SELECT cols.COLUMN_NAME
FROM USER_CONSTRAINTS cons
JOIN USER_CONS_COLUMNS cols ON cons.CONSTRAINT_NAME = cols.CONSTRAINT_NAME
WHERE cons.TABLE_NAME = '%s' AND cons.CONSTRAINT_TYPE = 'P'
) pk ON c.COLUMN_NAME = pk.COLUMN_NAME
WHERE c.TABLE_NAME = '%s'
ORDER BY c.COLUMN_ID
`
)
@ -44,7 +54,8 @@ func (d *Driver) TableFields(ctx context.Context, table string, schema ...string
result gdb.Result
link gdb.Link
usedSchema = gutil.GetOrDefaultStr(d.GetSchema(), schema...)
structureSql = fmt.Sprintf(tableFieldsSqlTmp, strings.ToUpper(table))
upperTable = strings.ToUpper(table)
structureSql = fmt.Sprintf(tableFieldsSqlTmp, upperTable, upperTable)
)
if link, err = d.SlaveLink(usedSchema); err != nil {
return nil, err
@ -53,6 +64,7 @@ func (d *Driver) TableFields(ctx context.Context, table string, schema ...string
if err != nil {
return nil, err
}
fields = make(map[string]*gdb.TableField)
for i, m := range result {
isNull := false
@ -65,6 +77,7 @@ func (d *Driver) TableFields(ctx context.Context, table string, schema ...string
Name: m["FIELD"].String(),
Type: m["TYPE"].String(),
Null: isNull,
Key: m["KEY"].String(),
}
}
return fields, nil

View File

@ -139,10 +139,10 @@ func Test_Do_Insert(t *testing.T) {
"CREATE_TIME": gtime.Now().String(),
}
_, err := db.Save(ctx, "t_user", data, 10)
gtest.AssertNE(err, nil)
gtest.AssertNil(err)
_, err = db.Replace(ctx, "t_user", data, 10)
gtest.AssertNE(err, nil)
gtest.AssertNil(err)
})
}
@ -185,6 +185,7 @@ func Test_DB_Insert(t *testing.T) {
table := createTable()
defer dropTable(table)
// db.SetDebug(true)
gtest.C(t, func(t *gtest.T) {
_, err := db.Insert(ctx, table, g.Map{
"ID": 1,
@ -233,7 +234,7 @@ func Test_DB_Insert(t *testing.T) {
one, err := db.Model(table).Where("ID", 3).One()
t.AssertNil(err)
fmt.Println(one)
// fmt.Println(one)
t.Assert(one["ID"].Int(), 3)
t.Assert(one["PASSPORT"].String(), "user_3")
t.Assert(one["PASSWORD"].String(), "25d55ad283aa400af464c76d713c07ad")

View File

@ -113,16 +113,48 @@ func createTable(table ...string) (name string) {
dropTable(name)
if _, err := db.Exec(ctx, fmt.Sprintf(`
CREATE TABLE %s (
ID NUMBER(10) NOT NULL,
PASSPORT VARCHAR(45) NOT NULL,
PASSWORD CHAR(32) NOT NULL,
NICKNAME VARCHAR(45) NOT NULL,
CREATE_TIME varchar(45),
SALARY NUMBER(18,2),
PRIMARY KEY (ID))
`, name)); err != nil {
// Step 1: Create table
createTableSQL := fmt.Sprintf(`
CREATE TABLE %s (
ID NUMBER(10) NOT NULL,
PASSPORT VARCHAR(45) NOT NULL,
PASSWORD CHAR(32) NOT NULL,
NICKNAME VARCHAR(45) NOT NULL,
CREATE_TIME VARCHAR(45),
SALARY NUMBER(18,2),
PRIMARY KEY (ID)
)`, name)
if _, err := db.Exec(ctx, createTableSQL); err != nil {
gtest.Fatal(err)
}
// Step 2: Create sequence
createSeqSQL := fmt.Sprintf(`
CREATE SEQUENCE %s_ID_SEQ
START WITH 1
INCREMENT BY 1
MINVALUE 1
MAXVALUE 9999999999
NOCYCLE
NOCACHE`, name)
if _, err := db.Exec(ctx, createSeqSQL); err != nil {
gtest.Fatal(err)
}
// Step 3: Create trigger - only set ID from sequence when it's NULL
createTriggerSQL := fmt.Sprintf(`
CREATE OR REPLACE TRIGGER %s_ID_TRG
BEFORE INSERT ON %s
FOR EACH ROW
BEGIN
IF :NEW.ID IS NULL THEN
:NEW.ID := %s_ID_SEQ.NEXTVAL;
END IF;
END;`, name, name, name)
if _, err := db.Exec(ctx, createTriggerSQL); err != nil {
gtest.Fatal(err)
}
@ -160,7 +192,15 @@ func dropTable(table string) {
if count == 0 {
return
}
// Drop table
if _, err = db.Exec(ctx, fmt.Sprintf("DROP TABLE %s", table)); err != nil {
gtest.Fatal(err)
}
// Drop sequence if exists
seqCount, err := db.GetCount(ctx, "SELECT COUNT(*) FROM USER_SEQUENCES WHERE SEQUENCE_NAME = ?", strings.ToUpper(table+"_ID_SEQ"))
if err == nil && seqCount > 0 {
db.Exec(ctx, fmt.Sprintf("DROP SEQUENCE %s_ID_SEQ", table))
}
}

View File

@ -233,6 +233,67 @@ func Test_Model_Insert(t *testing.T) {
})
}
func Test_Model_InsertIgnore(t *testing.T) {
table := createInitTable()
defer dropTable(table)
// db.SetDebug(true)
gtest.C(t, func(t *gtest.T) {
data := g.Map{
"id": 1,
"passport": fmt.Sprintf(`t%d`, 777),
"password": fmt.Sprintf(`p%d`, 777),
"nickname": fmt.Sprintf(`T%d`, 777),
"create_time": gtime.Now(),
}
_, err := db.Model(table).Data(data).InsertIgnore()
t.AssertNil(err)
one, err := db.Model(table).WherePri(1).One()
t.AssertNil(err)
t.Assert(one["PASSPORT"].String(), "user_1")
count, err := db.Model(table).Count()
t.AssertNil(err)
t.Assert(count, TableSize)
})
gtest.C(t, func(t *gtest.T) {
data := g.Map{
"passport": fmt.Sprintf(`t%d`, 777),
"password": fmt.Sprintf(`p%d`, 777),
"nickname": fmt.Sprintf(`T%d`, 777),
"create_time": gtime.Now(),
}
_, err := db.Model(table).Data(data).InsertIgnore()
t.AssertNE(err, nil)
count, err := db.Model(table).Count()
t.AssertNil(err)
t.Assert(count, TableSize)
})
}
func Test_Model_InsertAndGetId(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
data := g.Map{
// "id": 1,
"passport": fmt.Sprintf(`t%d`, 1),
"password": fmt.Sprintf(`p%d`, 1),
"nickname": fmt.Sprintf(`T%d`, 1),
"create_time": gtime.Now(),
}
lastId, err := db.Model(table).Data(data).InsertAndGetId()
t.AssertNil(err)
t.AssertGT(lastId, 0)
})
}
// https://github.com/gogf/gf/issues/3286
func Test_Model_Insert_Raw(t *testing.T) {
table := createTable()
@ -1179,14 +1240,73 @@ func Test_Model_Replace(t *testing.T) {
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
_, err := db.Model(table).Data(g.Map{
// Insert initial record
result, err := db.Model(table).Data(g.Map{
"id": 1,
"passport": "t1",
"password": "pass1",
"nickname": "T1",
"create_time": "2018-10-24 10:00:00",
}).Insert()
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 1)
// Replace with new data (should update existing record using MERGE)
result, err = db.Model(table).Data(g.Map{
"id": 1,
"passport": "t11",
"password": "25d55ad283aa400af464c76d713c07ad",
"nickname": "T11",
"create_time": "2018-10-24 10:00:00",
}).OnConflict("id").Replace()
t.AssertNil(err)
n, _ = result.RowsAffected()
t.Assert(n, 1)
// Verify the data was replaced
one, err := db.Model(table).WherePri(1).One()
t.AssertNil(err)
t.Assert(one["PASSPORT"].String(), "t11")
t.Assert(one["PASSWORD"].String(), "25d55ad283aa400af464c76d713c07ad")
t.Assert(one["NICKNAME"].String(), "T11")
// Replace with new ID (insert new record)
result, err = db.Model(table).Data(g.Map{
"id": 2,
"passport": "t222",
"password": "pass2",
"nickname": "T222",
"create_time": "2018-10-24 11:00:00",
}).OnConflict("id").Replace()
t.AssertNil(err)
n, _ = result.RowsAffected()
t.Assert(n, 1)
// Verify new record was inserted
one, err = db.Model(table).Where("id", 2).One()
t.AssertNil(err)
t.Assert(one["PASSPORT"].String(), "t222")
t.Assert(one["NICKNAME"].String(), "T222")
// Replace without OnConflict (primary key auto-detection is implemented)
_, err = db.Model(table).Data(g.Map{
"id": 3,
"passport": "t3",
"password": "pass3",
"nickname": "T3",
"create_time": "2018-10-24 12:00:00",
}).Replace()
t.Assert(err, "Replace operation is not supported by oracle driver")
t.AssertNil(err)
_, err = db.Model(table).Data(g.Map{
// "id": 3,
"passport": "t3",
"password": "pass3",
"nickname": "T3",
"create_time": "2018-10-24 12:00:00",
}).Replace()
t.AssertNE(err, nil)
})
}

View File

@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/drivers/pgsql/v2
go 1.23.0
require (
github.com/gogf/gf/v2 v2.9.6
github.com/gogf/gf/v2 v2.9.7
github.com/google/uuid v1.6.0
github.com/lib/pq v1.10.9
)

View File

@ -5,10 +5,6 @@
// You can obtain one at https://github.com/gogf/gf.
// Package pgsql implements gdb.Driver, which supports operations for database PostgreSQL.
//
// Note:
// 1. It does not support Replace features.
// 2. It does not support Insert Ignore features.
package pgsql
import (

View File

@ -14,7 +14,6 @@ import (
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/text/gstr"
)
// DoInsert inserts or updates data for given table.
@ -39,9 +38,9 @@ func (d *Driver) DoInsert(
)
}
foundPrimaryKey := false
for _, conflictKey := range primaryKeys {
for _, primaryKey := range primaryKeys {
for dataKey := range list[0] {
if strings.EqualFold(dataKey, conflictKey) {
if strings.EqualFold(dataKey, primaryKey) {
foundPrimaryKey = true
break
}
@ -58,16 +57,20 @@ func (d *Driver) DoInsert(
table,
)
}
// TODO consider composite primary keys.
option.OnConflict = primaryKeys
}
// Treat Replace as Save operation
option.InsertOption = gdb.InsertOptionSave
case gdb.InsertOptionDefault:
// pgsql support InsertIgnore natively, so no need to set primary key in context.
case gdb.InsertOptionIgnore, gdb.InsertOptionDefault:
// Get table fields to retrieve the primary key TableField object (not just the name)
// because DoExec needs the `TableField.Type` to determine if LastInsertId is supported.
tableFields, err := d.GetCore().GetDB().TableFields(ctx, table)
if err == nil {
for _, field := range tableFields {
if gstr.Equal(field.Key, "pri") {
if strings.EqualFold(field.Key, "pri") {
pkField := *field
ctx = context.WithValue(ctx, internalPrimaryKeyInCtx, pkField)
break

View File

@ -841,5 +841,24 @@ func Test_Model_InsertIgnore(t *testing.T) {
value, err := db.Model(table).Fields("passport").WherePri(1).Value()
t.AssertNil(err)
t.Assert(value.String(), "t1")
count, err := db.Model(table).Count()
t.AssertNil(err)
t.Assert(count, 1)
// pgsql support ignore without primary key
result, err = db.Model(table).Data(g.Map{
// "id": 1,
"uid": 1,
"passport": "t2",
"password": "25d55ad283aa400af464c76d713c07ad",
"nickname": "name_2",
"create_time": gtime.Now().String(),
}).InsertIgnore()
t.AssertNil(err)
count, err = db.Model(table).Count()
t.AssertNil(err)
t.Assert(count, 1)
})
}

View File

@ -4,7 +4,7 @@ go 1.23.0
require (
github.com/glebarez/go-sqlite v1.21.2
github.com/gogf/gf/v2 v2.9.6
github.com/gogf/gf/v2 v2.9.7
)
require (

View File

@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/drivers/sqlitecgo/v2
go 1.23.0
require (
github.com/gogf/gf/v2 v2.9.6
github.com/gogf/gf/v2 v2.9.7
github.com/mattn/go-sqlite3 v1.14.17
)

View File

@ -0,0 +1,44 @@
module github.com/gogf/gf/contrib/drivers/tidb/v2
go 1.23.0
require (
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.7
github.com/gogf/gf/v2 v2.9.7
)
require (
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/grokify/html-strip-tags-go v0.1.0 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.0.9 // indirect
github.com/olekukonko/tablewriter v1.1.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.25.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace (
github.com/gogf/gf/contrib/drivers/mysql/v2 => ../mysql
github.com/gogf/gf/v2 => ../../../
)

View File

@ -0,0 +1,81 @@
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU=
github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4=
github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY=
github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -0,0 +1,49 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
// Package tidb implements gdb.Driver, which supports operations for database TiDB.
package tidb
import (
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/contrib/drivers/mysql/v2"
)
// Driver is the driver for TiDB database.
//
// TiDB is an open-source NewSQL database that supports Hybrid Transactional and Analytical Processing (HTAP).
// This driver uses the MySQL protocol to communicate with TiDB database, as TiDB is designed to be highly
// compatible with the MySQL protocol.
//
// Although TiDB is compatible with MySQL protocol, it is packaged as a separate driver component
// rather than reusing the mysql adapter directly. This design allows for future extensibility,
// such as implementing TiDB-specific features like distributed transactions or optimizations.
type Driver struct {
*mysql.Driver
}
func init() {
var (
err error
driverObj = New()
driverNames = g.SliceStr{"tidb"}
)
for _, driverName := range driverNames {
if err = gdb.Register(driverName, driverObj); err != nil {
panic(err)
}
}
}
// New creates and returns a driver that implements gdb.Driver, which supports operations for TiDB.
func New() gdb.Driver {
mysqlDriver := mysql.New().(*mysql.Driver)
return &Driver{
Driver: mysqlDriver,
}
}

View File

@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/metric/otelmetric/v2
go 1.23.0
require (
github.com/gogf/gf/v2 v2.9.6
github.com/gogf/gf/v2 v2.9.7
github.com/prometheus/client_golang v1.23.2
go.opentelemetry.io/contrib/instrumentation/runtime v0.63.0
go.opentelemetry.io/otel v1.38.0

View File

@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/nosql/redis/v2
go 1.23.0
require (
github.com/gogf/gf/v2 v2.9.6
github.com/gogf/gf/v2 v2.9.7
github.com/redis/go-redis/v9 v9.12.1
go.opentelemetry.io/otel v1.38.0
go.opentelemetry.io/otel/trace v1.38.0

View File

@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/registry/consul/v2
go 1.23.0
require (
github.com/gogf/gf/v2 v2.9.6
github.com/gogf/gf/v2 v2.9.7
github.com/hashicorp/consul/api v1.26.1
)

View File

@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/registry/etcd/v2
go 1.23.0
require (
github.com/gogf/gf/v2 v2.9.6
github.com/gogf/gf/v2 v2.9.7
go.etcd.io/etcd/client/v3 v3.5.17
google.golang.org/grpc v1.59.0
)

View File

@ -2,7 +2,7 @@ module github.com/gogf/gf/contrib/registry/file/v2
go 1.23.0
require github.com/gogf/gf/v2 v2.9.6
require github.com/gogf/gf/v2 v2.9.7
require (
github.com/BurntSushi/toml v1.5.0 // indirect

View File

@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/registry/nacos/v2
go 1.23.0
require (
github.com/gogf/gf/v2 v2.9.6
github.com/gogf/gf/v2 v2.9.7
github.com/nacos-group/nacos-sdk-go/v2 v2.3.3
)

View File

@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/registry/polaris/v2
go 1.23.0
require (
github.com/gogf/gf/v2 v2.9.6
github.com/gogf/gf/v2 v2.9.7
github.com/polarismesh/polaris-go v1.6.1
)

Some files were not shown because too many files have changed in this diff Show More