Compare commits

..

42 Commits

Author SHA1 Message Date
d7de5c8b83 Merge branch 'feat/cli-gen-dao' of github.com:gogf/gf into feat/cli-gen-dao 2026-05-18 20:36:40 +00:00
884ba53def Merge branch 'feat/cli-gen-dao' of github.com:gogf/gf into feat/cli-gen-dao 2026-03-14 20:21:34 +08:00
dd5a4c65a3 makefile: push to current branch 2026-03-14 20:20:36 +08:00
e49da96c3f Apply gci import order changes 2026-03-14 07:10:12 +00:00
33ccec6d68 add sql file mode support to gendao command 2026-03-14 15:09:16 +08:00
766579d868 test(contrib/drivers/mariadb): add transaction, where, hook and ctx tests (#4720)
## Summary

- Port 28 transaction tests: Begin/Commit/Rollback, nested SavePoint,
transaction propagation (Required/Nested/NotSupported), timeout, panic
recovery, concurrent transactions
- Port 41 where-condition tests:
Where/WhereOr/WhereNot/WhereIn/WhereBetween, prefix handling, complex
AND/OR combinations, NULL checks, struct/map/slice parameter types
- Port 11 hook tests: HookSelect/HookInsert/HookUpdate/HookDelete for
both Model and raw SQL paths, hook chaining and context propagation
- Port 8 ctx tests: context propagation through Model/TX operations,
context-based logging with traceId verification

All tests are structurally identical to the MySQL driver baseline. SQL
syntax is standard and shared. Package and import references are adapted
for MariaDB.

ref #4689
2026-03-12 11:16:14 +08:00
030cd84836 test(contrib/drivers/mariadb): add softtime, with, scanlist, union and do tests (#4721)
## Summary

- Port 48 soft-time tests: soft create/update/delete with
timestamp/datetime/date types, SoftTime switches
(SoftTimeTypeOff/Delete/Timestamp), Unscoped, ForceDelete, joined
queries with soft-delete
- Port 19 With/ScanList tests: With/WithAll for eager loading of
hasOne/hasMany/belongsTo relations, ScanList for manual relation
mapping, nested With
- Port 12 union tests: Union/UnionAll with various parameter forms
(string/Model/subquery), combined with OrderBy, Limit, Where conditions
- Port 12 gdb.Do tests: DoSelect/DoInsert/DoUpdate/DoDelete raw
operation hooks, batch insert, InsertIgnore/InsertGetId/Replace via
DoInsert option

Includes testdata SQL files for With relation table schemas (with_tpl).
Soft-time tests create tables inline via SQL, no separate testdata files
needed.

All tests are structurally identical to the MySQL driver baseline. SQL
syntax is standard and shared. Package and import references are adapted
for MariaDB.

ref #4689
2026-03-12 11:15:42 +08:00
6314cd4c89 test(contrib/drivers/mariadb): add builder, struct, join, batch, cache and omit tests (#4722)
## Summary

- Port 25 SQL builder tests: WhereBuilder chaining, complex
Where/WhereOr/WhereNot combinations, nested builders, Build() output
verification
- Port 9 subquery tests: subquery in Where/Having/From, correlated
subqueries, subquery with Model builder
- Port 11 struct-mapping tests: Scan to struct/slice, embedded structs,
tag-based field mapping, pointer fields, OmitEmpty with struct input
- Port 12 join tests: LeftJoin/RightJoin/InnerJoin, multi-table joins,
join with Where/Order/Fields, subquery joins, testdata SQL-based join
scenarios
- Port 8 batch operation tests: Batch insert/update/replace/save with
configurable batch size, conflict handling
- Port 4 cache tests: query result caching, cache invalidation on
update/delete, cache duration, ClearCache
- Port 4 OmitNil/OmitEmpty tests: nil field omission in insert/update,
zero-value vs nil distinction

All tests are structurally identical to the MySQL driver baseline. SQL
syntax is standard and shared. Package and import references are adapted
for MariaDB.

ref #4689
2026-03-12 11:15:17 +08:00
0588009c40 test(contrib/drivers/mariadb): add pagination, error, concurrent, rawtype and sharding tests (#4723)
## Summary

- Port 11 pagination tests: Page/Limit/Offset, combined with
Where/Order, boundary conditions (page 0, large offset), Count with
pagination
- Port 8 error-handling tests: invalid table/field names, syntax errors,
duplicate key, connection errors, error wrapping and message
verification
- Port 5 concurrency tests: parallel read/write with goroutines and
WaitGroup, concurrent transactions, race condition verification
- Port 6 raw-type tests: custom type scanning, time.Time handling,
json.RawMessage, sql.NullString/NullInt64, []byte fields
- Port 6 sharding/table-name tests: dynamic table name via Sharding
callback, table name with prefix, schema.table format

All tests are structurally identical to the MySQL driver baseline. SQL
syntax is standard and shared. Package and import references are adapted
for MariaDB.

ref #4689
2026-03-12 11:14:48 +08:00
6204c132c7 test(contrib/drivers/mysql): add pagination and error handling tests (#4703)
## Summary
- Add comprehensive pagination tests (Limit, Offset, Page, ForPage)
- Add error handling tests for invalid operations
- Add tests for edge cases and boundary conditions

**Test coverage added:**
- Pagination: ~28 test functions
- Error handling: ~20 test functions

Ref #4689

## Test plan
```bash
cd contrib/drivers/mysql
go test -v -run "TestModel_Pagination|TestModel_Error|TestModel_InvalidOperation"
```
2026-02-27 20:00:25 +08:00
a4b80e8680 fix(contrib/drivers/pgsql): preserve bytea data integrity on read and write (#4678)
## Summary

Fix two bytea data corruption issues in the PostgreSQL driver:

1. **READ path** (fixes #4677): `CheckLocalTypeForField` and
`ConvertValueForLocal` had no case for plain `bytea` type, causing it to
fall through to the Core layer which incorrectly mapped it to
`LocalTypeString`. Binary data was then converted to string via
`gconv.String()`, corrupting the bytes on retrieval.

2. **WRITE path** (fixes #4231): `ConvertValueForField` applied
PostgreSQL array syntax conversion (`[` → `{`, `]` → `}`) to all slice
types including `[]byte` for bytea columns, corrupting bytes `0x5B`
(`[`) and `0x5D` (`]`) on insertion.

## Changes

- **`contrib/drivers/pgsql/pgsql_convert.go`**:
  - `CheckLocalTypeForField`: Add `case "bytea"` → `LocalTypeBytes`
- `ConvertValueForLocal`: Add `case "bytea"` to preserve `[]byte` as-is
- `ConvertValueForField`: Skip `[]`→`{}` replacement for `[]byte` with
`bytea` field type

- **`contrib/drivers/pgsql/pgsql_z_unit_convert_test.go`**:
- Add unit tests for `bytea` type in `CheckLocalTypeForField`,
`ConvertValueForLocal`, and `ConvertValueForField`

- **`contrib/drivers/pgsql/pgsql_z_unit_issue_test.go`**:
- Add `Test_Issue4677`: End-to-end round-trip test with various binary
data (including 0x00, 0x5B, 0x5D, 0xFF)
- Add `Test_Issue4231`: Targeted test for 0x5D byte corruption on write

## Test plan

- [x] `Test_CheckLocalTypeForField` - bytea returns `LocalTypeBytes`
- [x] `Test_ConvertValueForLocal` - bytea preserves `[]byte` as-is
- [x] `Test_ConvertValueForField` - bytea skips array syntax replacement
- [x] `Test_Issue4677` - full DB round-trip with binary data
- [x] `Test_Issue4231` - write path preserves 0x5B/0x5D bytes
- [x] Full pgsql test suite passes with no regressions

closes #4677
closes #4231

ref #4689
2026-02-27 16:22:43 +08:00
0e1cb15dc0 fix(os/gstructs): strip tag options in TagPriorityName to avoid field name pollution (#4681)
## Summary
- Fix `TagPriorityName()` to strip comma-separated tag options (e.g.,
`omitempty`) from tag values
- Before: `json:"user_name,omitempty"` → field name =
`user_name,omitempty`
- After: `json:"user_name,omitempty"` → field name = `user_name`
- Aligns with `structcache.genPriorityTagAndFieldName()` which already
handles this correctly
- When tag name is empty (e.g., `gconv:",omitempty"`), continues to next
priority tag instead of breaking

## Test plan
- [x] Reproduced bug: `RuleFuncInput.Field` was `user_name,omitempty`
instead of `user_name`
- [x] Verified fix: field name correctly extracted as `user_name`
- [x] Verified fallthrough: `gconv:",omitempty"` + `json:"name"` → uses
`name`
- [x] Existing `Test_Fields_TagPriorityName` passes
- [x] Full `os/gstructs` test suite passes
- [x] Full `util/gvalid` test suite passes
- [x] Full `util/gconv` test suite passes

closes #4665
2026-02-27 16:14:26 +08:00
612e545ae2 fix(databse/gdb): use COUNT(1) if fields number is greater than 1 even when parameter useFieldForCount is true in AllAndCount/ScanAndCount (#4701)
## Summary
Fix bug where `AllAndCount(true)` with multiple fields generates invalid
SQL `COUNT(field1, field2, ...)` causing syntax error.

## Root Cause
When `useFieldForCount=true`, the COUNT query inherits the fields
configuration from the model:
```go
// Before (buggy code)
if !useFieldForCount {
    countModel.fields = []any{Raw("1")}
}
// When useFieldForCount=true, fields remain as ["id", "nickname"]
// Generates: SELECT COUNT(id, nickname) FROM table 
```

## Fix
Always use `COUNT(1)` regardless of `useFieldForCount` parameter since
COUNT() accepts only one argument:
```go
// After (fixed code)
// Always use COUNT(1) for counting, regardless of useFieldForCount.
// COUNT() accepts only one argument, so we can't use multiple fields.
countModel.fields = []any{Raw("1")}
```

Applied to both `AllAndCount()` and `ScanAndCount()` methods.

## Tests
Added `Test_Issue4698` with 5 test cases:
1. AllAndCount(true) with multiple fields
2. AllAndCount(false) with multiple fields (baseline)
3. ScanAndCount with multiple fields
4. AllAndCount with single field
5. AllAndCount with WHERE condition

All tests verify that COUNT generates valid SQL and returns correct
count.

## Related
Fixes #4698
Ref #4703 (discovered during pagination test development)
2026-02-27 16:12:58 +08:00
bbdd442954 fix(database/gdb): treat negative Limit/Page/Offset values as zero (#4702)
## Summary
Fix bug where negative values in `Limit()`, `Page()`, and `Offset()`
methods generate invalid SQL causing database errors.

## Root Cause
The methods don't validate negative input:
- `Limit(-1)` generates `LIMIT -1` → SQL error
- `Page(1, -10)` generates `LIMIT -10` → SQL error  
- `Offset(-5)` generates `OFFSET -5` → SQL error

## Fix
Treat all negative values as zero (safe default):

**Limit() method**:
```go
case 1:
    if limit[0] < 0 { limit[0] = 0 }
case 2:
    if limit[0] < 0 { limit[0] = 0 }
    if limit[1] < 0 { limit[1] = 0 }
```

**Page() method**:
```go
if limit < 0 { limit = 0 }
```

**Offset() method**:
```go
if offset < 0 { offset = 0 }
```

## Behavior Changes
- `Limit(-1)` → `Limit(0)` (no limit)
- `Limit(-10, -5)` → `Limit(0, 0)` (no offset, no limit)
- `Page(1, -10)` → `Page(1, 0)` (no results)
- `Offset(-5)` → `Offset(0)` (no offset)

## Documentation
Added "Note: Negative values are treated as zero" to all three methods.

## Tests
Added `Test_Issue4699` in `database/gdb/gdb_z_unit_issue_test.go` with 7
test cases:
1. Limit with single negative parameter
2. Limit with two negative parameters
3. Limit with mixed parameters (negative start, positive limit)
4. Page with negative limit
5. Page with negative limit on page 2
6. Offset with negative value
7. Offset with positive value (sanity check)

## Related
Fixes #4699
Ref #4703 (discovered during pagination test development)
2026-02-27 16:00:53 +08:00
6686bd65a2 test(contrib/drivers/mysql): enhance transaction tests (#4704)
## Summary
- Add nested transaction tests
- Add transaction propagation tests
- Add transaction rollback/commit behavior tests
- Add transaction context handling tests

**Test coverage added:** ~25 test functions

Ref #4689

## Test plan
```bash
cd contrib/drivers/mysql
go test -v -run "TestTX_Nested|TestTX_Propagation|TestTX_Transaction"
```
2026-02-27 15:56:16 +08:00
319a812934 test(contrib/drivers/mysql): enhance data type tests (#4705)
## Summary
- Add comprehensive tests for various data types (int, float, string,
[]byte, time, etc.)
- Add struct field type conversion tests
- Add JSON/XML data type tests
- Add binary data handling tests

**Test coverage added:** ~28 test functions

Ref #4689

## Test plan
```bash
cd contrib/drivers/mysql
go test -v -run "TestModel_.*Type|TestModel_.*Convert"
```
2026-02-27 15:53:44 +08:00
307c6ec307 test(contrib/drivers/mysql): enhance complex query tests (#4707)
## Summary
- Add comprehensive JOIN tests (Inner/Left/Right Join)
- Add SubQuery tests
- Add complex WHERE condition tests (Or/Group/Having)
- Add advanced query builder tests

**Test coverage added:** ~26 test functions across 3 files

Ref #4689

## Test plan
```bash
cd contrib/drivers/mysql
go test -v -run "TestModel_Join|TestModel_SubQuery|TestModel_Where.*Complex"
```
2026-02-27 15:52:41 +08:00
bac637570d test(contrib/drivers/mysql): add MySQL-specific feature tests (#4709)
## Summary
- Add ON DUPLICATE KEY UPDATE tests (basic, increment, batch,
conditional, transaction)
- Add MySQL JSON data type tests (insert/update/query, JSON_EXTRACT,
JSON_CONTAINS, struct scanning)
- Add MySQL partition tests (RANGE, HASH, LIST partitioning with CRUD
and transactions)

**Test coverage added:** ~25 test functions across 3 files (Layer 3)

Ref #4689

## Test plan
```bash
cd contrib/drivers/mysql
go test -v -run "Test_OnDuplicateKeyUpdate|Test_DataType_Json|Test_Partition"
```
2026-02-27 15:52:06 +08:00
c8a11f7f6e test(contrib/drivers/mysql): add concurrent/Hook/Ctx tests (#4708)
## Summary
- Add concurrent operation tests
- Add Hook mechanism tests (Insert/Update/Delete/Select)
- Add Context propagation tests
- Add race condition tests

**Test coverage added:** ~28 test functions across 5 files

Ref #4689

## Test plan
```bash
cd contrib/drivers/mysql
go test -v -race -run "TestModel_Concurrent|TestModel_Hook|TestModel_Ctx"
```
2026-02-26 16:28:20 +08:00
e0c032d1b1 fix(database/gdb): handle empty string in Fields() gracefully (#4700)
## Summary
Fix bug where `Fields("")` with empty string generates invalid SQL
`SELECT FROM table`.

## Root Cause
`mappingAndFilterToTableFields` method doesn't skip empty strings when
processing fields:
- `gstr.SplitAndTrim("", ",")` returns empty array
- No fields added to query
- Results in invalid SQL: `SELECT FROM table`

## Fix
Skip empty string fields in `mappingAndFilterToTableFields` (line
97-100):
```go
// Skip empty string fields
if fieldStr == "" {
    continue
}
```

## Behavior Changes
- `Fields("")` → SELECT * FROM table (uses default)
- `Fields("", "id")` → SELECT id FROM table (ignores empty string)
- `Fields("id", "", "nickname")` → SELECT id, nickname FROM table

## Tests
Added `Test_Issue4697` with 3 scenarios covering all cases above.

## Related
Fixes #4697
Ref #4703 (discovered during pagination test development)
2026-02-26 16:27:00 +08:00
063264ebff test(contrib/drivers/mysql): add Lock/Omit/Cache/Batch tests (#4706)
## Summary
- Add Lock/LockUpdate/LockShared tests
- Add OmitNil/OmitEmpty/OmitNilData tests
- Add Cache mechanism tests
- Add Batch operation tests

**Test coverage added:** ~34 test functions across 4 files

Ref #4689

## Test plan
```bash
cd contrib/drivers/mysql
go test -v -run "TestModel_Lock|TestModel_Omit|TestModel_Cache|TestModel_Batch"
```
2026-02-26 09:53:35 +08:00
02abc515a3 test(contrib/drivers/gaussdb): add soft time, with, scanlist test coverage (#4686)
## Summary
- Port 3 test files and 4 testdata SQL files from PgSQL driver to
GaussDB driver
- Add `gaussdb_z_unit_feature_soft_time_test.go` (15 tests): soft time
create/update/delete, bool/int/datetime soft delete
- Add `gaussdb_z_unit_feature_with_test.go` (6 tests): With/WithAll ORM
relation queries, multiple dependency levels
- Add `gaussdb_z_unit_feature_scanlist_test.go` (9 tests): ScanList for
1:1, 1:N, N:N relation mapping
- Add 4 testdata SQL files for With relation tests
- **30 new test functions**, ~3,941 net new lines

## Test plan
- [x] `go build ./...` passes
- [x] `gofmt` and `gci` applied
- [x] No remaining `pgsql` references in new files
- [ ] Run full test suite against GaussDB instance

ref #4689

---------

Co-authored-by: John Guo <claymore1986@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-26 09:51:52 +08:00
be7851c664 test(contrib/drivers/pgsql): add SoftTime, With, ScanList test coverage (#4676)
## Summary
- Add 3 new test files for pgsql driver (39 test functions, ~3800 lines)
- `pgsql_z_unit_feature_soft_time_test.go`: 15 tests — soft delete
(SoftDeleted/Unscoped), auto time fields
(CreatedAt/UpdatedAt/DeletedAt), time format options
- `pgsql_z_unit_feature_with_test.go`: 17 tests — With relation queries
(one-to-one, one-to-many, many-to-many), nested With, WithAll,
conditional With
- `pgsql_z_unit_feature_scanlist_test.go`: 7 tests — ScanList relation
mapping for struct slices
- Add testdata SQL templates for With tests

**PostgreSQL adaptations from MySQL:**
- `AUTO_INCREMENT` → `SERIAL/BIGSERIAL`
- `datetime` → `timestamp`
- MySQL backticks → PostgreSQL double quotes for identifiers
- Timestamp format handling for soft time fields

## Test plan
- [x] Run `go test -v -run "Test_Model_Soft" -count=1` in
`contrib/drivers/pgsql`
- [x] Run `go test -v -run "Test_Model_With" -count=1` in
`contrib/drivers/pgsql`
- [x] Run `go test -v -run "Test_Model_ScanList" -count=1` in
`contrib/drivers/pgsql`

ref #4689
2026-02-26 09:49:48 +08:00
dc08920a7f feat(gcrypto/gsha512): add sha512 implements (#4667)
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-26 09:47:08 +08:00
1ab0b18115 feat(os/gcfg): add GetEffective method with standard config priority (#4673)
## Summary
- Add `GetEffective` and `MustGetEffective` methods following 12-Factor
App config priority
- Priority: Command line > Environment variables > Config file > Default
value
- Add clarifying notes to existing `GetWithEnv`/`GetWithCmd` methods
- Add comprehensive unit tests

## Test plan
- [x] All gcfg unit tests pass (44 tests)
- [x] New `Test_GetEffective` covers 6 scenarios:
  - Config file only
  - Env overrides config
  - Cmd overrides env
  - Default value fallback
  - Empty string override (industry standard)
  - Key only in env

Closes #4650

---------

Co-authored-by: John Guo <claymore1986@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-26 09:45:56 +08:00
ebd78fb533 test(contrib/drivers/pgsql): add transaction, where, hook, ctx test coverage (#4675)
## Summary
- Add 4 new test files for pgsql driver to align with MySQL driver test
coverage (86 test functions, ~3100 lines)
- `pgsql_z_unit_transaction_test.go`: 40 tests — TX CRUD, nested
transactions, propagation behaviors
(Required/RequiresNew/Nested/NotSupported/Mandatory/Never/Supports),
isolation levels (ReadCommitted/RepeatableRead/Serializable), savepoints
- `pgsql_z_unit_model_where_test.go`: 35 tests — Where variants
(string/slice/map/struct/gmap), comparisons (LT/LTE/GT/GTE), IN/NotIn,
Between, Like, Null, EXISTS/NOT EXISTS subqueries, WherePrefix with JOIN
- `pgsql_z_unit_feature_hook_test.go`: 6 tests —
Select/Insert/Update/Delete hooks, Count with hook, hook chaining and
error handling
- `pgsql_z_unit_feature_ctx_test.go`: 5 tests — context propagation,
trace logging (SpanId/TraceId), transaction context, timeout
cancellation
- Migrate `Test_Model_Where` from `pgsql_z_unit_model_test.go` to
dedicated where test file with expanded coverage (2 → 30+ sub-tests)

**PostgreSQL adaptations from MySQL:**
- `?` → `$N` placeholders for raw SQL
- `REPLACE INTO` → `OnConflict("id").Save()` for upsert
- `AUTO_INCREMENT` → `bigserial`
- `user` alias → `"user"` (reserved word in PgSQL)
- Skip `READ UNCOMMITTED` dirty read test (PgSQL treats as READ
COMMITTED)

## Test plan
- [ ] Run `go test -v -run "Test_TX_" -count=1` in
`contrib/drivers/pgsql`
- [ ] Run `go test -v -run "Test_Model_Where" -count=1` in
`contrib/drivers/pgsql`
- [ ] Run `go test -v -run "Test_Model_Hook" -count=1` in
`contrib/drivers/pgsql`
- [ ] Run `go test -v -run "Test_Ctx" -count=1` in
`contrib/drivers/pgsql`
- [ ] Verify `go vet ./...` passes (only unreachable code warnings
matching MySQL driver pattern)

ref #4689

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-26 09:43:32 +08:00
841003eeb3 test(contrib/drivers/gaussdb): add transaction, where, hook, ctx test coverage (#4685)
## Summary
- Port 4 test files from PgSQL driver to GaussDB driver to align test
coverage
- Add `gaussdb_z_unit_transaction_test.go` (40 tests): nested
transactions, savepoints, rollback, panic recovery, context propagation
- Add `gaussdb_z_unit_model_where_test.go` (35 tests): comprehensive
Where clause combinations (map, slice, struct, pointer, operators, nil,
empty)
- Add `gaussdb_z_unit_feature_hook_test.go` (6 tests): model hook
callbacks (Select/Insert/Update/Delete)
- Add `gaussdb_z_unit_feature_ctx_test.go` (5 tests): context
propagation, timeout, logging with context
- Remove old `Test_Model_Where` (2 sub-tests) from
`gaussdb_z_unit_model_test.go`, replaced by comprehensive version in
dedicated where test file (35 tests)
- **86 new test functions**, ~3,224 net new lines

## Test plan
- [x] `go build ./...` passes
- [x] `gofmt` and `gci` applied
- [x] No remaining `pgsql` references in new files
- [ ] Run `go test -v -run "Test_TX_" -count=1` against GaussDB instance
- [ ] Run `go test -v -run "Test_Model_Where" -count=1` against GaussDB
instance
- [ ] Run `go test -v -run "Test_Model_Hook" -count=1` against GaussDB
instance
- [ ] Run `go test -v -run "Test_Ctx" -count=1` against GaussDB instance

ref #4689
2026-02-26 09:42:10 +08:00
1739d4dfb2 feat(i18n/gi18n): decoding and loading i18n files content by automatic file extension check (#4662)
The i18n file handling now checks file extensions instead of content. If
no extension info is found, it reverts to checking the file content.
2026-02-11 15:19:40 +08:00
46cc4cef9e test(contrib/drivers/pgsql): add Union, DO and Raw Where test coverage (#4679)
## Summary
- Add `pgsql_z_unit_feature_union_test.go`: 4 tests for Union/UnionAll
on both db and model level
- Add `pgsql_z_unit_feature_model_do_test.go`: 10 tests for DO (Data
Object) pattern - insert, batch insert, update, pointer fields, WHERE,
DAO pattern, and field prefix handling
- Enhance `pgsql_z_unit_raw_test.go`: add `Test_Raw_Where` for subquery
NOT EXISTS and field comparison using `gdb.Raw()`, adapted for PgSQL
double-quote quoting
- Add `testdata/table_with_prefix.sql` for PgSQL-compatible FieldPrefix
test

All tests adapted from MySQL driver test suite with PgSQL-specific
adjustments:
- Nullable table schema for DO partial inserts (PgSQL NOT NULL is
stricter than MySQL)
- Double-quote identifier quoting instead of backticks
- Unquoted table aliases in generated SQL

## Test plan
- [x] All 15 new tests pass locally
- [x] Full pgsql test suite (107 tests) passes with zero regressions

ref #4689

---------

Co-authored-by: John Guo <claymore1986@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-11 14:41:42 +08:00
90331d85bf test(contrib/drivers/gaussdb): add union, DO, raw where test coverage (#4687)
## Summary
- Port 2 test files, 1 testdata SQL, and append Test_Raw_Where from
PgSQL driver to GaussDB driver
- Add `gaussdb_z_unit_feature_union_test.go` (4 tests): Union/UnionAll
query operations
- Add `gaussdb_z_unit_feature_model_do_test.go` (10 tests): DO
struct-based CRUD operations
- Append `Test_Raw_Where` to `gaussdb_z_unit_raw_test.go` (1 test): raw
SQL in Where with subquery and column comparison
- Add `testdata/table_with_prefix.sql` for DO prefix tests
- **15 new test functions**, ~605 net new lines

## Test plan
- [x] `go build ./...` passes
- [x] `gofmt` and `gci` applied
- [x] No remaining `pgsql` references in new files
- [ ] Run full test suite against GaussDB instance

ref #4689

---------

Co-authored-by: John Guo <claymore1986@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-11 14:40:46 +08:00
98fd2a1973 chore: cleanup makefile, remove unnecessary scripts (#4684)
This pull request primarily removes submodule management targets from
the `Makefile` and makes minor updates to the project documentation in
both English and Chinese. The most important changes are grouped below.

Makefile cleanup:

* Removed the `subup` and `subsync` targets from the `Makefile`,
eliminating commands related to updating and committing submodules.

Documentation updates:

* Updated the logo alt text in both `README.MD` and `README.zh_CN.MD`
from "goframe gf logo" to "goframe logo" for clarity.
[[1]](diffhunk://#diff-01e6d9ffed056a02cae8d8a0ec5d476a64d017bf85c0d5a94bb23ca21f33f5aaL4-R4)
[[2]](diffhunk://#diff-c93759cb9a9500f20e551c741eb167fc72825fd638d36121357feb8253ce6ac1L4-R4)
* Revised a description in `README.zh_CN.MD` to clarify the framework's
purpose, changing “一个强大的框架” to “一款强大的框架”.
* Simplified the license description in `README.zh_CN.MD` to state
"100%开源和免费".
2026-02-11 14:37:49 +08:00
d5633ebad7 fix(contrib/registry): etcd doKeepAlive does not exit even when client context done (#4669)
Fixed #4668 

```
// client
package main

import (
	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/gsvc"
	"github.com/gogf/gf/v2/os/gctx"

	"github.com/gogf/gf/contrib/registry/etcd/v2"
)

func main() {
	gsvc.SetRegistry(etcd.New(`etcd.etcd.orb.local:2379`))

	var (
		ctx    = gctx.New()
		client = g.Client()
	)
	client.SetDiscovery(gsvc.GetRegistry())
	res := client.GetContent(ctx, `http://hello.svc/`)
	g.Log().Info(ctx, res)
}

// server
package main

import (
	"github.com/gogf/gf/contrib/registry/etcd/v2"
	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
	"github.com/gogf/gf/v2/net/gsvc"
)

func main() {
	gsvc.SetRegistry(etcd.New(`etcd.etcd.orb.local:2379`))

	s := g.Server(`hello.svc`)
	s.BindHandler("/", func(r *ghttp.Request) {
		g.Log().Info(r.Context(), `request received`)
		r.Response.Write(`Hello world`)
	})
	s.Run()
}

```

```
        /Users/shown/workspace/golang/open_source/gf/contrib/registry/etcd/etcd_registrar.go:105
2. context deadline exceeded
 
Stack:
1.  github.com/gogf/gf/contrib/registry/etcd/v2.(*Registry).doKeepAlive
    /Users/shown/workspace/golang/open_source/gf/contrib/registry/etcd/etcd_registrar.go:107

{"level":"warn","ts":"2026-01-30T22:30:33.863409+0800","logger":"etcd-client","caller":"v3@v3.5.17/retry_interceptor.go:63","msg":"retrying of unary invoker failed","target":"etcd-endpoints://0x1400023a780/etcd.etcd.orb.local:2379","attempt":0,"error":"rpc error: code = DeadlineExceeded desc = latest balancer error: last connection error: connection error: desc = \"transport: Error while dialing: dial tcp 192.168.138.6:2379: connect: operation timed out\""}
2026-01-30T22:30:33.863+08:00 [ERRO] keepalive retry register failed, will retry in 2s: etcd grant failed with keepalive ttl "10s": context deadline exceeded
1. etcd grant failed with keepalive ttl "10s"
   1).  github.com/gogf/gf/contrib/registry/etcd/v2.(*Registry).doRegisterLease
        /Users/shown/workspace/golang/open_source/gf/contrib/registry/etcd/etcd_registrar.go:38
   2).  github.com/gogf/gf/contrib/registry/etcd/v2.(*Registry).doKeepAlive
        /Users/shown/workspace/golang/open_source/gf/contrib/registry/etcd/etcd_registrar.go:105
2. context deadline exceeded
 
Stack:
1.  github.com/gogf/gf/contrib/registry/etcd/v2.(*Registry).doKeepAlive
    /Users/shown/workspace/golang/open_source/gf/contrib/registry/etcd/etcd_registrar.go:107

{"level":"warn","ts":"2026-01-30T22:30:40.865971+0800","logger":"etcd-client","caller":"v3@v3.5.17/retry_interceptor.go:63","msg":"retrying of unary invoker failed","target":"etcd-endpoints://0x1400023a780/etcd.etcd.orb.local:2379","attempt":0,"error":"rpc error: code = DeadlineExceeded desc = latest balancer error: last connection error: connection error: desc = \"transport: Error while dialing: dial tcp 192.168.138.6:2379: connect: operation timed out\""}
2026-01-30T22:30:40.866+08:00 [ERRO] keepalive retry register failed, will retry in 3s: etcd grant failed with keepalive ttl "10s": context deadline exceeded
1. etcd grant failed with keepalive ttl "10s"
   1).  github.com/gogf/gf/contrib/registry/etcd/v2.(*Registry).doRegisterLease
        /Users/shown/workspace/golang/open_source/gf/contrib/registry/etcd/etcd_registrar.go:38
   2).  github.com/gogf/gf/contrib/registry/etcd/v2.(*Registry).doKeepAlive
        /Users/shown/workspace/golang/open_source/gf/contrib/registry/etcd/etcd_registrar.go:105
2. context deadline exceeded
 
Stack:
1.  github.com/gogf/gf/contrib/registry/etcd/v2.(*Registry).doKeepAlive
    /Users/shown/workspace/golang/open_source/gf/contrib/registry/etcd/etcd_registrar.go:107

2026-01-30T22:30:43.903+08:00 [DEBU] etcd put success with key "/service/default/default/hello.svc/latest/192.168.27.229:60201,192.168.139.3:60201,192.168.163.0:60201", value "{"insecure":true,"protocol":"http"}", lease "7587892536770637317"
2026-01-30T22:30:43.904+08:00 [INFO] keepalive retry register success for service "/service/default/default/hello.svc/latest/192.168.27.229:60201,192.168.139.3:60201,192.168.163.0:60201"
2026-01-30T22:30:51.385+08:00 [INFO] {e0ffad1cac888f18eed5ef7a3ba3f6d0} request received
2026-01-30T22:30:52.121+08:00 [INFO] {78ca8848ac888f18573a386e0b596eaa} request received
```

---------

Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
2026-02-11 14:37:15 +08:00
58d6410291 fix(registry/etcd): etcd.NewWithClient() has no DialTimeout (#4670)
# Description

The `etcd.NewWithClient()` function internally does not set a
`DialTimeout` value, which causes it to default to 0. This leads to all
`context.WithTimeout(context.Background(), r.etcdConfig.DialTimeout)`
calls immediately timing out, as a timeout of 0 results in instant
expiration.

# Example

```go
package main

import (
	"context"
	"testing"
	"time"

	"github.com/gogf/gf/contrib/registry/etcd/v2"
	"github.com/gogf/gf/v2/errors/gerror"
	"github.com/gogf/gf/v2/net/gsvc"
	clientv3 "go.etcd.io/etcd/client/v3"
)

func TestEtcdWithClient(t *testing.T) {
	cli, _ := clientv3.New(clientv3.Config{
		Endpoints:   []string{"http://127.0.0.1:2379"},
		DialTimeout: 2 * time.Second,
	})
	defer cli.Close()

	registry := etcd.NewWithClient(cli)
	_, err := registry.Register(context.Background(), &gsvc.LocalService{
		Name:      "test",
		Endpoints: gsvc.NewEndpoints("127.0.0.1:8888"),
	})
	if err != nil {
		t.Error(gerror.Stack(err))
		return
	}
}
```

Running tool: /opt/homebrew/bin/go test -test.fullpath=true -timeout 30s
-run ^TestEtcdWithClient$ etop.roommanageserver

=== RUN   TestEtcdWithClient

{"level":"warn","ts":"2026-01-31T09:59:06.994867+0800","logger":"etcd-client","caller":"v3@v3.6.7/retry_interceptor.go:65","msg":"retrying
of unary invoker
failed","target":"etcd-endpoints://0x14000262f00/127.0.0.1:2379","method":"/etcdserverpb.Lease/LeaseGrant","attempt":0,"error":"rpc
error: code = DeadlineExceeded desc = context deadline exceeded"}
/Users/guolihui/projects/mpl-poker/room-manage-server/main_test.go:27:
1. etcd grant failed with keepalive ttl "10s"
1).
github.com/gogf/gf/contrib/registry/etcd/v2.(*Registry).doRegisterLease

/Users/guolihui/projects/mpl-poker/room-manage-server/gfv2/contrib/registry/etcd/etcd_registrar.go:38
2). github.com/gogf/gf/contrib/registry/etcd/v2.(*Registry).Register

/Users/guolihui/projects/mpl-poker/room-manage-server/gfv2/contrib/registry/etcd/etcd_registrar.go:24
           3).  etop%2eroommanageserver.TestEtcdWithClient
/Users/guolihui/projects/mpl-poker/room-manage-server/main_test.go:22
        2. context deadline exceeded

--- FAIL: TestEtcdWithClient (0.00s)
2026-02-11 14:25:19 +08:00
54087de518 test(contrib/drivers/pgsql): add Builder/Subquery/Join/Struct tests (#4680)
## Summary
- Port MySQL test coverage for Builder, Subquery, Join, and Struct
features to PgSQL driver
- Add 4 new test files with 23 test functions covering builder patterns,
subquery WHERE/HAVING/Model, all JOIN types, and struct scanning
- PgSQL dialect adaptations: double-quoted identifiers, GROUP BY with
HAVING, letter-prefixed table names, int64 id assertions, removed `Uid`
field

## Test plan
- [x] Builder tests pass: `go test -v -run
"Test_Model_Builder|Test_Safe_Builder" -count=1`
- [x] Subquery tests pass: `go test -v -run "Test_Model_SubQuery"
-count=1`
- [x] Join tests pass: `go test -v -run
"Test_Model_.*Join.*|Test_Model_FieldsPrefix" -count=1`
- [x] Struct tests pass: `go test -v -run
"Test_Model_Embedded|Test_Struct|Test_Structs|Test_Model_Scan|Test_Scan_Auto"
-count=1`
- [x] Full PgSQL test suite: 113/113 PASS

ref #4689
2026-02-11 13:51:47 +08:00
fc39fffe9c test(contrib/drivers/gaussdb): add builder, subquery, join, struct test coverage (#4688)
## Summary
- Port 4 test files from PgSQL driver to GaussDB driver
- Add `gaussdb_z_unit_feature_model_builder_test.go` (2 tests): SQL
builder with raw expressions and safe mode
- Add `gaussdb_z_unit_feature_model_subquery_test.go` (3 tests):
subquery in Select/Where/Having
- Add `gaussdb_z_unit_feature_model_join_test.go` (7 tests):
LeftJoin/RightJoin/InnerJoin with various conditions
- Add `gaussdb_z_unit_feature_model_struct_test.go` (11 tests):
struct-based insert/update/scan with tag mapping
- **23 new test functions**, ~861 net new lines

## Test plan
- [x] `go build ./...` passes
- [x] `gofmt` and `gci` applied
- [x] No remaining `pgsql` references in new files
- [ ] Run full test suite against GaussDB instance

ref #4689
2026-02-11 13:50:30 +08:00
6a3ea897a8 docs: Update README Add DeepWiki badges (#4661) 2026-01-28 15:42:11 +08:00
91f9864b25 fix: update gf cli to v2.10.0 (#4658)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

Co-authored-by: gqcn <gqcn@users.noreply.github.com>
2026-01-27 17:43:29 +08:00
8c8c7c8c71 feat: new version v2.10.0 (#4657)
This pull request upgrades the GoFrame framework and all related
dependencies from version `v2.9.8` (and similar) to `v2.10.0` across the
codebase. It also refactors the `.make_version.sh` script to improve
cross-platform compatibility when editing files, and ensures
documentation reflects the new version. These changes help keep the
project up-to-date and simplify version management.

**Dependency upgrades:**

* Updated all `go.mod` files in the main repo and contrib modules to
require `github.com/gogf/gf/v2 v2.10.0` (replacing `v2.9.8` and similar)
for consistency and latest features/bugfixes.
[[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)

**Script improvements:**

* Refactored `.make_version.sh` to use a new `sed_inplace` function for
in-place file editing, improving cross-platform support (Linux/macOS)
and removing reliance on a global variable for the sed command.
* Updated `.make_version.sh` to use `sed_inplace` consistently for
version replacement and dependency cleanup steps, ensuring robust file
modification regardless of OS.
[[1]](diffhunk://#diff-546db9206ba1b7973e6187a1025b3904a0b08681d40d0ee4767082040fd0f661L46-R47)
[[2]](diffhunk://#diff-546db9206ba1b7973e6187a1025b3904a0b08681d40d0ee4767082040fd0f661L84-R97)
* Added a step in `.make_version.sh` to insert local development replace
directives for Go modules, streamlining local testing and development.

**Documentation updates:**

* Updated contributor badge version in `README.MD` and `README.zh_CN.MD`
to reflect the new GoFrame version (`v2.10.0`).
[[1]](diffhunk://#diff-01e6d9ffed056a02cae8d8a0ec5d476a64d017bf85c0d5a94bb23ca21f33f5aaL48-R48)
[[2]](diffhunk://#diff-c93759cb9a9500f20e551c741eb167fc72825fd638d36121357feb8253ce6ac1L48-R48)
2026-01-26 20:37:48 +08:00
73211707fb refactor(container): add default nil checker, rename RegisterNilChecker to SetNilChecker, migrate instance containers to type-safe generics (#4630)
## 变更说明

本 PR 主要对代码库进行了重构,以提升类型安全性和优化连接管理实现。

### 详细变更

#### 1. 数据库连接管理优化
- 修改 `RegisterNilChecker`方法返回实例以支持链式调用,涉及
`KVMap`、`ListKVMap`、`TSet`、`AVLKVTree`、`BKVTree`、`RedBlackKVTree`
等多个容器类型
- 更新 `Core`结构体中 `links`字段类型为类型安全的 `KVMap[ConfigNode, *sql.DB]`
- 添加专门的链接检查器函数用于连接池管理
- 使用泛型 `KVMap`替代原始 map 类型提升类型安全性
- 简化连接关闭逻辑并移除不必要的类型断言
- 优化统计功能中的迭代器实现提高性能

#### 2. 数据库驱动类型安全增强
- 将 dm、gaussdb、mssql、oracle 驱动中的 `conflictKeySet` 从 `gset.New`修改为
`gset.NewStrSet`
- 统一使用字符串集合类型以提高类型安全性

#### 3. 配置文件适配器类型安全改进
- 将 `jsonMap`从 `StrAnyMap` 类型更改为泛型 `KVMap[string, *gjson.Json]` 类型
- 添加 `jsonMapChecker` 函数用于 JSON 对象验证
- 使用 `NewKVMapWithChecker` 替代 `NewStrAnyMap` 提高类型安全性
- 简化数据库链接关闭日志中的键值转换逻辑

## 影响范围

- 数据库连接管理模块
- 多个数据库驱动实现
- 配置文件管理系统

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: John Guo <john@johng.cn>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-23 16:37:38 +08:00
609f44c5fe fix(cmd/gf): fix genservice losing versioned import paths (#4242) (#4638)
## Summary
- Fix `gf gen service` incorrectly handling versioned imports (e.g.,
`github.com/minio/minio-go/v7` → `github.com/minio/minio-go`)
- The root cause was faulty package name inference from import paths -
Go allows package names to differ from directory names
- Solution: Keep all non-anonymous imports and let gofmt clean up unused
ones

## Changes
- Simplified `calculateImportedItems` function in
`genservice_calculate.go`
- Added test case for versioned imports and aliased imports

## Test plan
- [x] All existing genservice tests pass (`Test_Gen_Service_Default`,
`Test_Issue3328`, `Test_Issue3835`)
- [x] New test `Test_Issue4242` verifies both versioned imports and
aliased imports are preserved
- [x] Verified generated files match expected output exactly

Closes #4242

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-22 20:45:19 +08:00
0a82036da5 feat(contrib/registry): update nacos sdk to 2.3.5 (#4628)
- Update nacos go sdk to 2.3.5;
- ctx params not use, skip it;
- adjust docs style

---------

Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
Co-authored-by: hailaz <739476267@qq.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-22 19:09:06 +08:00
b4053ed32e feat(os/gcfg): add Loader with automatic struct binding and config watching (like Spring Boot @ConfigurationProperties) (#4575)
# Loader 配置加载器

Loader 是一个通用的配置管理器,提供了类似于 Spring Boot
的`@ConfigurationProperties`的配置加载、监控、更新和管理功能。

## 功能特性

- **泛型支持**:使用 Go 泛型,类型安全的配置绑定
- **配置加载**:从配置源加载数据并绑定到结构体
- **配置监控**:自动监控配置变化并更新
- **自定义转换器**:支持自定义数据转换函数
- **回调处理**:配置变更时的回调函数
- **错误处理**:灵活的错误处理机制

## 安装

```bash
go get github.com/gogf/gf/v2
```

## 使用示例

### 1. 基本用法

#### 用法一

```go
package main

import (
	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/os/gcfg"
	"github.com/gogf/gf/v2/os/gctx"
)

type AppConfig struct {
	Name     string       `json:"name"`
	Age      int          `json:"age"`
	Enabled  bool         `json:"enabled"`
	Features []string     `json:"features"`
	Server   ServerConfig `json:"server"`
}

type ServerConfig struct {
	Host string `json:"host"`
	Port int    `json:"port"`
}

func main() {
	ctx := gctx.New()
	// 创建配置器实例
	loader := gcfg.NewLoader[AppConfig](g.Cfg("test"), "")

	// 加载和监听配置
	loader.MustLoadAndWatch(ctx, "test-watcher")

	// 获取配置
	config := loader.Get()
	fmt.Println(config.Name)
}
```

#### 用法二

```go
package main

import (
	"fmt"
	"github.com/gogf/gf/v2/os/gcfg"
	"github.com/gogf/gf/v2/os/gctx"
)

type AppConfig struct {
	Name     string       `json:"name"`
	Age      int          `json:"age"`
	Enabled  bool         `json:"enabled"`
	Features []string     `json:"features"`
	Server   ServerConfig `json:"server"`
}

type ServerConfig struct {
	Host string `json:"host"`
	Port int    `json:"port"`
}

func main() {
	ctx := gctx.New()

	// 使用单独的适配器创建
	// 创建配置管理器
	cfg, _ := gcfg.NewAdapterFile("test.yaml")
	// 创建配置器实例
	loader := gcfg.NewLoaderWithAdapter[AppConfig](cfg, "")

	// 加载和监听配置
	loader.MustLoadAndWatch(ctx, "test-watcher")

	// 获取配置
	config := loader.Get()
	fmt.Println(config.Name)
}
```

### 2. 配置监控

```go


// 仅加载App配置
loader := gcfg.NewLoaderWithAdapter[AppConfig](cfg, "app")

// 设置配置变更回调
loader.OnChange(func (updated AppConfig) error {
// 配置变更时的处理逻辑
println("配置已更新:", updated.Name)
return nil
})

// 加载数据
err := loader.Load(ctx)
if err != nil {
panic(err)
}

// 开始监控配置变化
err := loader.Watch(context.Background(), "my-watcher")
if err != nil {
panic(err)
}

```

### 3. 自定义转换器

```go
// 设置自定义转换器
loader.SetConverter(func (data any, target *AppConfig) error {
// 自定义数据转换逻辑
return nil
})
```

### 4. 便捷方法

```go
// 一步完成加载和监控
loader.MustLoadAndWatch(context.Background(), "my-app")
```

## API 参考

### `NewLoader`

创建一个新的 Loader 实例。

```go
func NewLoader[T any](config *Config, propertyKey string, targetStruct ...*T) *Loader[T]
```

参数:

- `config`: 配置实例,用于监控变化
- `propertyKey`: 监控的属性键模式(使用 "" 或 "." 监控所有配置)
- `targetStruct`: 接收配置值的结构体指针(可选)

### `NewLoaderWithAdapter`

使用适配器创建一个新的 Loader 实例。

```go
func NewLoaderWithAdapter[T any](adapter Adapter, propertyKey string, targetStruct ...*T) *Loader[T]
```

### `Load`

从配置实例加载数据并绑定到目标结构体。

```go
func (l *Loader[T]) Load(ctx context.Context) error
```

### `MustLoad`

与 Load 类似,但出错时会 panic。

```go
func (l *Loader[T]) MustLoad(ctx context.Context)
```

### `Watch`

开始监控配置变化并自动更新目标结构体。

```go
func (l *Loader[T]) Watch(ctx context.Context, name string) error
```

### `MustWatch`

与 Watch 类似,但出错时会 panic。

```go
func (l *Loader[T]) MustWatch(ctx context.Context, name string)
```

### `MustLoadAndWatch`

便捷方法,调用 MustLoad 和 MustWatch。

```go
func (l *Loader[T]) MustLoadAndWatch(ctx context.Context, name string)
```

### `Get`

返回当前配置结构体。

```go
func (l *Loader[T]) Get() T
```

### `GetPointer() *T`

返回指向当前配置结构体的指针。

```go
func (l *Loader[T]) GetPointer() *T
```

### `OnChange`

设置配置变化时调用的回调函数。

```go
func (l *Loader[T]) OnChange(fn func (updated T) error)
```

### `SetConverter`

设置在 Load 操作期间使用的自定义转换函数。

```go
func (l *Loader[T]) SetConverter(converter func (data any, target *T) error)
```

### `SetWatchErrorHandler`

设置在 Watch 过程中 Load 操作失败时调用的错误处理函数。

```go
func (l *Loader[T]) SetWatchErrorHandler(errorFunc func(ctx context.Context, err error))
```

### `SetReuseTargetStruct`

设置是否在更新时重用相同的目标结构体或创建新结构体。

```go
func (l *Loader[T]) SetReuseTargetStruct(reuse bool)
```

### `StopWatch`

停止监控配置变化并移除关联的监控器。

```go
func (l *Loader[T]) StopWatch(ctx context.Context) (bool, error)
```

### `IsWatching`

返回 Loader 是否正在监控配置变化。

```go
func (l *Loader[T]) IsWatching() bool
```

## 高级用法

### 监控特定配置键

```go
// 只监控特定配置键
loader := gcfg.NewLoaderWithAdapter[ServerConfig](cfg, "server")
```

### 使用默认值

```go
// 创建带默认值的目标结构体
var targetConfig AppConfig
targetConfig.Name = "default-app" // 设置默认值

loader := gcfg.NewLoaderWithAdapter(cfg, "", &targetConfig)
```

## 错误处理

Loader 提供了灵活的错误处理机制:

```go
loader.SetWatchErrorHandler(func(ctx context.Context, err error) {
    // 处理加载错误
    log.Printf("配置加载失败: %v", err)
})
```

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2026-01-22 19:04:52 +08:00
232 changed files with 45854 additions and 6317 deletions

3
.gitignore vendored
View File

@ -25,4 +25,5 @@ node_modules
output
.example/
.golangci.bck.yml
*.exe
*.exe
.aiprompt.zh.md

View File

@ -1,7 +1,6 @@
version: "2"
run:
concurrency: 4
go: "1.25"
modules-download-mode: readonly
issues-exit-code: 2
tests: false

View File

@ -1,5 +1,16 @@
#!/usr/bin/env bash
# Function to run sed in-place with OS-specific options
sed_replace() {
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS - requires empty string after -i
sed -i '' "$@"
else
# Linux/Windows Git Bash
sed -i "$@"
fi
}
workdir=.
echo "Prepare to tidy all go.mod files in the ${workdir} directory"
@ -27,9 +38,9 @@ for file in `find ${workdir} -name go.mod`; do
cd $goModPath
# Remove indirect dependencies
sed -i '/\/\/ indirect/d' go.mod
sed_replace '/\/\/ indirect/d' go.mod
go mod tidy
# Remove toolchain line if exists
sed -i '' '/^toolchain/d' go.mod
sed_replace '/^toolchain/d' go.mod
cd - > /dev/null
done

View File

@ -1,19 +1,16 @@
#!/usr/bin/env bash
# Function to detect OS and set sed parameters
setup_sed() {
# Function to run sed in-place with OS-specific options
sed_replace() {
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
SED_INPLACE="sed -i ''"
# macOS - requires empty string after -i
sed -i '' "$@"
else
# Linux/Windows Git Bash
SED_INPLACE="sed -i"
sed -i "$@"
fi
}
# Initialize sed command
setup_sed
if [ $# -ne 2 ]; then
echo "Parameter exception, please execute in the format of $0 [directory] [version number]"
echo "PS$0 ./ v2.4.0"
@ -43,10 +40,11 @@ fi
if [[ true ]]; then
# Use sed to replace the version number in version.go
$SED_INPLACE 's/VERSION = ".*"/VERSION = "'${newVersion}'"/' version.go
sed_replace 's/VERSION = ".*"/VERSION = "'${newVersion}'"/' version.go
# Use sed to replace the version number in README.MD
$SED_INPLACE 's/version=[^"]*/version='${newVersion}'/' README.MD
sed_replace 's/version=[^"]*/version='${newVersion}'/' README.MD
sed_replace 's/version=[^"]*/version='${newVersion}'/' README.zh_CN.MD
fi
if [ -f "go.work" ]; then
@ -70,6 +68,8 @@ for file in `find ${workdir} -name go.mod`; do
fi
cd $goModPath
# Add replace directive for local development.
if [ $goModPath = "./cmd/gf" ]; then
mv go.work go.work.version.bak
go mod edit -replace github.com/gogf/gf/v2=../../
@ -81,20 +81,20 @@ for file in `find ${workdir} -name go.mod`; do
go mod edit -replace github.com/gogf/gf/contrib/drivers/sqlite/v2=../../contrib/drivers/sqlite
fi
# Remove indirect dependencies
sed -i '/\/\/ indirect/d' go.mod
sed_replace '/\/\/ indirect/d' go.mod
go mod tidy
# Remove toolchain line if exists
$SED_INPLACE '/^toolchain/d' go.mod
sed_replace '/^toolchain/d' go.mod
# Upgrading only GoFrame related libraries, sometimes even if a version number is specified,
# Upgrading only GoFrame related libraries, sometimes even if a version number is specified,
# it may not be possible to successfully upgrade. Please confirm before submitting the code
go list -f "{{if and (not .Indirect) (not .Main)}}{{.Path}}@${newVersion}{{end}}" -m all | grep "^github.com/gogf/gf"
go list -f "{{if and (not .Indirect) (not .Main)}}{{.Path}}@${newVersion}{{end}}" -m all | grep "^github.com/gogf/gf" | xargs -L1 go get -v
go list -f "{{if and (not .Indirect) (not .Main)}}{{.Path}}@${newVersion}{{end}}" -m all | grep "^github.com/gogf/gf" | xargs -L1 go get -v
# Remove indirect dependencies
sed -i '/\/\/ indirect/d' go.mod
sed_replace '/\/\/ indirect/d' go.mod
go mod tidy
# Remove toolchain line if exists
$SED_INPLACE '/^toolchain/d' go.mod
sed_replace '/^toolchain/d' go.mod
if [ $goModPath = "./cmd/gf" ]; then
go mod edit -dropreplace github.com/gogf/gf/v2
go mod edit -dropreplace github.com/gogf/gf/contrib/drivers/clickhouse/v2

View File

@ -1,231 +0,0 @@
# go list 性能优化 - 完成报告
**日期**2026-01-09
**优化目标**:减少 `go list` 调用次数,提升依赖分析性能
**优化结果**:✅ 从 3 次调用减少到 2 次调用 (-33%)
---
## 问题分析
### 原始问题
`cmddep_analyzer.go``loadPackages()` 方法中存在 **3 次 `go list` 调用**
1. `go list -json %s` (行 158)
- 目标:获取指定的包信息
- 问题:只获取主包,不含依赖信息
2. `go list -json -deps %s` (行 179)
- 目标:获取指定包及其所有依赖
- 问题:和第一次调用冗余,且可能因时间差而导致数据不一致
3. `go list -json -m all` (行 198)
- 目标:获取所有模块信息(包括在 go.mod 中但代码未使用的模块)
- 问题:第三次调用导致性能开销
### 性能影响
- **频繁I/O**:每次 `go list` 都涉及 Go 工具链的启动和包元数据扫描
- **时间累积**:大型项目中每次调用可能耗时 200-500ms3 次调用总耗时可达 1-1.5s
- **不一致风险**:连续调用可能因依赖版本变更而返回不同结果
---
## 优化方案
### 关键优化点
**优化前的调用顺序**
```
调用1: go list -json %s (主包)
调用2: go list -json -deps %s (主包+依赖)
调用3: go list -json -m all (所有模块)
```
**优化后的调用顺序**
```
调用1: go list -json -m all (所有模块) - 快速
↓ 用模块信息预填充 packages 集合
调用2: go list -json -deps %s (主包+依赖) - 覆盖/补充包信息
```
### 具体改进
1. **取消第一次调用**
- 原因:`go list -json -deps` 已包含主包信息,第一次调用冗余
- 效果:减少 1 次调用 (-33%)
2. **调整模块加载时机**
- 原因:模块信息加载应先执行,为包信息加载做准备
- 优势:支持 go.mod 中声明但未在代码中直接使用的模块出现在依赖图中
3. **优化错误处理**
- 模块加载失败不中断:`if err != nil { moduleResult = "" }`
- 保证即使模块加载失败,包加载仍能继续
---
## 实现细节
### 改动代码 (cmddep_analyzer.go)
```go
// loadPackages loads package information using go list with optimized approach.
// OPTIMIZATION: Reduced from 3 separate go list calls to 2 efficient calls:
// Previously:
// 1. go list -json %s (target packages only)
// 2. go list -json -deps %s (with dependencies)
// 3. go list -json -m all (all modules)
// Now (optimized):
// 1. go list -json -m all (all modules - fast, definitive)
// 2. go list -json -deps ./... (all packages with dependencies)
func (a *analyzer) loadPackages(ctx context.Context, pkgPath string) error {
// First, load module information - fast, provides metadata
moduleCmd := "go list -json -m all"
moduleResult, err := gproc.ShellExec(ctx, moduleCmd)
if err != nil {
moduleResult = "" // Module loading is optional
}
// Parse modules and pre-populate packages
if moduleResult != "" {
// ... decode modules ...
}
// Second, load package information with dependencies
cmd := fmt.Sprintf("go list -json -deps %s", pkgPath)
result, err := gproc.ShellExec(ctx, cmd)
if err != nil {
// ... error handling ...
}
// Parse packages
// ... decode packages ...
return nil
}
```
---
## 验证结果
### 编译检查
✅ 编译成功,无编译错误
### Lint 检查
✅ 无 lint 警告或错误
### 向后兼容性
**完全兼容**
- `loadPackages()` 方法签名不变
- 返回结果(`a.packages` 数据结构)不变
- 所有调用者代码无需修改
### 功能正确性
✅ 功能保持一致
- 获取相同的包和模块信息
- 建立相同的依赖关系图
- 支持所有原有的过滤和遍历操作
---
## 性能指标
### 理论改进
| 指标 | 优化前 | 优化后 | 改进 |
|------|------|------|------|
| **go list 调用数** | 3 次 | 2 次 | -33% |
| **预期执行时间** | ~600-1500ms | ~400-1000ms | -33% |
### 说明
- 假设每次调用 200-500ms
- 模块加载(`go list -m all`)通常最快,因为不需要扫描代码
- 包依赖加载(`go list -deps`)取决于项目复杂度
---
## 设计决策
### 为什么是 2 次而不是 1 次?
虽然设计目标是"1 次调用",但实际上 2 次调用是更合理的方案:
**原因**
1. `go list -deps %s``go list -m all`**命令标志组合不兼容**
- `-deps` 要求指定包/模块作为分析起点
- `-m` 要求以模块视图操作
- 两者不能在同一命令中有效结合
2. 模块加载和包加载的 **职责不同**
- 模块加载:获取 go.mod 声明的完整依赖
- 包加载:获取代码中实际使用的包
- 两者信息互补
3. **错误隔离**的好处
- 模块加载失败不影响包加载
- 提高系统鲁棒性
---
## 后续优化机会
### 1. 缓存层 (未来版本)
```go
// 缓存 go list 结果,支持增量更新
type PackageCache struct {
modules map[string]bool // 缓存模块清单
packages map[string]*goPackage // 缓存包信息
checksum string // go.mod 校验和
}
```
### 2. 并行加载 (未来版本)
```go
// 并行执行两次 go list 调用
go func() { moduleResult = shell_exec(moduleCmd) }()
result = shell_exec(packageCmd) // 同时执行
// 等待两者完成...
```
### 3. 按需加载 (未来版本)
- 支持渐进式加载(只分析指定包及其直接依赖)
- 支持深度控制(避免加载整个传递依赖树)
---
## 代码变更统计
| 指标 | 值 |
|------|-----|
| 修改文件 | 1 (`cmddep_analyzer.go`) |
| 修改行数 | ~70 行 |
| 删除行数 | 50+ 行 |
| 净增加 | ~20 行 |
| 编译错误 | 0 |
| Lint 警告 | 0 |
| 测试通过 | ✅ |
---
## 总结
**性能优化完成** - 成功将 `go list` 调用从 3 次减少到 2 次,提升了依赖分析性能并改善了代码清晰度。
### 主要成就
- ✅ 减少 1/3 的 go list 调用
- ✅ 改进代码结构(模块优先加载)
- ✅ 增强错误隔离(模块加载失败不影响包加载)
- ✅ 保持 100% 向后兼容
- ✅ 零性能退化
### 建议
- 立即合并此优化
- 后续考虑实现缓存和并行加载进一步改进
---
**完成日期**2026-01-09
**优化者**AI Assistant
**状态**:✅ 完成并验证

View File

@ -1,263 +0,0 @@
# 修复报告 - MainModuleOnly 参数无效问题
**日期**2026-01-09
**优先级**:🔴 高 (功能缺陷)
**状态**:✅ 已修复
---
## 问题描述
`--main-only` (仅主模块) 参数在代码中定义但从未实际被使用,导致该参数完全无效。
### 表现
- 用户使用 `gf dep --main-only` 时,仍然显示所有包(包括子模块的包)
- 参数被解析,但在过滤逻辑中被忽略
### 根本原因
虽然有定义 `MainModuleOnly` 字段在 `FilterOptions` 中,但:
1. `ShouldInclude()` 方法从未检查这个参数
2. `PackageInfo` 没有记录包是否属于主模块的信息
3. 虽然有 `isMainModulePackage()` 方法可以判断,但它不被调用
---
## 修复方案
### 改动 1: 扩展 `PackageInfo` 结构
**文件**`cmddep_analyzer.go` (L51-60)
```go
type PackageInfo struct {
ImportPath string // Full import path
ModulePath string // Module path
Kind PackageKind // Package classification
Tier int // Package tier
Imports []string // Direct imports
IsStdLib bool // Standard library marker
IsModuleRoot bool // Is this the root package of its module
IsMainModule bool // ← NEW: Is this package from the main module
}
```
**原因**:需要在包信息中记录"是否属于主模块",以便在过滤时使用。
### 改动 2: 更新 `buildPackageStore()` 方法
**文件**`cmddep_analyzer.go` (L636-653)
```go
func (a *analyzer) buildPackageStore() *PackageStore {
store := newPackageStore(a.modulePrefix)
for path, goPkg := range a.packages {
pkgInfo := &PackageInfo{
ImportPath: path,
ModulePath: goPkg.Module.Path,
IsStdLib: goPkg.Standard,
Imports: goPkg.Imports,
IsMainModule: a.isMainModulePackage(path), // ← NEW
}
pkgInfo.Kind = store.identifyPackageKind(pkgInfo)
store.packages[path] = pkgInfo
}
return store
}
```
**原因**:在构建 `PackageInfo` 时调用 `isMainModulePackage()` 来填充 `IsMainModule` 字段。
### 改动 3: 修复 `ShouldInclude()` 方法
**文件**`cmddep_analyzer.go` (L325-346)
```go
func (opts *FilterOptions) ShouldInclude(pkg *PackageInfo) bool {
// ← NEW: Check main module filter first
if opts.MainModuleOnly && !pkg.IsMainModule {
return false
}
// Filter by kind
switch pkg.Kind {
case KindStdLib:
if !opts.IncludeStdLib {
return false
}
case KindInternal:
if !opts.IncludeInternal {
return false
}
case KindExternal:
if !opts.IncludeExternal {
return false
}
}
return true
}
```
**原因**:在过滤逻辑中实际检查 `MainModuleOnly` 参数。
---
## 影响范围
### 受影响的功能
- ✅ 命令行 `gf dep --main-only` 命令
- ✅ Web UI "Main module only" 复选框
- ✅ HTTP API `?main=true` 参数
### 受影响的输出格式
- ✅ Tree 格式
- ✅ List 格式
- ✅ JSON 格式
- ✅ Mermaid 格式
- ✅ Dot 格式
- ✅ Reverse 格式
- ✅ Group 格式
---
## 验证结果
### 编译检查
✅ 编译成功,无编译错误
### Lint 检查
✅ 无 lint 警告或错误
### 向后兼容性
**完全兼容**
- 所有现有代码无需修改
- API 签名不变
### 功能正确性
✅ 现在可以正确过滤子模块的包
---
## 使用示例
### 命令行
```bash
# 仅显示主模块的包
gf dep --main-only
# 结合其他参数
gf dep --external --main-only
```
### Web UI
- 勾选 "Main module only" 复选框来过滤掉子模块
### HTTP API
```
GET /api/packages?main=true
GET /api/tree?main=true
```
---
## 技术细节
### `isMainModulePackage()` 工作原理
位于 `cmddep_analyzer.go` (L381-408)
```go
func (a *analyzer) isMainModulePackage(pkg string) bool {
// 没有模块前缀,认为所有包都在主模块中
if a.modulePrefix == "" {
return true
}
// 包不在模块范围内
if !gstr.HasPrefix(pkg, a.modulePrefix) {
return false
}
// 移除模块前缀得到相对路径
relativePath := gstr.TrimLeft(pkg[len(a.modulePrefix):], "/")
if relativePath == "" {
return true // 这是模块根本身
}
// 检查是否有子 go.mod 文件(表示子模块)
parts := gstr.Split(relativePath, "/")
for i := len(parts); i > 0; i-- {
subPath := gstr.Join(parts[:i], "/")
if subPath != "" && gfile.Exists(subPath+"/go.mod") {
return false // 找到了子模块
}
}
return true // 这是主模块的一部分
}
```
**工作流程**
1. 验证包在模块范围内
2. 检查包相对路径中是否存在 `go.mod` 文件
3. 如果存在子 `go.mod`,说明这是子模块,返回 `false`
4. 否则是主模块的一部分,返回 `true`
---
## 相关代码位置
| 组件 | 文件 | 行号 | 用途 |
|------|------|------|------|
| PackageInfo | cmddep_analyzer.go | L51-60 | 数据模型 |
| FilterOptions | cmddep_analyzer.go | L62-77 | 过滤参数 |
| ShouldInclude() | cmddep_analyzer.go | L323-346 | 过滤决策 |
| buildPackageStore() | cmddep_analyzer.go | L636-653 | 数据构建 |
| isMainModulePackage() | cmddep_analyzer.go | L381-408 | 主模块检测 |
---
## 测试建议
### 手动测试
```bash
# 创建有子模块的项目或使用现有项目
cd /path/to/project/with/submodule
# 测试不带参数
gf dep --tree
# 测试只显示主模块
gf dep --tree --main-only
# 验证结果应该显著减少(子模块包被过滤)
```
### 自动测试
建议添加单元测试验证:
- `MainModuleOnly=true``ShouldInclude()` 正确返回 `false` 对于非主模块包
- `buildPackageStore()` 正确设置 `IsMainModule` 字段
- 各种输出格式都正确应用过滤
---
## 总结
**功能修复完成**
| 项 | 状态 |
|----|------|
| **编译** | ✅ 成功 |
| **Lint** | ✅ 无错误 |
| **功能** | ✅ 正常 |
| **兼容性** | ✅ 100% |
现在 `--main-only` 参数可以正确过滤掉子模块的包。
---
**完成日期**2026-01-09
**修复者**AI Assistant
**状态**:✅ 完成并验证

View File

@ -1,5 +1,27 @@
SHELL := /bin/bash
# commit changes with AI-generated commit message
.PHONY: up
up:
@if git diff --quiet HEAD && git diff --cached --quiet && [ -z "$$(git ls-files --others --exclude-standard)" ]; then \
echo "No changes to commit"; \
exit 0; \
fi
@git add -A
@echo "Analyzing changes and generating commit message via AI..."
@set -e; \
MSG=$$(git diff --cached --stat && echo "---" && git diff --cached | head -2000 | \
claude -p "Analyze the git diff above and generate a concise commit message (single line, max 72 chars, lowercase, no quotes). Output only the commit message itself, nothing else." \
--model haiku) || { echo "Error: Claude command failed"; exit 1; }; \
COMMIT_MSG=$$(echo "$$MSG" | tail -1); \
if [ -z "$$COMMIT_MSG" ]; then \
echo "Error: Failed to generate commit message"; \
exit 1; \
fi; \
echo "Commit: $$COMMIT_MSG"; \
git commit -m "$$COMMIT_MSG" && \
git push origin $$(git branch --show-current)
# execute "go mod tidy" on all folders that have go.mod file
.PHONY: tidy
tidy:
@ -52,31 +74,6 @@ tag:
git push origin $$newVersion; \
echo "Tag $$newVersion created and pushed successfully!"
# update submodules
.PHONY: subup
subup:
@set -e; \
echo "Updating submodules..."; \
git submodule init;\
git submodule update;
# update and commit submodules
.PHONY: subsync
subsync: subup
@set -e; \
echo "";\
cd examples; \
echo "Checking for changes..."; \
if git diff-index --quiet HEAD --; then \
echo "No changes to commit"; \
else \
echo "Found changes, committing..."; \
git add -A; \
git commit -m "examples update"; \
git push origin; \
fi; \
cd ..;
# manage docker services for local development
# usage: make docker or make docker cmd=start svc=mysql
.PHONY: docker

View File

@ -1,7 +1,7 @@
English | [简体中文](README.zh_CN.MD)
<div align=center>
<img src="https://goframe.org/img/logo_full.png" width="300" alt="goframe gf logo"/>
<img src="https://goframe.org/img/logo_full.png" width="300" alt="goframe logo"/>
[![Go Reference](https://pkg.go.dev/badge/github.com/gogf/gf/v2.svg)](https://pkg.go.dev/github.com/gogf/gf/v2)
[![GoFrame CI](https://github.com/gogf/gf/actions/workflows/ci-main.yml/badge.svg)](https://github.com/gogf/gf/actions/workflows/ci-main.yml)
@ -19,6 +19,7 @@ English | [简体中文](README.zh_CN.MD)
[![GitHub closed issues](https://img.shields.io/github/issues-closed/gogf/gf?style=flat)](https://github.com/gogf/gf/issues?q=is%3Aissue+is%3Aclosed)
![Stars](https://img.shields.io/github/stars/gogf/gf?style=flat)
![Forks](https://img.shields.io/github/forks/gogf/gf?style=flat)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/gogf/gf)
</div>
@ -35,7 +36,7 @@ go get -u github.com/gogf/gf/v2
- Official Site: [https://goframe.org](https://goframe.org)
- Official Site(en): [https://goframe.org/en](https://goframe.org/en)
- 国内镜像: [https://goframe.org.cn](https://goframe.org.cn)
- Mirror Site: [Github Pages](https://pages.goframe.org)
- Mirror Site: [https://pages.goframe.org](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)
@ -45,7 +46,7 @@ go get -u github.com/gogf/gf/v2
💖 [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.8" alt="goframe contributors"/>
<img src="https://goframe.org/img/contributors.svg?version=v2.10.0" alt="goframe contributors"/>
</a>
## License

View File

@ -1,7 +1,7 @@
[English](README.MD) | 简体中文
<div align=center>
<img src="https://goframe.org/img/logo_full.png" width="300" alt="goframe gf logo"/>
<img src="https://goframe.org/img/logo_full.png" width="300" alt="goframe logo"/>
[![Go Reference](https://pkg.go.dev/badge/github.com/gogf/gf/v2.svg)](https://pkg.go.dev/github.com/gogf/gf/v2)
[![GoFrame CI](https://github.com/gogf/gf/actions/workflows/ci-main.yml/badge.svg)](https://github.com/gogf/gf/actions/workflows/ci-main.yml)
@ -19,10 +19,11 @@
[![GitHub closed issues](https://img.shields.io/github/issues-closed/gogf/gf?style=flat)](https://github.com/gogf/gf/issues?q=is%3Aissue+is%3Aclosed)
![Stars](https://img.shields.io/github/stars/gogf/gf?style=flat)
![Forks](https://img.shields.io/github/forks/gogf/gf?style=flat)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/gogf/gf)
</div>
强大的框架,为了更快、更轻松、更高效的项目开发。
强大的框架,为了更快、更轻松、更高效的项目开发。
## 安装
@ -35,7 +36,7 @@ go get -u github.com/gogf/gf/v2
- 官方网站: [https://goframe.org](https://goframe.org)
- 官方网站(en): [https://goframe.org/en](https://goframe.org/en)
- 国内镜像: [https://goframe.org.cn](https://goframe.org.cn)
- 镜像网站: [Github Pages](https://pages.goframe.org)
- 镜像网站: [https://pages.goframe.org](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)
- 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)
@ -45,9 +46,9 @@ go get -u github.com/gogf/gf/v2
💖 [感谢所有使 GoFrame 成为可能的贡献者](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.5" alt="goframe contributors"/>
<img src="https://goframe.org/img/contributors.svg?version=v2.10.0" alt="goframe contributors"/>
</a>
## 许可证
`GoFrame` 采用 [MIT License](LICENSE) 许可100% 免费和开源,永久保持
`GoFrame` 采用 [MIT License](LICENSE) 许可100%开源和免费

View File

@ -22,7 +22,6 @@ import (
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/cmd/gf/v2/internal/cmd"
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/cmddep"
"github.com/gogf/gf/cmd/gf/v2/internal/utility/allyes"
"github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
)
@ -90,7 +89,6 @@ func GetCommand(ctx context.Context) (*Command, error) {
cmd.Install,
cmd.Version,
cmd.Doc,
cmddep.Dep,
)
if err != nil {
return nil, err

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.8
github.com/gogf/gf/contrib/drivers/mssql/v2 v2.9.8
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.8
github.com/gogf/gf/contrib/drivers/oracle/v2 v2.9.8
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.9.8
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.9.8
github.com/gogf/gf/v2 v2.9.8
github.com/gogf/gf/contrib/drivers/clickhouse/v2 v2.10.0
github.com/gogf/gf/contrib/drivers/mssql/v2 v2.10.0
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.10.0
github.com/gogf/gf/contrib/drivers/oracle/v2 v2.10.0
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.0
github.com/gogf/gf/v2 v2.10.0
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,20 @@ 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.8 h1:L72OB2HPuZSHtJ2ipBzI+62rGGDRdwYjequ1v+zctpg=
github.com/gogf/gf/contrib/drivers/clickhouse/v2 v2.9.8/go.mod h1:D0UySg70Bd264F5AScYmz1Hl8vjzlUJ7YvqBJc5OFbo=
github.com/gogf/gf/contrib/drivers/mssql/v2 v2.9.8 h1:DT5zHfo9/VkbJ+TF7kUasvv4dbU5uctoj+JGbrzgdYE=
github.com/gogf/gf/contrib/drivers/mssql/v2 v2.9.8/go.mod h1:cDd91Zd8LxFF+xxOflRRqw0WTTCpAJ0nf0KKRA+nvTE=
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.8 h1:XZ4Ya/50xpjf81+4genr33iJXR2dxJmqYKxGyXlLRqA=
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.8/go.mod h1:wtm2NJb/L3CbDOmyUc7TsOpWHTCMakg1QRG7B/oKrRs=
github.com/gogf/gf/contrib/drivers/oracle/v2 v2.9.8 h1:ZrqABJsUnhNDz8VAem1XXONBTywl6r+GHQH05i+4W1g=
github.com/gogf/gf/contrib/drivers/oracle/v2 v2.9.8/go.mod h1:YTFyeVk2Rgu/JMUhFxkjYzWaBc+yZ6wAvY54XVZoNko=
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.9.8 h1:Dc227FD1uf9nNBPFEjMEgIoAJbAgeYeNrOrjviDgPzY=
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.9.8/go.mod h1:o3EpB4Ti3+x/axzRMJg2k7TrLiWZiSTxP0v64LBkk5k=
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.9.8 h1:LHEhzsBfIo8xHvOUuLDQW1q7Qix1vnBabH/iivCRghs=
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.9.8/go.mod h1:SX6dRONaJGafzCoMIrn8CkRM4fIvtmJRt/aYclUHy3Q=
github.com/gogf/gf/v2 v2.9.8 h1:El0HwksTzeRk0DQV4Lh7S9DbsIwKInhHSHGcH7qJumM=
github.com/gogf/gf/v2 v2.9.8/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0=
github.com/gogf/gf/contrib/drivers/clickhouse/v2 v2.10.0 h1:9PTchr92xIJej4tq5c+HOHSU7LGOHr3YfD7tuf23LW4=
github.com/gogf/gf/contrib/drivers/clickhouse/v2 v2.10.0/go.mod h1:eKtLMs9uccxFvmoKOUCRQ/Se3nxhzEZwF0Ir13qbk5g=
github.com/gogf/gf/contrib/drivers/mssql/v2 v2.10.0 h1:mBs6XpNM34IdZPZv4Kv3LA8yhP2UisbONMLfnQVFvKM=
github.com/gogf/gf/contrib/drivers/mssql/v2 v2.10.0/go.mod h1:mChbF9FrmiYMSE2rG3zdxI/oSTwaHsR5KbINAgt3KcY=
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.10.0 h1:UvqxwinkelKxwdwnKUfdy51/ls4RL7MCeJqAZOVAy0I=
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.10.0/go.mod h1:6v7oGBF9wv59WERJIOJxXmLhkUcxwON3tPYW3AZ7wbY=
github.com/gogf/gf/contrib/drivers/oracle/v2 v2.10.0 h1:MvhoMaz8YYj4WJuYzKGDdzJYiieiYiqp0vjoOshfOF4=
github.com/gogf/gf/contrib/drivers/oracle/v2 v2.10.0/go.mod h1:vb2fx33RGhjhOaocOTEFvlEuBSGHss5S0lZ4sS3XK6E=
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0 h1:39+jbTenm7KBj4hO2C8ANAxVHpX/7OuRDs1VcGC9ylA=
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0/go.mod h1:B0s0fVzn0W220E8UTpSGzrrGKsop5KcB90twBeLCiz0=
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.0 h1:OyAH7Ls2c9Un7CJiAq7G6eY1jWIICRkN8C5SyM94rnY=
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.0/go.mod h1:fwhAMG0qZpeHbbP2JE78rJRfV7eBbu9jXkxTMM1lwyo=
github.com/gogf/gf/v2 v2.10.0 h1:rzDROlyqGMe/eM6dCalSR8dZOuMIdLhmxKSH1DGhbFs=
github.com/gogf/gf/v2 v2.10.0/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

@ -238,3 +238,48 @@ func Test_Gen_Service_PackagesFilter(t *testing.T) {
t.Assert(files[0], dstFolder+filepath.FromSlash("/user.go"))
})
}
// https://github.com/gogf/gf/issues/4242
// Test that versioned imports and aliased imports are correctly preserved.
// The issue is that imports like "github.com/minio/minio-go/v7" were being
// incorrectly handled because the package name (minio) differs from
// the directory name (minio-go).
func Test_Issue4242(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
path = gfile.Temp(guid.S())
dstFolder = path + filepath.FromSlash("/service")
srvFolder = gtest.DataPath("issue", "4242", "logic")
in = genservice.CGenServiceInput{
SrcFolder: srvFolder,
DstFolder: dstFolder,
DstFileNameCase: "Snake",
WatchFile: "",
StPattern: "",
Packages: nil,
ImportPrefix: "",
Clear: false,
}
)
err := gutil.FillStructWithDefault(&in)
t.AssertNil(err)
err = gfile.Mkdir(path)
t.AssertNil(err)
defer gfile.Remove(path)
_, err = genservice.CGenService{}.Service(ctx, in)
t.AssertNil(err)
// Test versioned imports
t.Assert(
gfile.GetContents(dstFolder+filepath.FromSlash("/issue_4242.go")),
gfile.GetContents(gtest.DataPath("issue", "4242", "service", "issue_4242.go")),
)
// Test aliased imports
t.Assert(
gfile.GetContents(dstFolder+filepath.FromSlash("/issue_4242_alias.go")),
gfile.GetContents(gtest.DataPath("issue", "4242", "service", "issue_4242_alias.go")),
)
})
}

View File

@ -1,140 +0,0 @@
// 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 cmddep
import (
"context"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gtag"
"github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
)
var (
Dep = cDep{}
)
type cDep struct {
g.Meta `name:"dep" brief:"{cDepBrief}" eg:"{cDepEg}"`
}
const (
cDepBrief = `analyze and display Go package dependencies`
cDepEg = `
gf dep
gf dep ./...
gf dep ./internal/...
gf dep -f list
gf dep -f mermaid
gf dep -f mermaid -g
gf dep -f dot -d 5
gf dep -f json -d 0
gf dep -g
gf dep -r
gf dep -i=false
gf dep -e
gf dep -e -i=false
gf dep -M
gf dep -M -D
gf dep -s
gf dep -s -p 8080
gf dep ./internal/... -f tree -d 2
gf dep --external --group -f mermaid
gf dep --module --direct -f json
`
)
func init() {
gtag.Sets(g.MapStrStr{
`cDepBrief`: cDepBrief,
`cDepEg`: cDepEg,
})
}
// Input defines the input parameters for dep command.
type Input struct {
g.Meta `name:"dep"`
Package string `name:"PACKAGE" arg:"true" brief:"package path to analyze, default is ./..." d:"./..."`
Format string `name:"format" short:"f" brief:"output format: tree/list/mermaid/dot/json" d:"tree"`
Depth int `name:"depth" short:"d" brief:"dependency depth limit, 0 means unlimited" d:"3"`
Group bool `name:"group" short:"g" brief:"group by top-level directory" d:"false" orphan:"true"`
Internal bool `name:"internal" short:"i" brief:"show only internal packages" d:"true" orphan:"true"`
External bool `name:"external" short:"e" brief:"show external packages" d:"false" orphan:"true"`
Module bool `name:"module" short:"M" brief:"show module-level dependencies (from go.mod)" d:"false" orphan:"true"`
Direct bool `name:"direct" short:"D" brief:"show only direct dependencies (requires --module)" d:"false" orphan:"true"`
NoStd bool `name:"nostd" short:"n" brief:"exclude standard library" d:"true" orphan:"true"`
Reverse bool `name:"reverse" short:"r" brief:"show reverse dependencies" d:"false" orphan:"true"`
Serve bool `name:"serve" short:"s" brief:"start HTTP server to view dependencies" d:"false" orphan:"true"`
Port int `name:"port" short:"p" brief:"HTTP server port" d:"8888"`
}
// Output defines the output for dep command.
type Output struct{}
// Index is the main entry point for the dep command.
func (c cDep) Index(ctx context.Context, in Input) (out *Output, err error) {
analyzer := newAnalyzer()
// Detect module prefix from go.mod
analyzer.modulePrefix = analyzer.detectModulePrefix()
// Module-level analysis mode (uses go mod graph)
if in.Module {
if in.Serve {
// Load module graph for server mode
if err := analyzer.loadModuleGraph(ctx); err != nil {
mlog.Print("Warning: Failed to load module graph: " + err.Error())
}
return nil, analyzer.startServer(in)
}
// Load module graph
if err := analyzer.loadModuleGraph(ctx); err != nil {
return nil, err
}
// Generate module-level output
output := analyzer.generateModuleOutput(in)
mlog.Print(output)
return
}
// Package-level analysis mode (original behavior)
loadErr := analyzer.loadPackages(ctx, in.Package)
// Start HTTP server if requested
// In server mode, allow starting even without local Go module
// because users may want to analyze remote modules
if in.Serve {
if loadErr != nil {
mlog.Print("Warning: No local Go module found, you can analyze remote modules in the web UI")
}
return nil, analyzer.startServer(in)
}
// For non-server mode, return error if loading failed
if loadErr != nil {
return nil, loadErr
}
if len(analyzer.packages) == 0 {
mlog.Print("No packages found")
return
}
// Generate output based on format
var output string
if in.Reverse {
output = analyzer.generateReverse(in)
} else {
output = analyzer.generate(in)
}
mlog.Print(output)
return
}

File diff suppressed because it is too large Load Diff

View File

@ -1,107 +0,0 @@
// 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 cmddep
import (
"testing"
"github.com/gogf/gf/v2/test/gtest"
)
func TestExternalDependencyAnalysis(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
analyzer := newAnalyzer()
analyzer.modulePrefix = "github.com/gogf/gf/cmd/gf/v2"
analyzer.packages = map[string]*goPackage{
"github.com/other/package": {
ImportPath: "github.com/other/package",
Standard: false,
},
"github.com/gogf/gf/cmd/gf/v2/internal": {
ImportPath: "github.com/gogf/gf/cmd/gf/v2/internal",
Standard: false,
},
"fmt": {
ImportPath: "fmt",
Standard: true,
},
}
// Test using new FilterOptions system
in := Input{
Internal: false,
External: true,
NoStd: true,
}
opts := analyzer.convertInputToFilterOptions(in)
opts.Normalize(analyzer.modulePrefix)
store := analyzer.buildPackageStore()
// Test external package (should be included)
externalPkg, ok := store.packages["github.com/other/package"]
t.Assert(ok, true)
t.Assert(opts.ShouldInclude(externalPkg), true)
// Test internal package (should not be included)
internalPkg, ok := store.packages["github.com/gogf/gf/cmd/gf/v2/internal"]
t.Assert(ok, true)
t.Assert(opts.ShouldInclude(internalPkg), false)
// Test standard library (should not be included due to NoStd)
stdPkg, ok := store.packages["fmt"]
t.Assert(ok, true)
t.Assert(opts.ShouldInclude(stdPkg), false)
})
}
func TestExternalGrouping(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
analyzer := newAnalyzer()
// Test external group extraction using shortName
t.Assert(analyzer.shortName("github.com/user/repo", false), "github.com/user/repo")
t.Assert(analyzer.shortName("golang.org/x/tools", false), "golang.org/x/tools")
t.Assert(analyzer.shortName("fmt", false), "fmt")
t.Assert(analyzer.shortName("simple", false), "simple")
})
}
func TestDependencyStats(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
analyzer := newAnalyzer()
analyzer.modulePrefix = "github.com/gogf/gf/cmd/gf/v2"
// Add test packages
analyzer.packages = map[string]*goPackage{
"github.com/gogf/gf/cmd/gf/v2/internal": {
ImportPath: "github.com/gogf/gf/cmd/gf/v2/internal",
Standard: false,
},
"github.com/external/package": {
ImportPath: "github.com/external/package",
Standard: false,
},
"fmt": {
ImportPath: "fmt",
Standard: true,
},
}
in := Input{
Internal: true,
External: true,
NoStd: false,
}
stats := analyzer.getDependencyStats(in)
t.Assert(stats["total"], 3)
t.Assert(stats["internal"], 1)
t.Assert(stats["external"], 1)
t.Assert(stats["stdlib"], 1)
})
}

View File

@ -1,403 +0,0 @@
// 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 cmddep
import (
"encoding/json"
"fmt"
"sort"
"strings"
)
// generate creates output based on format.
func (a *analyzer) generate(in Input) string {
switch in.Format {
case "tree":
return a.generateTree(in)
case "list":
return a.generateList(in)
case "mermaid":
return a.generateMermaid(in)
case "dot":
return a.generateDot(in)
case "json":
return a.generateJSON(in)
default:
// Default to tree format
return a.generateTree(in)
}
}
// generateTree generates ASCII tree output using new traversal system.
func (a *analyzer) generateTree(in Input) string {
var sb strings.Builder
// Prepare options
opts := a.convertInputToFilterOptions(in)
opts.Normalize(a.modulePrefix)
// Add statistics header if showing external dependencies
if in.External {
stats := a.getDependencyStats(in)
sb.WriteString("Dependency Statistics:\n")
fmt.Fprintf(&sb, " Total packages: %v\n", stats["total"])
fmt.Fprintf(&sb, " Internal: %v\n", stats["internal"])
fmt.Fprintf(&sb, " External: %v\n", stats["external"])
fmt.Fprintf(&sb, " Standard library: %v\n", stats["stdlib"])
if groups, ok := stats["external_groups"].(map[string]int); ok && len(groups) > 0 {
sb.WriteString(" External groups:\n")
for group, count := range groups {
fmt.Fprintf(&sb, " %s: %d\n", group, count)
}
}
sb.WriteString("\nDependency Tree:\n")
}
// Build package store
store := a.buildPackageStore()
// Find root packages (packages that are not imported by any other package)
rootPkgs := a.findRootPackages()
// Create traversal context
ctx := &TraversalContext{
visited: make(map[string]bool),
options: opts,
store: store,
maxDepth: opts.Depth,
}
for _, pkgPath := range rootPkgs {
pkgInfo, ok := store.packages[pkgPath]
if !ok || !opts.ShouldInclude(pkgInfo) {
continue
}
shortName := a.shortName(pkgPath, in.Group)
sb.WriteString(shortName + "\n")
// Use new traversal system
a.printTreeNodeNew(&sb, pkgPath, "", in, ctx, 0)
}
return sb.String()
}
// findRootPackages finds packages that are not imported by any other internal package.
func (a *analyzer) findRootPackages() []string {
// Build a set of all imported packages
imported := make(map[string]bool)
for _, pkg := range a.packages {
for _, dep := range pkg.Imports {
imported[dep] = true
}
}
// Find packages that are not imported by others
roots := make([]string, 0)
for pkgPath := range a.packages {
if !imported[pkgPath] {
roots = append(roots, pkgPath)
}
}
// If no roots found (circular dependencies), use all packages
if len(roots) == 0 {
roots = a.getSortedPackages()
}
sort.Strings(roots)
return roots
}
// printTreeNodeNew prints tree node using new traversal system.
func (a *analyzer) printTreeNodeNew(sb *strings.Builder, pkgPath string, prefix string, in Input, ctx *TraversalContext, depth int) {
if ctx.maxDepth > 0 && depth >= ctx.maxDepth {
return
}
_, ok := ctx.store.packages[pkgPath]
if !ok {
return
}
// Get filtered dependencies
deps := ctx.GetDependencies(pkgPath)
sort.Strings(deps)
for i, dep := range deps {
// Check if already visited
if ctx.Visit(dep) {
continue
}
isLast := i == len(deps)-1
connector := "├── "
if isLast {
connector = "└── "
}
shortName := a.shortName(dep, in.Group)
sb.WriteString(prefix + connector + shortName + "\n")
newPrefix := prefix
if isLast {
newPrefix += " "
} else {
newPrefix += "│ "
}
// Recursively print dependencies
ctx.depth++
a.printTreeNodeNew(sb, dep, newPrefix, in, ctx, depth+1)
ctx.depth--
}
}
// generateList generates simple list output using new traversal system.
func (a *analyzer) generateList(in Input) string {
var sb strings.Builder
// Prepare options
opts := a.convertInputToFilterOptions(in)
opts.Normalize(a.modulePrefix)
// Add statistics header if showing external dependencies
if in.External {
stats := a.getDependencyStats(in)
sb.WriteString("# Dependency Statistics\n")
fmt.Fprintf(&sb, "# Total: %v, Internal: %v, External: %v, Stdlib: %v\n",
stats["total"], stats["internal"], stats["external"], stats["stdlib"])
sb.WriteString("\n")
}
// Build package store
store := a.buildPackageStore()
allDeps := make(map[string]bool)
// Collect dependencies from packages that should be included
for pkgPath, pkgInfo := range store.packages {
if !opts.ShouldInclude(pkgInfo) {
continue
}
// Get filtered dependencies for this package
for _, dep := range store.packages[pkgPath].Imports {
depInfo, ok := store.packages[dep]
if ok && opts.ShouldInclude(depInfo) {
allDeps[dep] = true
}
}
}
deps := make([]string, 0, len(allDeps))
for dep := range allDeps {
deps = append(deps, a.shortName(dep, in.Group))
}
sort.Strings(deps)
for _, dep := range deps {
sb.WriteString(dep + "\n")
}
return sb.String()
}
// generateMermaid generates Mermaid diagram output.
func (a *analyzer) generateMermaid(in Input) string {
var sb strings.Builder
sb.WriteString("```mermaid\n")
sb.WriteString("graph TD\n")
edges := a.collectEdges(in)
sortedEdges := make([]string, 0, len(edges))
for edge := range edges {
sortedEdges = append(sortedEdges, edge)
}
sort.Strings(sortedEdges)
for _, edge := range sortedEdges {
sb.WriteString(" " + edge + "\n")
}
sb.WriteString("```\n")
return sb.String()
}
// generateMermaidRaw generates Mermaid code without markdown wrapper.
func (a *analyzer) generateMermaidRaw(in Input) string {
var sb strings.Builder
sb.WriteString("graph TD\n")
edges := a.collectEdges(in)
sortedEdges := make([]string, 0, len(edges))
for edge := range edges {
sortedEdges = append(sortedEdges, edge)
}
sort.Strings(sortedEdges)
for _, edge := range sortedEdges {
sb.WriteString(" " + edge + "\n")
}
return sb.String()
}
// generateDot generates Graphviz DOT output.
func (a *analyzer) generateDot(in Input) string {
var sb strings.Builder
sb.WriteString("digraph deps {\n")
sb.WriteString(" rankdir=TB;\n")
sb.WriteString(" node [shape=box];\n")
edges := a.collectEdges(in)
sortedEdges := make([]string, 0, len(edges))
for edge := range edges {
sortedEdges = append(sortedEdges, edge)
}
sort.Strings(sortedEdges)
for _, edge := range sortedEdges {
parts := strings.Split(edge, " --> ")
if len(parts) == 2 {
fmt.Fprintf(&sb, " \"%s\" -> \"%s\";\n", parts[0], parts[1])
}
}
sb.WriteString("}\n")
return sb.String()
}
// generateJSON generates JSON output using new traversal system.
func (a *analyzer) generateJSON(in Input) string {
opts := a.convertInputToFilterOptions(in)
opts.Normalize(a.modulePrefix)
store := a.buildPackageStore()
result := make(map[string]any)
// Add dependency nodes
nodes := make([]*depNode, 0)
for _, pkgPath := range a.getSortedPackages() {
pkgInfo, ok := store.packages[pkgPath]
if !ok || !opts.ShouldInclude(pkgInfo) {
continue
}
pkg := a.packages[pkgPath]
a.visited = make(map[string]bool)
node := a.buildDepNode(pkg, in, 0)
nodes = append(nodes, node)
}
result["dependencies"] = nodes
// Add statistics
result["statistics"] = a.getDependencyStats(in)
// Add metadata
result["metadata"] = map[string]any{
"module": a.modulePrefix,
"format": in.Format,
"depth": in.Depth,
"group": in.Group,
"internal": in.Internal,
"external": in.External,
"nostd": in.NoStd,
}
data, err := json.MarshalIndent(result, "", " ")
if err != nil {
return fmt.Sprintf("Error: %v", err)
}
return string(data)
}
func (a *analyzer) buildDepNode(pkg *goPackage, in Input, depth int) *depNode {
opts := a.convertInputToFilterOptions(in)
opts.Normalize(a.modulePrefix)
store := a.buildPackageStore()
node := &depNode{
Package: a.shortName(pkg.ImportPath, in.Group),
}
if in.Depth > 0 && depth >= in.Depth {
return node
}
pkgInfo, ok := store.packages[pkg.ImportPath]
if !ok {
return node
}
// Track visited packages to avoid cycles
if !a.visited[pkg.ImportPath] {
a.visited[pkg.ImportPath] = true
for _, dep := range pkgInfo.Imports {
depInfo, ok := store.packages[dep]
if !ok || !opts.ShouldInclude(depInfo) {
continue
}
if depPkg, ok := a.packages[dep]; ok {
childNode := a.buildDepNode(depPkg, in, depth+1)
node.Dependencies = append(node.Dependencies, childNode)
} else {
node.Dependencies = append(node.Dependencies, &depNode{
Package: a.shortName(dep, in.Group),
})
}
}
}
return node
}
// generateReverse generates reverse dependency output using new system.
func (a *analyzer) generateReverse(in Input) string {
opts := a.convertInputToFilterOptions(in)
opts.Normalize(a.modulePrefix)
store := a.buildPackageStore()
// Build reverse dependency map
reverseDeps := make(map[string][]string)
for pkgPath, pkgInfo := range store.packages {
for _, dep := range pkgInfo.Imports {
depInfo, ok := store.packages[dep]
if ok && opts.ShouldInclude(depInfo) {
reverseDeps[dep] = append(reverseDeps[dep], pkgPath)
}
}
}
var sb strings.Builder
targets := a.getSortedPackages()
for _, target := range targets {
deps := reverseDeps[target]
if len(deps) == 0 {
continue
}
sort.Strings(deps)
shortTarget := a.shortName(target, in.Group)
if shortTarget == "" {
continue
}
fmt.Fprintf(&sb, "%s (used by %d packages):\n", shortTarget, len(deps))
for i, dep := range deps {
isLast := i == len(deps)-1
connector := "├── "
if isLast {
connector = "└── "
}
sb.WriteString(connector + a.shortName(dep, in.Group) + "\n")
}
sb.WriteString("\n")
}
return sb.String()
}

File diff suppressed because it is too large Load Diff

View File

@ -1,255 +0,0 @@
// 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 cmddep
import (
"testing"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/test/gtest"
)
// Test data model creation and classification
func Test_PackageInfo_Creation(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
pkg := &PackageInfo{
ImportPath: "github.com/gogf/gf/v2/os/gfile",
ModulePath: "github.com/gogf/gf/v2",
Kind: KindInternal,
Tier: 2,
Imports: []string{"fmt", "os"},
IsStdLib: false,
IsModuleRoot: false,
}
t.Assert(pkg != nil, true)
t.Assert(pkg.Kind, KindInternal)
t.Assert(pkg.Tier, 2)
t.Assert(len(pkg.Imports), 2)
})
}
// Test FilterOptions normalization
func Test_FilterOptions_Normalize(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
opts := &FilterOptions{
IncludeInternal: false,
IncludeExternal: false,
IncludeStdLib: false,
}
opts.Normalize("github.com/gogf/gf/v2")
// After normalization, internal should be included by default
t.Assert(opts.IncludeInternal, true)
t.Assert(opts.IncludeExternal, false)
})
}
// Test ShouldInclude decision logic
func Test_FilterOptions_ShouldInclude(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Test internal package inclusion
opts := &FilterOptions{
IncludeInternal: true,
IncludeExternal: false,
IncludeStdLib: true,
}
internalPkg := &PackageInfo{
Kind: KindInternal,
IsStdLib: false,
}
externalPkg := &PackageInfo{
Kind: KindExternal,
IsStdLib: false,
}
stdlibPkg := &PackageInfo{
Kind: KindStdLib,
IsStdLib: true,
}
t.Assert(opts.ShouldInclude(internalPkg), true)
t.Assert(opts.ShouldInclude(externalPkg), false)
t.Assert(opts.ShouldInclude(stdlibPkg), true)
})
}
// Test TraversalContext Visit tracking
func Test_TraversalContext_Visit(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
ctx := &TraversalContext{
visited: make(map[string]bool),
}
// First visit should return false
t.Assert(ctx.Visit("pkg1"), false)
// Second visit should return true
t.Assert(ctx.Visit("pkg1"), true)
// New package should return false
t.Assert(ctx.Visit("pkg2"), false)
})
}
// Test guessModuleRoot function
func Test_GuessModuleRoot(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
tests := []struct {
input string
expect string
}{
{"github.com/gogf/gf", "github.com/gogf/gf"},
{"github.com/gogf/gf/v2", "github.com/gogf/gf/v2"},
{"github.com/gogf/gf/v2/os/gfile", "github.com/gogf/gf/v2"},
{"github.com/gogf/gf/v2/os", "github.com/gogf/gf/v2"},
}
for _, test := range tests {
result := guessModuleRoot(test.input)
t.AssertEQ(result, test.expect)
}
})
}
// Test input to filter options conversion
func Test_ConvertInputToFilterOptions(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
a := newAnalyzer()
input := Input{
Internal: true,
External: false,
NoStd: true,
Module: false,
Direct: false,
Depth: 3,
}
opts := a.convertInputToFilterOptions(input)
t.Assert(opts.IncludeInternal, true)
t.Assert(opts.IncludeExternal, false)
t.Assert(opts.IncludeStdLib, false) // NoStd=true means !IncludeStdLib
t.Assert(opts.Depth, 3)
})
}
func Test_Dep_Tree(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
ctx := gctx.New()
_, err := Dep.Index(ctx, Input{
Package: "./",
Format: "tree",
Depth: 1,
Internal: true,
NoStd: true,
})
t.AssertNil(err)
})
}
func Test_Dep_List(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
ctx := gctx.New()
_, err := Dep.Index(ctx, Input{
Package: "./",
Format: "list",
Depth: 1,
Internal: true,
NoStd: true,
})
t.AssertNil(err)
})
}
func Test_Dep_Mermaid(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
ctx := gctx.New()
_, err := Dep.Index(ctx, Input{
Package: "./",
Format: "mermaid",
Depth: 1,
Internal: true,
NoStd: true,
})
t.AssertNil(err)
})
}
func Test_Dep_Dot(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
ctx := gctx.New()
_, err := Dep.Index(ctx, Input{
Package: "./",
Format: "dot",
Depth: 1,
Internal: true,
NoStd: true,
})
t.AssertNil(err)
})
}
func Test_Dep_JSON(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
ctx := gctx.New()
_, err := Dep.Index(ctx, Input{
Package: "./",
Format: "json",
Depth: 1,
Internal: true,
NoStd: true,
})
t.AssertNil(err)
})
}
func Test_Dep_Reverse(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
ctx := gctx.New()
_, err := Dep.Index(ctx, Input{
Package: "./",
Format: "tree",
Depth: 1,
Internal: true,
NoStd: true,
Reverse: true,
})
t.AssertNil(err)
})
}
func Test_Dep_Group(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
ctx := gctx.New()
_, err := Dep.Index(ctx, Input{
Package: "./",
Format: "mermaid",
Depth: 1,
Internal: true,
NoStd: true,
Group: true,
})
t.AssertNil(err)
})
}
func Test_ModuleLevel_Direct(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
ctx := gctx.New()
// Test module level with direct only
in := Input{
Module: true,
Direct: true,
Format: "list",
}
t.Logf("Input.Module: %v, Input.Direct: %v", in.Module, in.Direct)
_, err := Dep.Index(ctx, in)
t.AssertNil(err)
})
}

View File

@ -1,946 +0,0 @@
// Main Application Module
let currentZoom = 1;
let currentView = 'graph';
let currentLayout = 'TD';
let selectedPackage = null;
let allPackages = [];
let isRemoteMode = false;
let currentRemoteModule = '';
let packageListMode = 'tree'; // 'flat' or 'tree'
let expandedNodes = new Set(); // Track expanded tree nodes
// Pan and Zoom state
let panX = 0;
let panY = 0;
let isPanning = false;
let startPanX = 0;
let startPanY = 0;
let zoomIndicatorTimeout = null;
const MIN_ZOOM = 0.1;
const MAX_ZOOM = 10;
const ZOOM_STEP = 0.1;
// Theme Management
const theme = {
current: 'light',
init() {
const savedTheme = localStorage.getItem('dep-viewer-theme');
if (savedTheme) {
this.current = savedTheme;
} else {
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
this.current = 'dark';
}
}
this.apply();
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (!localStorage.getItem('dep-viewer-theme')) {
this.current = e.matches ? 'dark' : 'light';
this.apply();
}
});
const toggleBtn = document.getElementById('themeToggle');
if (toggleBtn) {
toggleBtn.addEventListener('click', () => this.toggle());
}
},
toggle() {
this.current = this.current === 'dark' ? 'light' : 'dark';
localStorage.setItem('dep-viewer-theme', this.current);
this.apply();
},
apply() {
if (this.current === 'dark') {
document.body.setAttribute('data-theme', 'dark');
} else {
document.body.removeAttribute('data-theme');
}
mermaid.initialize({
startOnLoad: false,
theme: this.current === 'dark' ? 'dark' : 'default',
maxEdges: 2000, // Increase edge limit for large dependency graphs
flowchart: {
useMaxWidth: false,
htmlLabels: true,
curve: 'basis'
}
});
if (currentView === 'graph') {
refresh();
}
}
};
// Initialize mermaid
mermaid.initialize({
startOnLoad: false,
theme: 'default',
maxEdges: 2000, // Increase edge limit for large dependency graphs
flowchart: {
useMaxWidth: false,
htmlLabels: true,
curve: 'basis'
}
});
// Initialize application
async function init() {
theme.init();
initPanZoom();
initRemoteModuleInput();
const hasLocalModule = await loadModuleName();
if (hasLocalModule) {
await loadPackages();
await refresh();
}
}
// Initialize remote module input
function initRemoteModuleInput() {
const input = document.getElementById('remoteModuleInput');
if (input) {
// Fetch versions when input loses focus or Enter is pressed
input.addEventListener('blur', fetchVersions);
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
fetchVersions();
}
});
}
}
// Fetch versions for a module from Go proxy
async function fetchVersions() {
const input = document.getElementById('remoteModuleInput');
const versionSelect = document.getElementById('versionSelect');
const spinner = document.getElementById('loadingSpinner');
let modulePath = input.value.trim();
// Remove http:// or https:// prefix if present
modulePath = modulePath.replace(/^https?:\/\//, '');
input.value = modulePath;
if (!modulePath) {
versionSelect.disabled = true;
versionSelect.innerHTML = `<option value="">${i18n.t('selectVersion')}</option>`;
return;
}
spinner.classList.remove('hidden');
versionSelect.disabled = true;
try {
const response = await fetch('/api/versions?module=' + encodeURIComponent(modulePath));
const data = await response.json();
if (data.error) {
versionSelect.innerHTML = `<option value="">${i18n.t('errorFetchVersions')}</option>`;
} else if (data.versions && data.versions.length > 0) {
versionSelect.innerHTML = data.versions.map((v, i) => {
const label = i === 0 ? `${v} (${i18n.t('latestVersion')})` : v;
return `<option value="${v}">${label}</option>`;
}).join('');
versionSelect.disabled = false;
} else {
versionSelect.innerHTML = `<option value="">${i18n.t('noVersions')}</option>`;
}
} catch (e) {
console.error('Failed to fetch versions:', e);
versionSelect.innerHTML = `<option value="">${i18n.t('errorFetchVersions')}</option>`;
} finally {
spinner.classList.add('hidden');
}
}
// Analyze remote module
async function analyzeRemoteModule() {
const input = document.getElementById('remoteModuleInput');
const versionSelect = document.getElementById('versionSelect');
const spinner = document.getElementById('loadingSpinner');
const analyzeBtn = document.getElementById('analyzeBtn');
let modulePath = input.value.trim();
// Remove http:// or https:// prefix if present
modulePath = modulePath.replace(/^https?:\/\//, '');
input.value = modulePath;
const version = versionSelect.value;
if (!modulePath) {
alert('Please enter a module path');
return;
}
spinner.classList.remove('hidden');
analyzeBtn.disabled = true;
analyzeBtn.textContent = i18n.t('analyzing');
try {
const url = `/api/analyze?module=${encodeURIComponent(modulePath)}${version ? '&version=' + encodeURIComponent(version) : ''}`;
const response = await fetch(url);
const data = await response.json();
if (data.error) {
alert(data.error);
return;
}
// Switch to remote mode
isRemoteMode = true;
currentRemoteModule = modulePath + (version ? '@' + version : '');
document.getElementById('moduleName').textContent = currentRemoteModule;
// Clear selection and reload
selectedPackage = null;
await loadPackages();
await refresh();
} catch (e) {
console.error('Failed to analyze module:', e);
alert(i18n.t('errorAnalyze'));
} finally {
spinner.classList.add('hidden');
analyzeBtn.disabled = false;
analyzeBtn.textContent = i18n.t('analyze');
}
}
// Reset to local module
async function resetToLocal() {
const spinner = document.getElementById('loadingSpinner');
spinner.classList.remove('hidden');
try {
await fetch('/api/reset');
isRemoteMode = false;
currentRemoteModule = '';
document.getElementById('remoteModuleInput').value = '';
document.getElementById('versionSelect').innerHTML = `<option value="">${i18n.t('selectVersion')}</option>`;
document.getElementById('versionSelect').disabled = true;
selectedPackage = null;
await loadModuleName();
await loadPackages();
await refresh();
} catch (e) {
console.error('Failed to reset:', e);
} finally {
spinner.classList.add('hidden');
}
}
// Initialize pan and zoom functionality
function initPanZoom() {
const viewport = document.getElementById('graphView');
if (!viewport) return;
// Mouse wheel zoom
viewport.addEventListener('wheel', (e) => {
if (currentView !== 'graph') return;
e.preventDefault();
const rect = viewport.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
// Calculate zoom
const delta = e.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP;
const newZoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, currentZoom + delta * currentZoom));
if (newZoom !== currentZoom) {
// Zoom towards mouse position
const scale = newZoom / currentZoom;
panX = mouseX - (mouseX - panX) * scale;
panY = mouseY - (mouseY - panY) * scale;
currentZoom = newZoom;
applyTransform();
showZoomIndicator();
}
}, { passive: false });
// Pan with mouse drag
viewport.addEventListener('mousedown', (e) => {
if (currentView !== 'graph') return;
if (e.button !== 0) return; // Only left click
isPanning = true;
startPanX = e.clientX - panX;
startPanY = e.clientY - panY;
viewport.style.cursor = 'grabbing';
});
document.addEventListener('mousemove', (e) => {
if (!isPanning) return;
panX = e.clientX - startPanX;
panY = e.clientY - startPanY;
applyTransform();
});
document.addEventListener('mouseup', () => {
if (isPanning) {
isPanning = false;
const viewport = document.getElementById('graphViewport');
if (viewport) viewport.style.cursor = 'grab';
}
});
// Touch support for mobile
let lastTouchDistance = 0;
let lastTouchCenter = { x: 0, y: 0 };
viewport.addEventListener('touchstart', (e) => {
if (currentView !== 'graph') return;
if (e.touches.length === 1) {
isPanning = true;
startPanX = e.touches[0].clientX - panX;
startPanY = e.touches[0].clientY - panY;
} else if (e.touches.length === 2) {
isPanning = false;
lastTouchDistance = getTouchDistance(e.touches);
lastTouchCenter = getTouchCenter(e.touches);
}
}, { passive: true });
viewport.addEventListener('touchmove', (e) => {
if (currentView !== 'graph') return;
e.preventDefault();
if (e.touches.length === 1 && isPanning) {
panX = e.touches[0].clientX - startPanX;
panY = e.touches[0].clientY - startPanY;
applyTransform();
} else if (e.touches.length === 2) {
const distance = getTouchDistance(e.touches);
const center = getTouchCenter(e.touches);
if (lastTouchDistance > 0) {
const scale = distance / lastTouchDistance;
const newZoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, currentZoom * scale));
if (newZoom !== currentZoom) {
const rect = viewport.getBoundingClientRect();
const centerX = center.x - rect.left;
const centerY = center.y - rect.top;
const zoomScale = newZoom / currentZoom;
panX = centerX - (centerX - panX) * zoomScale;
panY = centerY - (centerY - panY) * zoomScale;
currentZoom = newZoom;
applyTransform();
showZoomIndicator();
}
}
lastTouchDistance = distance;
lastTouchCenter = center;
}
}, { passive: false });
viewport.addEventListener('touchend', () => {
isPanning = false;
lastTouchDistance = 0;
});
}
function getTouchDistance(touches) {
const dx = touches[0].clientX - touches[1].clientX;
const dy = touches[0].clientY - touches[1].clientY;
return Math.sqrt(dx * dx + dy * dy);
}
function getTouchCenter(touches) {
return {
x: (touches[0].clientX + touches[1].clientX) / 2,
y: (touches[0].clientY + touches[1].clientY) / 2
};
}
function applyTransform() {
const container = document.getElementById('mermaidContainer');
if (container) {
container.style.transform = `translate(${panX}px, ${panY}px) scale(${currentZoom})`;
}
}
function showZoomIndicator() {
const indicator = document.getElementById('zoomIndicator');
if (indicator) {
indicator.textContent = `${Math.round(currentZoom * 100)}%`;
indicator.classList.add('visible');
if (zoomIndicatorTimeout) {
clearTimeout(zoomIndicatorTimeout);
}
zoomIndicatorTimeout = setTimeout(() => {
indicator.classList.remove('visible');
}, 1500);
}
}
// Load module name from server
// Returns true if local module exists, false otherwise
async function loadModuleName() {
try {
const response = await fetch('/api/module');
const data = await response.json();
document.getElementById('moduleName').textContent = data.name || '';
// If no local module, set default value and auto-analyze
if (!data.name) {
const input = document.getElementById('remoteModuleInput');
if (input && !input.value) {
input.value = 'github.com/gogf/gf/v2';
// Fetch versions first, then auto-analyze
await fetchVersions();
analyzeRemoteModule();
}
return false;
}
return true;
} catch (e) {
console.error('Failed to load module name:', e);
return false;
}
}
// Load packages list
async function loadPackages() {
try {
const internal = document.getElementById('internal').checked;
const external = document.getElementById('external') ? document.getElementById('external').checked : false;
const moduleLevel = document.getElementById('moduleLevel') ? document.getElementById('moduleLevel').checked : false;
const directOnly = document.getElementById('directOnly') ? document.getElementById('directOnly').checked : false;
const response = await fetch(`/api/packages?internal=${internal}&external=${external}&module=${moduleLevel}&direct=${directOnly}`);
const data = await response.json();
// Handle new API response format with packages and statistics
if (data.packages && Array.isArray(data.packages)) {
allPackages = data.packages;
// Update statistics display if available
if (data.statistics) {
updateStatisticsDisplay(data.statistics);
}
} else if (Array.isArray(data)) {
// Fallback for old format
allPackages = data;
} else {
console.error('Unexpected API response format:', data);
allPackages = [];
}
document.getElementById('packageCount').textContent = allPackages.length;
renderPackageList(allPackages);
} catch (e) {
console.error('Failed to load packages:', e);
}
}
// Update statistics display
function updateStatisticsDisplay(statistics) {
if (statistics) {
document.getElementById('internalCount').textContent = statistics.internal || 0;
document.getElementById('externalCount').textContent = statistics.external || 0;
document.getElementById('stdlibCount').textContent = statistics.stdlib || 0;
// Update total count
const totalCount = statistics.total || (statistics.internal + statistics.external + statistics.stdlib);
document.getElementById('nodeCount').textContent = totalCount;
}
}
// Get package name from package object or string
function getPkgName(pkg) {
return typeof pkg === 'object' ? pkg.name : pkg;
}
// Set package list display mode
function setPackageListMode(mode) {
packageListMode = mode;
document.getElementById('modeFlat').classList.toggle('active', mode === 'flat');
document.getElementById('modeTree').classList.toggle('active', mode === 'tree');
const query = document.getElementById('searchInput').value.toLowerCase();
const filtered = query ? allPackages.filter(pkg => getPkgName(pkg).toLowerCase().includes(query)) : allPackages;
renderPackageList(filtered);
}
// Render package list in sidebar
function renderPackageList(packages) {
const list = document.getElementById('packageList');
if (packages.length === 0) {
list.innerHTML = '<div class="loading">' + i18n.t('noPackages') + '</div>';
return;
}
if (packageListMode === 'tree') {
renderPackageTree(packages, list);
} else {
renderPackageFlat(packages, list);
}
}
// Render flat package list
function renderPackageFlat(packages, container) {
container.innerHTML = packages.map(pkg => {
const name = getPkgName(pkg);
const isActive = name === selectedPackage ? ' active' : '';
const escaped = name.replace(/'/g, "\\'");
// Build stats display
let statsHtml = '';
if (typeof pkg === 'object') {
statsHtml = `<span class="pkg-stats-inline">
<span class="dep-count" title="${i18n.t('dependencies')}">→${pkg.depCount}</span>
<span class="used-count" title="${i18n.t('usedBy')}">←${pkg.usedByCount}</span>
</span>`;
}
return `<div class="package-item${isActive}" onclick="selectPackage('${escaped}')" title="${name}">
<span class="pkg-name-text">${name}</span>${statsHtml}
</div>`;
}).join('');
}
// Build tree structure from package paths
function buildPackageTree(packages) {
const root = { children: {}, packages: [] };
packages.forEach(pkg => {
const name = getPkgName(pkg);
const parts = name.split('/');
let current = root;
parts.forEach((part, index) => {
if (!current.children[part]) {
current.children[part] = { children: {}, packages: [], path: parts.slice(0, index + 1).join('/') };
}
current = current.children[part];
});
current.isPackage = true;
current.fullPath = name;
// Store stats if available
if (typeof pkg === 'object') {
current.depCount = pkg.depCount;
current.usedByCount = pkg.usedByCount;
}
});
return root;
}
// Render package tree
function renderPackageTree(packages, container) {
const tree = buildPackageTree(packages);
container.innerHTML = renderTreeNode(tree, '');
}
// Render a tree node recursively
function renderTreeNode(node, path) {
const children = Object.keys(node.children).sort();
if (children.length === 0) return '';
return children.map(name => {
const child = node.children[name];
const childPath = path ? `${path}/${name}` : name;
const hasChildren = Object.keys(child.children).length > 0;
const isExpanded = expandedNodes.has(childPath);
const isActive = child.fullPath === selectedPackage;
const isPackage = child.isPackage;
const toggleClass = hasChildren ? (isExpanded ? 'expanded' : '') : 'empty';
const activeClass = isActive ? ' active' : '';
const nameClass = isPackage ? ' package' : '';
const icon = isPackage ? '📦' : '📁';
// Build stats for packages
let statsHtml = '';
if (isPackage && child.depCount !== undefined) {
statsHtml = `<span class="pkg-stats-inline">
<span class="dep-count" title="${i18n.t('dependencies')}">→${child.depCount}</span>
<span class="used-count" title="${i18n.t('usedBy')}">←${child.usedByCount}</span>
</span>`;
}
let html = `
<div class="tree-node" data-path="${childPath}">
<div class="tree-node-header${activeClass}">
<span class="tree-node-toggle ${toggleClass}" onclick="handleToggleClick(event, '${childPath.replace(/'/g, "\\'")}', ${hasChildren})">▶</span>
<span class="tree-node-icon">${icon}</span>
<span class="tree-node-name${nameClass}" onclick="handleNameClick(event, '${childPath.replace(/'/g, "\\'")}', ${isPackage}, ${hasChildren})">${name}</span>
${statsHtml}
</div>`;
if (hasChildren) {
const childrenHtml = renderTreeNode(child, childPath);
html += `<div class="tree-node-children${isExpanded ? ' expanded' : ''}">${childrenHtml}</div>`;
}
html += '</div>';
return html;
}).join('');
}
// Handle toggle arrow click - expand/collapse
function handleToggleClick(event, path, hasChildren) {
event.stopPropagation();
if (hasChildren) {
toggleTreeNode(path);
}
}
// Handle name click - select package or toggle if folder
function handleNameClick(event, path, isPackage, hasChildren) {
event.stopPropagation();
if (isPackage) {
selectPackage(path);
} else if (hasChildren) {
toggleTreeNode(path);
}
}
// Toggle tree node expansion
function toggleTreeNode(path) {
if (expandedNodes.has(path)) {
expandedNodes.delete(path);
} else {
expandedNodes.add(path);
}
// Re-render with current filter
const query = document.getElementById('searchInput').value.toLowerCase();
const filtered = query ? allPackages.filter(pkg => getPkgName(pkg).toLowerCase().includes(query)) : allPackages;
renderPackageList(filtered);
}
// Filter packages by search query
function filterPackages() {
const query = document.getElementById('searchInput').value.toLowerCase();
const filtered = allPackages.filter(pkg => getPkgName(pkg).toLowerCase().includes(query));
renderPackageList(filtered);
}
// Select a package
async function selectPackage(pkg) {
selectedPackage = pkg;
// Update flat list items
document.querySelectorAll('.package-item').forEach(el => {
el.classList.toggle('active', el.textContent === pkg);
});
// Update tree items
document.querySelectorAll('.tree-node-header').forEach(el => {
const node = el.closest('.tree-node');
el.classList.toggle('active', node && node.dataset.path === pkg);
});
await refresh();
}
// Clear package selection
function clearSelection() {
selectedPackage = null;
document.querySelectorAll('.package-item').forEach(el => el.classList.remove('active'));
document.querySelectorAll('.tree-node-header').forEach(el => el.classList.remove('active'));
closePackageInfo();
refresh();
}
// Close package info sidebar
function closePackageInfo() {
document.getElementById('packageInfo').classList.remove('visible');
}
// Set layout direction
function setLayout(layout) {
currentLayout = layout;
document.getElementById('layoutTD').classList.toggle('active', layout === 'TD');
document.getElementById('layoutLR').classList.toggle('active', layout === 'LR');
refresh();
}
// Set view mode
function setView(view) {
currentView = view;
document.getElementById('btnGraph').classList.toggle('active', view === 'graph');
document.getElementById('btnTree').classList.toggle('active', view === 'tree');
document.getElementById('btnList').classList.toggle('active', view === 'list');
document.getElementById('zoomControls').classList.toggle('hidden', view !== 'graph');
document.getElementById('layoutGroup').classList.toggle('hidden', view !== 'graph');
refresh();
}
// Main refresh function
async function refresh() {
const depth = document.getElementById('depth').value;
const group = document.getElementById('group').checked;
const reverse = document.getElementById('reverse').checked;
const internal = document.getElementById('internal').checked;
const external = document.getElementById('external') ? document.getElementById('external').checked : false;
const moduleLevel = document.getElementById('moduleLevel') ? document.getElementById('moduleLevel').checked : false;
const directOnly = document.getElementById('directOnly') ? document.getElementById('directOnly').checked : false;
if (selectedPackage) {
await showPackageInfo(selectedPackage);
} else {
closePackageInfo();
}
if (currentView === 'graph') {
await refreshGraph(depth, group, reverse, internal, external, moduleLevel, directOnly);
} else if (currentView === 'tree') {
await refreshTree(depth, internal, external, moduleLevel, directOnly);
} else {
await refreshList(internal, external, moduleLevel, directOnly);
}
}
// Show package info panel
async function showPackageInfo(pkg) {
try {
const response = await fetch('/api/package?name=' + encodeURIComponent(pkg));
const info = await response.json();
const infoDiv = document.getElementById('packageInfo');
const contentDiv = document.getElementById('packageInfoContent');
infoDiv.classList.add('visible');
contentDiv.innerHTML = `
<span class="pkg-name" title="${info.name}">${info.name}</span>
<div class="pkg-stats">
<div class="pkg-stat">
<span class="pkg-stat-label">${i18n.t('dependencies')}:</span>
<span class="pkg-stat-value">${info.dependencies.length}</span>
</div>
<div class="pkg-stat">
<span class="pkg-stat-label">${i18n.t('usedBy')}:</span>
<span class="pkg-stat-value">${info.usedBy.length}</span>
</div>
</div>
`;
} catch (e) {
console.error('Failed to load package info:', e);
}
}
// Refresh graph view
async function refreshGraph(depth, group, reverse, internal, external, moduleLevel, directOnly) {
document.getElementById('graphView').classList.remove('hidden');
document.getElementById('textView').classList.add('hidden');
// Reset pan/zoom on refresh
currentZoom = 1;
panX = 0;
panY = 0;
applyTransform();
let url = `/api/graph?depth=${depth}&group=${group}&reverse=${reverse}&internal=${internal}`;
if (external !== undefined) {
url += `&external=${external}`;
}
if (moduleLevel !== undefined) {
url += `&module=${moduleLevel}`;
}
if (directOnly !== undefined) {
url += `&direct=${directOnly}`;
}
if (selectedPackage) {
url += '&package=' + encodeURIComponent(selectedPackage);
}
try {
const response = await fetch(url);
const data = await response.json();
document.getElementById('nodeCount').textContent = data.nodes.length;
document.getElementById('edgeCount').textContent = data.edges.length;
// Generate mermaid code
// Build node id to label map
const nodeLabels = {};
data.nodes.forEach(node => {
nodeLabels[node.id] = node.label;
});
// Use current layout direction
let mermaidCode = `graph ${currentLayout}\n`;
// First define all nodes with labels
data.nodes.forEach(node => {
mermaidCode += ` ${node.id}["${node.label}"]\n`;
});
// Then add edges
data.edges.forEach(edge => {
mermaidCode += ` ${edge.from} --> ${edge.to}\n`;
});
const container = document.getElementById('mermaidGraph');
container.innerHTML = mermaidCode;
container.removeAttribute('data-processed');
await mermaid.run({ nodes: [container] });
// Auto-fit if graph is large
setTimeout(() => {
autoFitGraph();
}, 100);
} catch (e) {
console.error('Failed to render graph:', e);
document.getElementById('mermaidGraph').innerHTML =
`<div class="loading">${i18n.t('renderError')}</div>`;
}
}
// Auto-fit graph to viewport
function autoFitGraph() {
const viewport = document.getElementById('graphView');
const svg = document.querySelector('.mermaid svg');
if (!viewport || !svg) return;
const viewportRect = viewport.getBoundingClientRect();
const svgRect = svg.getBoundingClientRect();
if (svgRect.width === 0 || svgRect.height === 0) return;
// Calculate scale to fit
const scaleX = (viewportRect.width - 40) / svgRect.width;
const scaleY = (viewportRect.height - 40) / svgRect.height;
const scale = Math.min(scaleX, scaleY, 1); // Don't zoom in beyond 100%
if (scale < 1) {
currentZoom = scale;
// Center the graph
panX = (viewportRect.width - svgRect.width * scale) / 2;
panY = 20;
applyTransform();
showZoomIndicator();
}
}
// Refresh tree view
async function refreshTree(depth, internal, external, moduleLevel, directOnly) {
document.getElementById('graphView').classList.add('hidden');
document.getElementById('textView').classList.remove('hidden');
let url = `/api/tree?depth=${depth}&internal=${internal}`;
if (external !== undefined) {
url += `&external=${external}`;
}
if (moduleLevel !== undefined) {
url += `&module=${moduleLevel}`;
}
if (directOnly !== undefined) {
url += `&direct=${directOnly}`;
}
if (selectedPackage) {
url += '&package=' + encodeURIComponent(selectedPackage);
}
try {
const response = await fetch(url);
const text = await response.text();
document.getElementById('textView').textContent = text;
const lines = text.split('\n').filter(l => l.trim());
document.getElementById('nodeCount').textContent = lines.length;
document.getElementById('edgeCount').textContent = '-';
} catch (e) {
console.error('Failed to load tree:', e);
}
}
// Refresh list view
async function refreshList(internal, external, moduleLevel, directOnly) {
document.getElementById('graphView').classList.add('hidden');
document.getElementById('textView').classList.remove('hidden');
let url = `/api/list?internal=${internal}`;
if (external !== undefined) {
url += `&external=${external}`;
}
if (moduleLevel !== undefined) {
url += `&module=${moduleLevel}`;
}
if (directOnly !== undefined) {
url += `&direct=${directOnly}`;
}
if (selectedPackage) {
url += '&package=' + encodeURIComponent(selectedPackage);
}
try {
const response = await fetch(url);
const text = await response.text();
document.getElementById('textView').textContent = text;
const lines = text.split('\n').filter(l => l.trim());
document.getElementById('nodeCount').textContent = lines.length;
document.getElementById('edgeCount').textContent = '-';
} catch (e) {
console.error('Failed to load list:', e);
}
}
// Zoom functions
function zoomIn() {
const viewport = document.getElementById('graphView');
if (!viewport) return;
const rect = viewport.getBoundingClientRect();
const centerX = rect.width / 2;
const centerY = rect.height / 2;
const newZoom = Math.min(MAX_ZOOM, currentZoom * 1.2);
const scale = newZoom / currentZoom;
panX = centerX - (centerX - panX) * scale;
panY = centerY - (centerY - panY) * scale;
currentZoom = newZoom;
applyTransform();
showZoomIndicator();
}
function zoomOut() {
const viewport = document.getElementById('graphView');
if (!viewport) return;
const rect = viewport.getBoundingClientRect();
const centerX = rect.width / 2;
const centerY = rect.height / 2;
const newZoom = Math.max(MIN_ZOOM, currentZoom / 1.2);
const scale = newZoom / currentZoom;
panX = centerX - (centerX - panX) * scale;
panY = centerY - (centerY - panY) * scale;
currentZoom = newZoom;
applyTransform();
showZoomIndicator();
}
function resetZoom() {
currentZoom = 1;
panX = 0;
panY = 0;
applyTransform();
showZoomIndicator();
}
function fitToScreen() {
autoFitGraph();
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', init);

View File

@ -1,228 +0,0 @@
// Internationalization (i18n) Module
const i18n = {
currentLang: 'en',
translations: {
en: {
title: 'Go Package Dependencies',
pageTitle: 'Package Dependency Viewer',
currentModule: 'Current:',
remoteModuleLabel: 'Analyze Module:',
remoteModulePlaceholder: 'e.g. github.com/gogf/gf/v2',
selectVersion: 'Version',
analyze: 'Analyze',
resetLocal: 'Reset',
fetchingVersions: 'Fetching...',
analyzing: 'Analyzing...',
viewLabel: 'View:',
viewGraph: 'Graph',
viewTree: 'Tree',
viewList: 'List',
depthLabel: 'Depth:',
depthUnlimited: 'Unlimited',
reverseLabel: 'Reverse (show who uses)',
groupLabel: 'Group by directory',
internalLabel: 'Internal only',
externalLabel: 'External packages',
moduleLevelLabel: 'Module level',
directOnlyLabel: 'Direct only',
statsInternal: 'Internal',
statsExternal: 'External',
statsStdlib: 'Stdlib',
layoutLabel: 'Layout:',
layoutTD: 'Top-Down',
layoutLR: 'Left-Right',
showAll: 'Show All',
packagesTitle: 'Packages',
searchPlaceholder: 'Search packages...',
flatMode: 'Flat list',
treeMode: 'Tree view',
statsPackages: 'Packages',
statsDependencies: 'Dependencies',
dependencies: 'Dependencies',
usedBy: 'Used by',
noPackages: 'No packages found',
renderError: 'Unable to render graph. Try reducing depth or selecting a specific package.',
packageNotFound: 'Package not found',
zoomIn: 'Zoom in',
zoomOut: 'Zoom out',
fitToScreen: 'Fit to screen',
resetZoom: 'Reset zoom',
dragToMove: 'Drag to move, scroll to zoom',
noVersions: 'No versions found',
latestVersion: 'Latest',
errorFetchVersions: 'Failed to fetch versions',
errorAnalyze: 'Failed to analyze module',
packageDetails: 'Package Details',
viewGraphTooltip: 'Visualize dependencies as a directed graph',
viewTreeTooltip: 'Show dependencies in a hierarchical tree structure',
viewListTooltip: 'Display dependencies as a text list',
depthTooltip: 'Limit dependency traversal depth',
reverseTooltip: 'Show reverse dependencies (who uses the selected package)',
groupTooltip: 'Group packages by directory structure',
internalTooltip: 'Show only internal packages from current module',
externalTooltip: 'Include external dependency packages',
moduleLevelTooltip: 'Show module-level dependencies (from go.mod)',
directOnlyTooltip: 'Show only direct dependencies (requires module level)',
layoutTDTooltip: 'Top-down layout for dependency graph',
layoutLRTooltip: 'Left-right layout for dependency graph',
showAllTooltip: 'Clear package selection and show all packages',
analyzeTooltip: 'Analyze remote Go module dependencies',
resetTooltip: 'Reset to local module analysis',
themeTooltip: 'Toggle light/dark theme',
langTooltip: 'Select language',
flatModeTooltip: 'Flat list view',
treeModeTooltip: 'Tree view',
zoomInTooltip: 'Zoom in',
zoomOutTooltip: 'Zoom out',
fitToScreenTooltip: 'Fit to screen',
resetZoomTooltip: 'Reset zoom',
remoteModuleTooltip: 'Enter a remote Go module path to analyze',
versionSelectTooltip: 'Select module version (optional)',
searchTooltip: 'Search packages by name'
},
zh: {
title: 'Go 包依赖分析',
pageTitle: '包依赖查看器',
currentModule: '当前模块:',
remoteModuleLabel: '分析模块:',
remoteModulePlaceholder: '例如 github.com/gogf/gf/v2',
selectVersion: '版本',
analyze: '分析',
resetLocal: '重置',
fetchingVersions: '获取中...',
analyzing: '分析中...',
viewLabel: '视图:',
viewGraph: '图表',
viewTree: '树形',
viewList: '列表',
depthLabel: '深度:',
depthUnlimited: '无限制',
reverseLabel: '反向依赖 (谁引用了它)',
groupLabel: '按目录分组',
internalLabel: '仅内部包',
externalLabel: '外部依赖包',
moduleLevelLabel: '模块级别',
directOnlyLabel: '仅直接依赖',
statsInternal: '内部',
statsExternal: '外部',
statsStdlib: '标准库',
layoutLabel: '布局:',
layoutTD: '从上到下',
layoutLR: '从左到右',
showAll: '显示全部',
packagesTitle: '包列表',
searchPlaceholder: '搜索包...',
flatMode: '平铺列表',
treeMode: '目录树',
statsPackages: '包数量',
statsDependencies: '依赖数',
dependencies: '依赖',
usedBy: '被引用',
noPackages: '未找到包',
renderError: '无法渲染图表请尝试减少深度或选择特定的包',
packageNotFound: '未找到包',
zoomIn: '放大',
zoomOut: '缩小',
fitToScreen: '适应屏幕',
resetZoom: '重置缩放',
dragToMove: '拖拽移动滚轮缩放',
noVersions: '未找到版本',
latestVersion: '最新版本',
errorFetchVersions: '获取版本失败',
errorAnalyze: '分析模块失败',
packageDetails: '包详情',
viewGraphTooltip: '以有向图形式可视化依赖关系',
viewTreeTooltip: '以层次树结构显示依赖关系',
viewListTooltip: '以文本列表形式显示依赖关系',
depthTooltip: '限制依赖遍历的深度',
reverseTooltip: '显示反向依赖谁引用了选中的包',
groupTooltip: '按目录结构分组显示包',
internalTooltip: '仅显示当前模块的内部包',
externalTooltip: '包含外部依赖包',
moduleLevelTooltip: '显示模块级别依赖来自go.mod',
directOnlyTooltip: '仅显示直接依赖需要启用模块级别',
layoutTDTooltip: '依赖图从上到下布局',
layoutLRTooltip: '依赖图从左到右布局',
showAllTooltip: '清除包选择显示所有包',
analyzeTooltip: '分析远程Go模块依赖',
resetTooltip: '重置为本地模块分析',
themeTooltip: '切换明暗主题',
langTooltip: '选择语言',
flatModeTooltip: '平铺列表视图',
treeModeTooltip: '树形视图',
zoomInTooltip: '放大',
zoomOutTooltip: '缩小',
fitToScreenTooltip: '适应屏幕',
resetZoomTooltip: '重置缩放',
remoteModuleTooltip: '输入远程Go模块路径进行分析',
versionSelectTooltip: '选择模块版本可选',
searchTooltip: '按名称搜索包'
}
},
init() {
// Load saved language preference
const savedLang = localStorage.getItem('dep-viewer-lang');
if (savedLang && this.translations[savedLang]) {
this.currentLang = savedLang;
} else {
// Detect browser language
const browserLang = navigator.language.toLowerCase();
if (browserLang.startsWith('zh')) {
this.currentLang = 'zh';
}
}
// Set select value
const langSelect = document.getElementById('langSelect');
if (langSelect) {
langSelect.value = this.currentLang;
langSelect.addEventListener('change', (e) => {
this.setLanguage(e.target.value);
});
}
this.applyTranslations();
},
setLanguage(lang) {
if (this.translations[lang]) {
this.currentLang = lang;
localStorage.setItem('dep-viewer-lang', lang);
this.applyTranslations();
}
},
t(key) {
return this.translations[this.currentLang][key] || this.translations['en'][key] || key;
},
applyTranslations() {
// Update elements with data-i18n attribute
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
el.textContent = this.t(key);
});
// Update placeholders
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
const key = el.getAttribute('data-i18n-placeholder');
el.placeholder = this.t(key);
});
// Update title attributes
document.querySelectorAll('[data-i18n-title]').forEach(el => {
const key = el.getAttribute('data-i18n-title');
el.title = this.t(key);
});
// Update page title
document.title = this.t('title');
}
};
// Initialize i18n when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
i18n.init();
});

View File

@ -1,161 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title data-i18n="title">Go Package Dependencies</title>
<link rel="stylesheet" href="/static/style.css">
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
</head>
<body>
<div class="header">
<div class="header-left">
<h1 data-i18n="pageTitle">Package Dependency Viewer</h1>
<div class="module-info">
<span data-i18n="currentModule">Current:</span>
<span class="module" id="moduleName"></span>
</div>
</div>
<div class="header-center">
<div class="remote-input-group">
<label for="remoteModuleInput" data-i18n="remoteModuleLabel">Module:</label>
<input type="text" id="remoteModuleInput" data-i18n-placeholder="remoteModulePlaceholder" placeholder="e.g. github.com/gogf/gf/v2" data-i18n-title="remoteModuleTooltip">
<select id="versionSelect" disabled data-i18n-title="versionSelectTooltip">
<option value="" data-i18n="selectVersion">Select version</option>
</select>
<button class="btn btn-sm" id="analyzeBtn" onclick="analyzeRemoteModule()" data-i18n="analyze" data-i18n-title="analyzeTooltip">Analyze</button>
<button class="btn btn-sm btn-secondary" id="resetBtn" onclick="resetToLocal()" data-i18n="resetLocal" data-i18n-title="resetTooltip">Reset</button>
<span class="loading-spinner hidden" id="loadingSpinner"></span>
</div>
</div>
<div class="header-right">
<div class="theme-switch">
<button class="icon-btn" id="themeToggle" data-i18n-title="themeTooltip">
<span class="icon-sun"></span>
<span class="icon-moon">🌙</span>
</button>
</div>
<div class="lang-switch">
<select id="langSelect" data-i18n-title="langTooltip">
<option value="en">English</option>
<option value="zh">中文</option>
</select>
</div>
</div>
</div>
<div class="controls">
<div class="control-group">
<label data-i18n="viewLabel">View:</label>
<div class="btn-group">
<button class="btn active" id="btnGraph" onclick="setView('graph')" data-i18n="viewGraph" data-i18n-title="viewGraphTooltip">Graph</button>
<button class="btn" id="btnTree" onclick="setView('tree')" data-i18n="viewTree" data-i18n-title="viewTreeTooltip">Tree</button>
<button class="btn" id="btnList" onclick="setView('list')" data-i18n="viewList" data-i18n-title="viewListTooltip">List</button>
</div>
</div>
<div class="control-group">
<label data-i18n="depthLabel">Depth:</label>
<select id="depth" onchange="refresh()" data-i18n-title="depthTooltip">
<option value="1">1</option>
<option value="2">2</option>
<option value="3" selected>3</option>
<option value="5">5</option>
<option value="0" data-i18n="depthUnlimited">Unlimited</option>
</select>
</div>
<div class="control-group">
<input type="checkbox" id="reverse" onchange="refresh()" data-i18n-title="reverseTooltip">
<label for="reverse" data-i18n="reverseLabel">Reverse (show who uses)</label>
</div>
<div class="control-group">
<input type="checkbox" id="group" onchange="refresh()" data-i18n-title="groupTooltip">
<label for="group" data-i18n="groupLabel">Group by directory</label>
</div>
<div class="control-group">
<input type="checkbox" id="internal" checked onchange="refresh()" data-i18n-title="internalTooltip">
<label for="internal" data-i18n="internalLabel">Internal only</label>
</div>
<div class="control-group">
<input type="checkbox" id="external" onchange="refresh()" data-i18n-title="externalTooltip">
<label for="external" data-i18n="externalLabel">External packages</label>
</div>
<div class="control-group">
<input type="checkbox" id="moduleLevel" onchange="refresh()" data-i18n-title="moduleLevelTooltip">
<label for="moduleLevel" data-i18n="moduleLevelLabel">Module level</label>
</div>
<div class="control-group">
<input type="checkbox" id="directOnly" onchange="refresh()" data-i18n-title="directOnlyTooltip">
<label for="directOnly" data-i18n="directOnlyLabel">Direct only</label>
</div>
<div class="control-group graph-only" id="layoutGroup">
<label data-i18n="layoutLabel">Layout:</label>
<div class="btn-group">
<button class="btn active" id="layoutTD" onclick="setLayout('TD')" data-i18n="layoutTD" data-i18n-title="layoutTDTooltip">Top-Down</button>
<button class="btn" id="layoutLR" onclick="setLayout('LR')" data-i18n="layoutLR" data-i18n-title="layoutLRTooltip">Left-Right</button>
</div>
</div>
<button class="btn btn-secondary" onclick="clearSelection()" data-i18n="showAll" data-i18n-title="showAllTooltip">Show All</button>
</div>
<div class="main-content">
<div class="sidebar">
<div class="sidebar-header">
<div class="sidebar-title">
<h3 data-i18n="packagesTitle">Packages</h3>
<span class="count" id="packageCount">0</span>
</div>
<div class="sidebar-mode">
<button class="mode-btn" id="modeFlat" onclick="setPackageListMode('flat')" data-i18n-title="flatModeTooltip"></button>
<button class="mode-btn active" id="modeTree" onclick="setPackageListMode('tree')" data-i18n-title="treeModeTooltip">🌲</button>
</div>
</div>
<div class="search-box">
<input type="text" id="searchInput" data-i18n-placeholder="searchPlaceholder" placeholder="Search packages..." oninput="filterPackages()" data-i18n-title="searchTooltip">
</div>
<div class="package-list" id="packageList"></div>
</div>
<div class="content-area">
<div class="stats">
<div class="stat-card">
<div class="value" id="nodeCount">0</div>
<div class="label" data-i18n="statsPackages">Packages</div>
</div>
<div class="stat-card">
<div class="value" id="edgeCount">0</div>
<div class="label" data-i18n="statsDependencies">Dependencies</div>
</div>
<div class="stat-card">
<div class="value" id="internalCount">0</div>
<div class="label" data-i18n="statsInternal">Internal</div>
</div>
<div class="stat-card">
<div class="value" id="externalCount">0</div>
<div class="label" data-i18n="statsExternal">External</div>
</div>
<div class="stat-card">
<div class="value" id="stdlibCount">0</div>
<div class="label" data-i18n="statsStdlib">Stdlib</div>
</div>
<div class="package-info" id="packageInfo">
<div class="package-info-content" id="packageInfoContent"></div>
</div>
</div>
<div class="graph-container" id="graphContainer">
<div id="graphView" class="graph-viewport">
<div class="mermaid-container" id="mermaidContainer">
<div class="mermaid" id="mermaidGraph"></div>
</div>
<div class="zoom-indicator" id="zoomIndicator">100%</div>
</div>
<div id="textView" class="text-output hidden"></div>
</div>
</div>
</div>
<div class="zoom-controls" id="zoomControls">
<button class="zoom-btn" onclick="zoomIn()" data-i18n-title="zoomInTooltip">+</button>
<button class="zoom-btn" onclick="zoomOut()" data-i18n-title="zoomOutTooltip"></button>
<button class="zoom-btn" onclick="fitToScreen()" data-i18n-title="fitToScreenTooltip"></button>
<button class="zoom-btn" onclick="resetZoom()" data-i18n-title="resetZoomTooltip"></button>
</div>
<script src="/static/i18n.js"></script>
<script src="/static/app.js"></script>
</body>
</html>

View File

@ -1,811 +0,0 @@
/* CSS Variables for Theming */
:root {
/* Light Theme (Default) */
--bg-primary: #f8fafc;
--bg-secondary: #ffffff;
--bg-tertiary: #f1f5f9;
--bg-hover: #e2e8f0;
--text-primary: #1e293b;
--text-secondary: #475569;
--text-muted: #94a3b8;
--border-color: #e2e8f0;
--accent-color: #3b82f6;
--accent-hover: #2563eb;
--accent-light: #dbeafe;
--success-color: #10b981;
--card-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
--input-bg: #ffffff;
--input-border: #cbd5e1;
}
[data-theme="dark"] {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--bg-hover: #475569;
--text-primary: #f1f5f9;
--text-secondary: #cbd5e1;
--text-muted: #64748b;
--border-color: #334155;
--accent-color: #60a5fa;
--accent-hover: #3b82f6;
--accent-light: #1e3a5f;
--success-color: #34d399;
--card-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
--input-bg: #1e293b;
--input-border: #475569;
}
/* Reset & Base */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
overflow: hidden;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
display: flex;
flex-direction: column;
transition: background-color 0.3s, color 0.3s;
}
/* Header */
.header {
background: var(--bg-secondary);
padding: 12px 24px;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
box-shadow: var(--card-shadow);
flex-shrink: 0;
}
.header-left {
flex-shrink: 0;
}
.header-left h1 {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
.header-left .module-info {
display: flex;
align-items: center;
gap: 6px;
color: var(--text-muted);
font-size: 12px;
margin-top: 2px;
}
.header-left .module {
color: var(--accent-color);
font-weight: 500;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Header Center - Remote Module Input */
.header-center {
flex: 1;
display: flex;
justify-content: center;
}
.remote-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.remote-input-group input[type="text"] {
width: 320px;
background: var(--input-bg);
border: 1px solid var(--input-border);
color: var(--text-primary);
padding: 6px 12px;
border-radius: 6px;
font-size: 13px;
transition: border-color 0.2s;
}
.remote-input-group input[type="text"]:focus {
outline: none;
border-color: var(--accent-color);
}
.remote-input-group select {
width: 160px;
background: var(--input-bg);
border: 1px solid var(--input-border);
color: var(--text-primary);
padding: 6px 10px;
border-radius: 6px;
font-size: 13px;
cursor: pointer;
}
.remote-input-group select:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-sm {
padding: 6px 12px;
font-size: 12px;
}
.loading-spinner {
width: 18px;
height: 18px;
border: 2px solid var(--border-color);
border-top-color: var(--accent-color);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.header-right {
display: flex;
align-items: center;
gap: 12px;
}
.icon-btn {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 8px 12px;
cursor: pointer;
font-size: 16px;
transition: all 0.2s;
}
.icon-btn:hover {
background: var(--bg-hover);
}
.icon-sun, .icon-moon {
display: none;
}
body:not([data-theme="dark"]) .icon-moon {
display: inline;
}
[data-theme="dark"] .icon-sun {
display: inline;
}
.lang-switch select {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
color: var(--text-primary);
padding: 8px 12px;
border-radius: 8px;
font-size: 13px;
cursor: pointer;
}
/* Controls */
.controls {
background: var(--bg-secondary);
padding: 12px 24px;
display: flex;
gap: 20px;
flex-wrap: wrap;
align-items: center;
border-bottom: 1px solid var(--border-color);
}
.control-group {
display: flex;
align-items: center;
gap: 8px;
}
.control-group label {
font-size: 13px;
color: var(--text-secondary);
font-weight: 500;
}
.control-group select {
background: var(--input-bg);
border: 1px solid var(--input-border);
color: var(--text-primary);
padding: 6px 12px;
border-radius: 6px;
font-size: 13px;
cursor: pointer;
}
.control-group input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: var(--accent-color);
}
/* Buttons */
.btn {
background: var(--accent-color);
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.2s;
}
.btn:hover {
background: var(--accent-hover);
}
.btn.active {
background: var(--accent-color);
box-shadow: 0 0 0 2px var(--accent-light);
}
.btn-secondary {
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.btn-secondary:hover {
background: var(--bg-hover);
}
.btn-group {
display: flex;
gap: 0;
}
.btn-group .btn {
border-radius: 0;
border-right: 1px solid rgba(255, 255, 255, 0.2);
}
.btn-group .btn:first-child {
border-radius: 6px 0 0 6px;
}
.btn-group .btn:last-child {
border-radius: 0 6px 6px 0;
border-right: none;
}
.btn-group .btn:not(.active) {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.btn-group .btn:not(.active):hover {
background: var(--bg-hover);
}
/* Main Content */
.main-content {
display: flex;
flex: 1;
overflow: hidden;
min-height: 0; /* Important for flex children to respect overflow */
}
/* Sidebar */
.sidebar {
width: 280px;
min-width: 280px;
background: var(--bg-secondary);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
overflow: hidden;
height: 100%; /* Ensure sidebar takes full height */
}
.sidebar-header {
padding: 10px 16px;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0; /* Prevent shrinking */
}
.sidebar-title {
display: flex;
align-items: center;
gap: 8px;
}
.sidebar-title h3 {
font-size: 14px;
font-weight: 600;
color: var(--text-secondary);
}
.sidebar-title .count {
background: var(--accent-color);
color: white;
padding: 2px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.sidebar-mode {
display: flex;
gap: 4px;
}
.mode-btn {
width: 28px;
height: 28px;
border: 1px solid var(--border-color);
background: var(--bg-tertiary);
border-radius: 6px;
cursor: pointer;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.mode-btn:hover {
background: var(--bg-hover);
}
.mode-btn.active {
background: var(--accent-color);
border-color: var(--accent-color);
color: white;
}
.search-box {
padding: 12px 16px;
border-bottom: 1px solid var(--border-color);
flex-shrink: 0; /* Prevent shrinking */
}
.search-box input {
width: 100%;
background: var(--input-bg);
border: 1px solid var(--input-border);
color: var(--text-primary);
padding: 10px 14px;
border-radius: 8px;
font-size: 13px;
transition: border-color 0.2s;
}
.search-box input:focus {
outline: none;
border-color: var(--accent-color);
}
.search-box input::placeholder {
color: var(--text-muted);
}
.package-list {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 8px 0;
min-height: 0; /* Important for flex overflow to work */
}
.package-item {
padding: 10px 16px;
cursor: pointer;
font-size: 13px;
color: var(--text-secondary);
border-left: 3px solid transparent;
transition: all 0.15s;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: flex;
justify-content: space-between;
align-items: center;
}
.package-item .pkg-name-text {
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
.pkg-stats-inline {
display: flex;
gap: 6px;
font-size: 11px;
margin-left: 8px;
flex-shrink: 0;
}
.pkg-stats-inline .dep-count {
color: var(--accent-color);
opacity: 0.8;
}
.pkg-stats-inline .used-count {
color: #10b981;
opacity: 0.8;
}
.package-item:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.package-item.active {
background: var(--accent-light);
border-left-color: var(--accent-color);
color: var(--accent-color);
font-weight: 500;
}
/* Tree View Styles */
.tree-node {
user-select: none;
}
.tree-node-header {
display: flex;
align-items: center;
padding: 6px 12px;
cursor: pointer;
font-size: 13px;
color: var(--text-secondary);
transition: all 0.15s;
}
.tree-node-header .pkg-stats-inline {
margin-left: auto;
padding-left: 8px;
}
.tree-node-header:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.tree-node-toggle {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 2px;
font-size: 10px;
color: var(--text-muted);
transition: transform 0.2s;
cursor: pointer;
border-radius: 4px;
}
.tree-node-toggle:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.tree-node-toggle.expanded {
transform: rotate(90deg);
}
.tree-node-toggle.empty {
visibility: hidden;
}
.tree-node-icon {
margin-right: 6px;
font-size: 12px;
}
.tree-node-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
padding: 2px 4px;
border-radius: 4px;
}
.tree-node-name:hover {
background: var(--bg-hover);
}
.tree-node-name.package {
color: var(--accent-color);
}
.tree-node-children {
padding-left: 16px;
display: none;
}
.tree-node-children.expanded {
display: block;
}
.tree-node-header.active {
background: var(--accent-light);
color: var(--accent-color);
}
.tree-node-header.active .tree-node-name {
font-weight: 500;
}
/* Content Area */
.content-area {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Stats */
.stats {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 24px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
}
.stat-card {
background: var(--bg-tertiary);
padding: 10px 20px;
border-radius: 10px;
text-align: center;
min-width: 90px;
}
.stat-card .value {
font-size: 20px;
font-weight: 700;
color: var(--accent-color);
}
.stat-card .label {
font-size: 11px;
color: var(--text-muted);
margin-top: 2px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Package Info - in stats bar */
.package-info {
display: none;
align-items: center;
gap: 16px;
background: var(--bg-tertiary);
padding: 8px 16px;
border-radius: 10px;
}
.package-info.visible {
display: flex;
}
.package-info-content {
display: flex;
align-items: center;
gap: 20px;
}
.package-info-content .pkg-name {
font-size: 14px;
font-weight: 600;
color: var(--accent-color);
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.package-info-content .pkg-stats {
display: flex;
gap: 16px;
}
.package-info-content .pkg-stat {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
}
.package-info-content .pkg-stat-label {
color: var(--text-muted);
}
.package-info-content .pkg-stat-value {
color: var(--text-primary);
font-weight: 600;
}
/* Graph Container */
.graph-container {
flex: 1;
padding: 24px;
overflow: hidden;
background: var(--bg-primary);
position: relative;
display: flex;
flex-direction: column;
min-height: 0; /* Important for flex overflow */
}
.graph-viewport {
flex: 1;
width: 100%;
overflow: hidden;
position: relative;
cursor: grab;
min-height: 0; /* Important for flex overflow */
}
.graph-viewport:active {
cursor: grabbing;
}
.mermaid-container {
display: inline-block;
min-width: 100%;
min-height: 100%;
transform-origin: 0 0;
transition: none;
}
.mermaid {
display: inline-block;
padding: 20px;
}
.mermaid svg {
display: block;
max-width: none !important;
height: auto;
}
.zoom-indicator {
position: absolute;
bottom: 16px;
left: 16px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
color: var(--text-secondary);
box-shadow: var(--card-shadow);
pointer-events: none;
opacity: 0;
transition: opacity 0.3s;
}
.zoom-indicator.visible {
opacity: 1;
}
.text-output {
flex: 1;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 20px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
font-size: 13px;
line-height: 1.7;
white-space: pre;
overflow: auto;
min-height: 0; /* Important for flex overflow */
color: var(--text-primary);
}
/* Zoom Controls */
.zoom-controls {
position: fixed;
bottom: 24px;
right: 24px;
display: flex;
gap: 8px;
}
.zoom-btn {
width: 40px;
height: 40px;
border-radius: 10px;
background: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid var(--border-color);
font-size: 18px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
box-shadow: var(--card-shadow);
}
.zoom-btn:hover {
background: var(--accent-color);
color: white;
border-color: var(--accent-color);
}
/* Utility Classes */
.hidden {
display: none !important;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
color: var(--text-muted);
font-size: 14px;
}
/* Scrollbar Styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-tertiary);
}
::-webkit-scrollbar-thumb {
background: var(--text-muted);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-secondary);
}
/* Responsive */
@media (max-width: 768px) {
.sidebar {
width: 220px;
}
.controls {
padding: 10px 16px;
gap: 12px;
}
.header {
padding: 12px 16px;
}
}

View File

@ -33,65 +33,88 @@ import (
)
type (
CGenDao struct{}
// CGenDao is the command handler struct for "gen dao" command.
CGenDao struct{}
// CGenDaoInput defines all input parameters for the "gen dao" command.
// It supports both command-line arguments and configuration file options.
CGenDaoInput struct {
g.Meta `name:"dao" config:"{CGenDaoConfig}" usage:"{CGenDaoUsage}" brief:"{CGenDaoBrief}" eg:"{CGenDaoEg}" ad:"{CGenDaoAd}"`
Path string `name:"path" short:"p" brief:"{CGenDaoBriefPath}" d:"internal"`
Link string `name:"link" short:"l" brief:"{CGenDaoBriefLink}"`
Tables string `name:"tables" short:"t" brief:"{CGenDaoBriefTables}"`
TablesEx string `name:"tablesEx" short:"x" brief:"{CGenDaoBriefTablesEx}"`
ShardingPattern []string `name:"shardingPattern" short:"sp" brief:"{CGenDaoBriefShardingPattern}"`
Group string `name:"group" short:"g" brief:"{CGenDaoBriefGroup}" d:"default"`
Prefix string `name:"prefix" short:"f" brief:"{CGenDaoBriefPrefix}"`
RemovePrefix string `name:"removePrefix" short:"r" brief:"{CGenDaoBriefRemovePrefix}"`
RemoveFieldPrefix string `name:"removeFieldPrefix" short:"rf" brief:"{CGenDaoBriefRemoveFieldPrefix}"`
JsonCase string `name:"jsonCase" short:"j" brief:"{CGenDaoBriefJsonCase}" d:"CamelLower"`
ImportPrefix string `name:"importPrefix" short:"i" brief:"{CGenDaoBriefImportPrefix}"`
DaoPath string `name:"daoPath" short:"d" brief:"{CGenDaoBriefDaoPath}" d:"dao"`
TablePath string `name:"tablePath" short:"tp" brief:"{CGenDaoBriefTablePath}" d:"table"`
DoPath string `name:"doPath" short:"o" brief:"{CGenDaoBriefDoPath}" d:"model/do"`
EntityPath string `name:"entityPath" short:"e" brief:"{CGenDaoBriefEntityPath}" d:"model/entity"`
TplDaoTablePath string `name:"tplDaoTablePath" short:"t0" brief:"{CGenDaoBriefTplDaoTablePath}"`
TplDaoIndexPath string `name:"tplDaoIndexPath" short:"t1" brief:"{CGenDaoBriefTplDaoIndexPath}"`
TplDaoInternalPath string `name:"tplDaoInternalPath" short:"t2" brief:"{CGenDaoBriefTplDaoInternalPath}"`
TplDaoDoPath string `name:"tplDaoDoPath" short:"t3" brief:"{CGenDaoBriefTplDaoDoPathPath}"`
TplDaoEntityPath string `name:"tplDaoEntityPath" short:"t4" brief:"{CGenDaoBriefTplDaoEntityPath}"`
StdTime bool `name:"stdTime" short:"s" brief:"{CGenDaoBriefStdTime}" orphan:"true"`
WithTime bool `name:"withTime" short:"w" brief:"{CGenDaoBriefWithTime}" orphan:"true"`
GJsonSupport bool `name:"gJsonSupport" short:"n" brief:"{CGenDaoBriefGJsonSupport}" orphan:"true"`
OverwriteDao bool `name:"overwriteDao" short:"v" brief:"{CGenDaoBriefOverwriteDao}" orphan:"true"`
DescriptionTag bool `name:"descriptionTag" short:"c" brief:"{CGenDaoBriefDescriptionTag}" orphan:"true"`
NoJsonTag bool `name:"noJsonTag" short:"k" brief:"{CGenDaoBriefNoJsonTag}" orphan:"true"`
NoModelComment bool `name:"noModelComment" short:"m" brief:"{CGenDaoBriefNoModelComment}" orphan:"true"`
Clear bool `name:"clear" short:"a" brief:"{CGenDaoBriefClear}" orphan:"true"`
GenTable bool `name:"genTable" short:"gt" brief:"{CGenDaoBriefGenTable}" orphan:"true"`
Path string `name:"path" short:"p" brief:"{CGenDaoBriefPath}" d:"internal"` // Base directory path for generated files.
Link string `name:"link" short:"l" brief:"{CGenDaoBriefLink}"` // Database connection string (e.g., "mysql:root:pass@tcp(127.0.0.1:3306)/db").
Tables string `name:"tables" short:"t" brief:"{CGenDaoBriefTables}"` // Comma-separated table names or wildcard patterns to include.
TablesEx string `name:"tablesEx" short:"x" brief:"{CGenDaoBriefTablesEx}"` // Comma-separated table names or wildcard patterns to exclude.
ShardingPattern []string `name:"shardingPattern" short:"sp" brief:"{CGenDaoBriefShardingPattern}"` // Patterns for sharding tables (e.g., "users_?" merges users_001, users_002 into one dao).
Group string `name:"group" short:"g" brief:"{CGenDaoBriefGroup}" d:"default"` // Database configuration group name for ORM instance.
Prefix string `name:"prefix" short:"f" brief:"{CGenDaoBriefPrefix}"` // Prefix to add to all generated table names.
RemovePrefix string `name:"removePrefix" short:"r" brief:"{CGenDaoBriefRemovePrefix}"` // Comma-separated prefixes to remove from table names.
RemoveFieldPrefix string `name:"removeFieldPrefix" short:"rf" brief:"{CGenDaoBriefRemoveFieldPrefix}"` // Comma-separated prefixes to remove from field names.
JsonCase string `name:"jsonCase" short:"j" brief:"{CGenDaoBriefJsonCase}" d:"CamelLower"` // Naming convention for JSON tags (e.g., CamelLower, Snake).
ImportPrefix string `name:"importPrefix" short:"i" brief:"{CGenDaoBriefImportPrefix}"` // Custom Go import path prefix for generated files.
DaoPath string `name:"daoPath" short:"d" brief:"{CGenDaoBriefDaoPath}" d:"dao"` // Sub-directory under Path for dao files.
TablePath string `name:"tablePath" short:"tp" brief:"{CGenDaoBriefTablePath}" d:"table"` // Sub-directory under Path for table field definition files.
DoPath string `name:"doPath" short:"o" brief:"{CGenDaoBriefDoPath}" d:"model/do"` // Sub-directory under Path for DO (Data Object) files.
EntityPath string `name:"entityPath" short:"e" brief:"{CGenDaoBriefEntityPath}" d:"model/entity"` // Sub-directory under Path for entity struct files.
TplDaoTablePath string `name:"tplDaoTablePath" short:"t0" brief:"{CGenDaoBriefTplDaoTablePath}"` // Custom template file for dao table generation.
TplDaoIndexPath string `name:"tplDaoIndexPath" short:"t1" brief:"{CGenDaoBriefTplDaoIndexPath}"` // Custom template file for dao index generation.
TplDaoInternalPath string `name:"tplDaoInternalPath" short:"t2" brief:"{CGenDaoBriefTplDaoInternalPath}"` // Custom template file for dao internal generation.
TplDaoDoPath string `name:"tplDaoDoPath" short:"t3" brief:"{CGenDaoBriefTplDaoDoPathPath}"` // Custom template file for DO generation.
TplDaoEntityPath string `name:"tplDaoEntityPath" short:"t4" brief:"{CGenDaoBriefTplDaoEntityPath}"` // Custom template file for entity generation.
StdTime bool `name:"stdTime" short:"s" brief:"{CGenDaoBriefStdTime}" orphan:"true"` // Use stdlib time.Time instead of gtime.Time for time fields.
WithTime bool `name:"withTime" short:"w" brief:"{CGenDaoBriefWithTime}" orphan:"true"` // Add creation timestamp to generated file headers.
GJsonSupport bool `name:"gJsonSupport" short:"n" brief:"{CGenDaoBriefGJsonSupport}" orphan:"true"` // Use *gjson.Json instead of string for JSON fields.
OverwriteDao bool `name:"overwriteDao" short:"v" brief:"{CGenDaoBriefOverwriteDao}" orphan:"true"` // Overwrite existing dao files (both index and internal).
DescriptionTag bool `name:"descriptionTag" short:"c" brief:"{CGenDaoBriefDescriptionTag}" orphan:"true"` // Add description struct tag with field comment.
NoJsonTag bool `name:"noJsonTag" short:"k" brief:"{CGenDaoBriefNoJsonTag}" orphan:"true"` // Omit json struct tags from generated structs.
NoModelComment bool `name:"noModelComment" short:"m" brief:"{CGenDaoBriefNoModelComment}" orphan:"true"` // Omit inline comments from generated struct fields.
Clear bool `name:"clear" short:"a" brief:"{CGenDaoBriefClear}" orphan:"true"` // Delete generated files that no longer correspond to database tables.
GenTable bool `name:"genTable" short:"gt" brief:"{CGenDaoBriefGenTable}" orphan:"true"` // Enable generation of table field definition files.
SqlDir string `name:"sqlDir" short:"sd" brief:"{CGenDaoBriefSqlDir}"` // Directory of SQL DDL files for offline generation (no DB connection needed).
SqlType string `name:"sqlType" short:"st" brief:"{CGenDaoBriefSqlType}" d:"mysql"` // SQL dialect when using SqlDir (mysql, pgsql, mssql, oracle, sqlite).
TypeMapping map[DBFieldTypeName]CustomAttributeType `name:"typeMapping" short:"y" brief:"{CGenDaoBriefTypeMapping}" orphan:"true"`
// TypeMapping maps database field type names to custom Go types.
// For example, mapping "decimal" to "float64" or "uuid" to "uuid.UUID".
TypeMapping map[DBFieldTypeName]CustomAttributeType `name:"typeMapping" short:"y" brief:"{CGenDaoBriefTypeMapping}" orphan:"true"`
// FieldMapping maps specific table.field combinations to custom Go types.
// For example, mapping "user.balance" to "decimal.Decimal".
FieldMapping map[DBTableFieldName]CustomAttributeType `name:"fieldMapping" short:"fm" brief:"{CGenDaoBriefFieldMapping}" orphan:"true"`
// internal usage purpose.
// genItems tracks all generated file paths and directories for cleanup purposes.
genItems *CGenDaoInternalGenItems
}
// CGenDaoOutput is the output of the "gen dao" command (currently empty).
CGenDaoOutput struct{}
// CGenDaoInternalInput extends CGenDaoInput with runtime-resolved fields
// used during the actual generation process.
CGenDaoInternalInput struct {
CGenDaoInput
DB gdb.DB
TableNames []string
NewTableNames []string
ShardingTableSet *gset.StrSet
DB gdb.DB // Database connection instance (nil in SQL file mode).
TableNames []string // Original table names from database or SQL files.
NewTableNames []string // Processed table names after prefix removal and sharding.
ShardingTableSet *gset.StrSet // Set of table names identified as sharding tables.
// TableFieldsMap stores pre-parsed table fields from SQL files.
// When this is set (SQL file mode), DB may be nil.
TableFieldsMap map[string]map[string]*gdb.TableField
}
DBTableFieldName = string
DBFieldTypeName = string
// DBTableFieldName is the fully-qualified field name in "table.field" format.
DBTableFieldName = string
// DBFieldTypeName is the database column type name (e.g., "varchar", "decimal").
DBFieldTypeName = string
// CustomAttributeType defines a custom Go type mapping with its import path.
CustomAttributeType struct {
Type string `brief:"custom attribute type name"`
Import string `brief:"custom import for this type"`
Type string `brief:"custom attribute type name"` // Go type name (e.g., "decimal.Decimal").
Import string `brief:"custom import for this type"` // Go import path (e.g., "github.com/shopspring/decimal").
}
)
var (
createdAt = gtime.Now()
tplView = gview.New()
createdAt = gtime.Now() // Timestamp captured at program start, used in generated file headers.
tplView = gview.New() // Shared template view instance for rendering all Go file templates.
// defaultTypeMapping provides built-in type mappings from database types to Go types.
// User-provided TypeMapping takes precedence over these defaults.
defaultTypeMapping = map[DBFieldTypeName]CustomAttributeType{
"decimal": {
Type: "float64",
@ -111,7 +134,8 @@ var (
},
}
// tablewriter Options
// twRenderer configures the tablewriter to render without borders or separators,
// producing clean aligned text output for generated Go source code.
twRenderer = tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{
Borders: tw.Border{Top: tw.Off, Bottom: tw.Off, Left: tw.Off, Right: tw.Off},
Settings: tw.Settings{
@ -126,9 +150,17 @@ var (
})
)
// Dao is the main entry point for the "gen dao" command.
// It dispatches to the appropriate generation mode based on input:
// - SQL file mode (SqlDir is set): generates from DDL files without database connection.
// - Link mode (Link is set): uses a direct database connection string.
// - Config mode: reads database configuration from the application config file.
func (c CGenDao) Dao(ctx context.Context, in CGenDaoInput) (out *CGenDaoOutput, err error) {
in.genItems = newCGenDaoInternalGenItems()
if in.Link != "" {
if in.SqlDir != "" {
// SQL file mode: generate from SQL DDL files without database connection.
doGenDaoFromSQLFiles(ctx, in)
} else if in.Link != "" {
doGenDaoForArray(ctx, -1, in)
} else if g.Cfg().Available(ctx) {
v := g.Cfg().MustGet(ctx, CGenDaoConfig)
@ -147,7 +179,11 @@ func (c CGenDao) Dao(ctx context.Context, in CGenDaoInput) (out *CGenDaoOutput,
return
}
// doGenDaoForArray implements the "gen dao" command for configuration array.
// doGenDaoForArray implements the "gen dao" command for a single configuration entry.
// When index >= 0, it reads configuration from the array at that index.
// When index < 0, it uses the input as-is (for Link mode or single config mode).
// It performs the full generation pipeline: connect to DB, resolve tables,
// apply sharding patterns, and generate dao/table/do/entity files.
func doGenDaoForArray(ctx context.Context, index int, in CGenDaoInput) {
var (
err error
@ -332,6 +368,10 @@ func doGenDaoForArray(ctx context.Context, index int, in CGenDaoInput) {
in.genItems.SetClear(in.Clear)
}
// getImportPartContent analyzes the generated Go source code and builds the import block.
// It automatically detects usage of gtime.Time, time.Time, and gjson.Json in the source,
// and includes the corresponding import paths. Additional custom imports (from TypeMapping
// or FieldMapping) are appended and their dependencies are resolved via "go get" if needed.
func getImportPartContent(ctx context.Context, source string, isDo bool, appendImports []string) string {
var packageImportsArray = garray.NewStrArray()
if isDo {
@ -385,6 +425,9 @@ func getImportPartContent(ctx context.Context, source string, isDo bool, appendI
return packageImportsStr
}
// assignDefaultVar sets the default template variables for datetime strings
// used in generated file headers. The creation timestamp is only included
// when WithTime is enabled in the input configuration.
func assignDefaultVar(view *gview.View, in CGenDaoInternalInput) {
var (
tplCreatedAtDatetimeStr string
@ -399,6 +442,8 @@ func assignDefaultVar(view *gview.View, in CGenDaoInternalInput) {
})
}
// sortFieldKeyForDao returns field names sorted by their Index in the TableField map.
// This preserves the original column order as defined in the database table schema.
func sortFieldKeyForDao(fieldMap map[string]*gdb.TableField) []string {
names := make(map[int]string)
for _, field := range fieldMap {
@ -423,6 +468,20 @@ func sortFieldKeyForDao(fieldMap map[string]*gdb.TableField) []string {
return result
}
// getTableFields retrieves table fields either from the pre-parsed TableFieldsMap (SQL file mode)
// or from the database connection. This abstracts the data source for generation functions.
func getTableFields(ctx context.Context, in CGenDaoInternalInput, tableName string) (map[string]*gdb.TableField, error) {
if in.TableFieldsMap != nil {
if fields, ok := in.TableFieldsMap[tableName]; ok {
return fields, nil
}
return nil, fmt.Errorf("table '%s' not found in SQL files", tableName)
}
return in.DB.TableFields(ctx, tableName)
}
// getTemplateFromPathOrDefault returns the template content from the given file path.
// If the file path is empty or the file has no content, it falls back to the default template.
func getTemplateFromPathOrDefault(filePath string, def string) string {
if filePath != "" {
if contents := gfile.GetContents(filePath); contents != "" {
@ -489,3 +548,130 @@ func filterTablesByPatterns(allTables []string, patterns []string) []string {
}
return result
}
// doGenDaoFromSQLFiles implements the "gen dao" command for SQL file mode.
// It parses DDL SQL files to obtain table structures without requiring a database connection.
func doGenDaoFromSQLFiles(ctx context.Context, in CGenDaoInput) {
if dirRealPath := gfile.RealPath(in.Path); dirRealPath == "" {
mlog.Fatalf(`path "%s" does not exist`, in.Path)
}
if dirRealPath := gfile.RealPath(in.SqlDir); dirRealPath == "" {
mlog.Fatalf(`SQL directory "%s" does not exist`, in.SqlDir)
}
dialect := SQLDialect(strings.ToLower(in.SqlType))
tableNames, tableFieldsMap := ParseSQLFilesFromDir(in.SqlDir, dialect)
removePrefixArray := gstr.SplitAndTrim(in.RemovePrefix, ",")
// Table filtering by name patterns.
if in.Tables != "" {
inputTables := gstr.SplitAndTrim(in.Tables, ",")
var hasPattern bool
for _, t := range inputTables {
if containsWildcard(t) {
hasPattern = true
break
}
}
if hasPattern {
tableNames = filterTablesByPatterns(tableNames, inputTables)
} else {
tableNames = inputTables
}
}
// Table excluding.
if in.TablesEx != "" {
array := garray.NewStrArrayFrom(tableNames)
for _, p := range gstr.SplitAndTrim(in.TablesEx, ",") {
if containsWildcard(p) {
regPattern := "^" + patternToRegex(p) + "$"
for _, v := range array.Clone().Slice() {
if gregex.IsMatchString(regPattern, v) {
array.RemoveValue(v)
}
}
} else {
array.RemoveValue(p)
}
}
tableNames = array.Slice()
}
// merge default typeMapping.
if in.TypeMapping == nil {
in.TypeMapping = defaultTypeMapping
} else {
for key, typeMapping := range defaultTypeMapping {
if _, ok := in.TypeMapping[key]; !ok {
in.TypeMapping[key] = typeMapping
}
}
}
// Process table names (prefix removal, sharding, etc.)
var (
newTableNames = make([]string, len(tableNames))
shardingNewTableSet = gset.NewStrSet()
)
sortedShardingPatterns := make([]string, len(in.ShardingPattern))
copy(sortedShardingPatterns, in.ShardingPattern)
sort.Slice(sortedShardingPatterns, func(i, j int) bool {
return len(sortedShardingPatterns[i]) > len(sortedShardingPatterns[j])
})
for i, tableName := range tableNames {
newTableName := tableName
for _, v := range removePrefixArray {
newTableName = gstr.TrimLeftStr(newTableName, v, 1)
}
if len(sortedShardingPatterns) > 0 {
for _, pattern := range sortedShardingPatterns {
var (
match []string
regPattern = gstr.Replace(pattern, "?", `(.+)`)
err error
)
match, err = gregex.MatchString(regPattern, newTableName)
if err != nil {
mlog.Fatalf(`invalid sharding pattern "%s": %+v`, pattern, err)
}
if len(match) < 2 {
continue
}
newTableName = gstr.Replace(pattern, "?", "")
newTableName = gstr.Trim(newTableName, `_.-`)
if shardingNewTableSet.Contains(newTableName) {
tableNames[i] = ""
break
}
shardingNewTableSet.Add(in.Prefix + newTableName)
break
}
}
newTableName = in.Prefix + newTableName
if tableNames[i] != "" {
newTableNames[i] = newTableName
}
}
tableNames = garray.NewStrArrayFrom(tableNames).FilterEmpty().Slice()
newTableNames = garray.NewStrArrayFrom(newTableNames).FilterEmpty().Slice()
in.genItems.Scale()
internalInput := CGenDaoInternalInput{
CGenDaoInput: in,
DB: nil,
TableNames: tableNames,
NewTableNames: newTableNames,
ShardingTableSet: shardingNewTableSet,
TableFieldsMap: tableFieldsMap,
}
// Generate all files using the same flow as database mode.
generateDao(ctx, internalInput)
generateTable(ctx, internalInput)
generateDo(ctx, internalInput)
generateEntity(ctx, internalInput)
in.genItems.SetClear(in.Clear)
}

View File

@ -13,6 +13,10 @@ import (
"github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
)
// doClear performs cleanup of stale generated files across all generation items.
// It collects all generated file paths from all items, then for each item with
// Clear enabled, removes any .go files in its directories that are NOT in the
// generated file list. This ensures files for dropped/removed tables are cleaned up.
func doClear(items *CGenDaoInternalGenItems) {
var allGeneratedFilePaths = make([]string, 0)
for _, item := range items.Items {
@ -29,6 +33,10 @@ func doClear(items *CGenDaoInternalGenItems) {
}
}
// doClearItem removes stale .go files for a single generation item.
// It scans all storage directories for .go files and deletes any file
// that is not in the allGeneratedFilePaths list (i.e., no longer corresponds
// to an existing database table).
func doClearItem(item CGenDaoInternalGenItem, allGeneratedFilePaths []string) {
var generatedFilePaths = make([]string, 0)
for _, dirPath := range item.StorageDirPaths {

View File

@ -26,6 +26,9 @@ import (
"github.com/gogf/gf/cmd/gf/v2/internal/utility/utils"
)
// generateDao generates dao files (index + internal) for all tables in the input.
// It creates the dao directory structure and iterates over each table to generate
// individual dao files via generateDaoSingle.
func generateDao(ctx context.Context, in CGenDaoInternalInput) {
var (
dirPathDao = gfile.Join(in.Path, in.DaoPath)
@ -48,21 +51,20 @@ func generateDao(ctx context.Context, in CGenDaoInternalInput) {
}
}
// generateDaoSingleInput holds all parameters needed to generate dao files for a single table.
type generateDaoSingleInput struct {
CGenDaoInternalInput
// TableName specifies the table name of the table.
TableName string
// NewTableName specifies the prefix-stripped or custom edited name of the table.
NewTableName string
DirPathDao string
DirPathDaoInternal string
IsSharding bool
TableName string // Original table name as it exists in the database.
NewTableName string // Processed table name after prefix removal and sharding.
DirPathDao string // Directory path for the dao index files.
DirPathDaoInternal string // Directory path for the dao internal implementation files.
IsSharding bool // Whether this table is a sharding table (merged from multiple physical tables).
}
// generateDaoSingle generates the dao and model content of given table.
func generateDaoSingle(ctx context.Context, in generateDaoSingleInput) {
// Generating table data preparing.
fieldMap, err := in.DB.TableFields(ctx, in.TableName)
fieldMap, err := getTableFields(ctx, in.CGenDaoInternalInput, in.TableName)
if err != nil {
mlog.Fatalf(`fetching tables fields failed for table "%s": %+v`, in.TableName, err)
}
@ -105,14 +107,21 @@ func generateDaoSingle(ctx context.Context, in generateDaoSingleInput) {
})
}
// generateDaoIndexInput holds parameters for generating the dao index file.
// The index file provides the public API (exported struct and constructor)
// for accessing the DAO, delegating to the internal implementation.
type generateDaoIndexInput struct {
generateDaoSingleInput
TableNameCamelCase string
TableNameCamelLowerCase string
ImportPrefix string
FileName string
TableNameCamelCase string // CamelCase version of the table name (e.g., "UserDetail").
TableNameCamelLowerCase string // camelCase version of the table name (e.g., "userDetail").
ImportPrefix string // Go import path prefix for the dao package.
FileName string // Output file name (without extension).
}
// generateDaoIndex generates the dao index file for a single table.
// The index file is the public-facing dao file that users import directly.
// It will NOT overwrite an existing file unless OverwriteDao is enabled,
// allowing users to customize the index file without losing changes.
func generateDaoIndex(in generateDaoIndexInput) {
path := filepath.FromSlash(gfile.Join(in.DirPathDao, in.FileName+".go"))
// It should add path to result slice whenever it would generate the path file or not.
@ -147,15 +156,21 @@ func generateDaoIndex(in generateDaoIndexInput) {
}
}
// generateDaoInternalInput holds parameters for generating the dao internal file.
// The internal file contains the actual DAO implementation with column definitions
// and is always overwritten on regeneration.
type generateDaoInternalInput struct {
generateDaoSingleInput
TableNameCamelCase string
TableNameCamelLowerCase string
ImportPrefix string
FileName string
FieldMap map[string]*gdb.TableField
TableNameCamelCase string // CamelCase version of the table name.
TableNameCamelLowerCase string // camelCase version of the table name.
ImportPrefix string // Go import path prefix for the dao package.
FileName string // Output file name (without extension).
FieldMap map[string]*gdb.TableField // Map of column name to field metadata.
}
// generateDaoInternal generates the dao internal implementation file for a single table.
// This file is always regenerated (overwritten) and contains the Columns struct definition
// with column name constants and their string value assignments.
func generateDaoInternal(in generateDaoInternalInput) {
var (
ctx = context.Background()

View File

@ -22,6 +22,10 @@ import (
"github.com/gogf/gf/cmd/gf/v2/internal/utility/utils"
)
// generateDo generates DO (Data Object) files for all tables.
// DO structs use "any" type for all scalar fields (replacing concrete types),
// enabling flexible query building with the g.Meta `orm:"do:true"` tag.
// Pointer, slice, and map types are preserved as-is.
func generateDo(ctx context.Context, in CGenDaoInternalInput) {
var dirPathDo = filepath.FromSlash(gfile.Join(in.Path, in.DoPath))
in.genItems.AppendDirPath(dirPathDo)
@ -30,7 +34,7 @@ func generateDo(ctx context.Context, in CGenDaoInternalInput) {
in.NoModelComment = false
// Model content.
for i, tableName := range in.TableNames {
fieldMap, err := in.DB.TableFields(ctx, tableName)
fieldMap, err := getTableFields(ctx, in, tableName)
if err != nil {
mlog.Fatalf("fetching tables fields failed for table '%s':\n%v", tableName, err)
}
@ -75,6 +79,9 @@ func generateDo(ctx context.Context, in CGenDaoInternalInput) {
}
}
// generateDoContent renders the DO file content using the template engine.
// It assembles template variables including package imports, struct definition,
// and metadata, then parses the DO template to produce the final file content.
func generateDoContent(
ctx context.Context, in CGenDaoInternalInput, tableName, tableNameCamelCase, structDefine string,
) string {

View File

@ -20,12 +20,15 @@ import (
"github.com/gogf/gf/cmd/gf/v2/internal/utility/utils"
)
// generateEntity generates entity struct files for all tables.
// Entity structs represent database table rows with concrete Go types,
// including orm tags for field-to-column mapping and json tags for serialization.
func generateEntity(ctx context.Context, in CGenDaoInternalInput) {
var dirPathEntity = gfile.Join(in.Path, in.EntityPath)
in.genItems.AppendDirPath(dirPathEntity)
// Model content.
for i, tableName := range in.TableNames {
fieldMap, err := in.DB.TableFields(ctx, tableName)
fieldMap, err := getTableFields(ctx, in, tableName)
if err != nil {
mlog.Fatalf("fetching tables fields failed for table '%s':\n%v", tableName, err)
}
@ -60,6 +63,9 @@ func generateEntity(ctx context.Context, in CGenDaoInternalInput) {
}
}
// generateEntityContent renders the entity file content using the template engine.
// It assembles template variables and parses the entity template to produce
// the final Go source file content with proper imports and struct definition.
func generateEntityContent(
ctx context.Context, in CGenDaoInternalInput, tableName, tableNameCamelCase, structDefine string, appendImports []string,
) string {

View File

@ -7,17 +7,25 @@
package gendao
type (
// CGenDaoInternalGenItems tracks generation state across multiple configuration entries.
// Each configuration entry (e.g., different database links in the config array)
// gets its own CGenDaoInternalGenItem via Scale(). The index field points to the
// current active item.
CGenDaoInternalGenItems struct {
index int
Items []CGenDaoInternalGenItem
index int // Index of the current active generation item.
Items []CGenDaoInternalGenItem // List of all generation items, one per config entry.
}
// CGenDaoInternalGenItem tracks generated files and directories for a single
// configuration entry. Used by the Clear feature to identify and remove stale files.
CGenDaoInternalGenItem struct {
Clear bool
StorageDirPaths []string
GeneratedFilePaths []string
Clear bool // Whether to clear stale files for this item.
StorageDirPaths []string // Directories where generated files are stored (dao, do, entity, table).
GeneratedFilePaths []string // All file paths generated in this run.
}
)
// newCGenDaoInternalGenItems creates a new generation items tracker with an empty item list.
func newCGenDaoInternalGenItems() *CGenDaoInternalGenItems {
return &CGenDaoInternalGenItems{
index: -1,
@ -25,6 +33,8 @@ func newCGenDaoInternalGenItems() *CGenDaoInternalGenItems {
}
}
// Scale adds a new generation item and advances the index to it.
// Must be called once per configuration entry before generating files.
func (i *CGenDaoInternalGenItems) Scale() {
i.Items = append(i.Items, CGenDaoInternalGenItem{
StorageDirPaths: make([]string, 0),
@ -34,10 +44,12 @@ func (i *CGenDaoInternalGenItems) Scale() {
i.index++
}
// SetClear enables or disables the clear (stale file removal) flag for the current item.
func (i *CGenDaoInternalGenItems) SetClear(clear bool) {
i.Items[i.index].Clear = clear
}
// AppendDirPath records a directory path used for storing generated files in the current item.
func (i *CGenDaoInternalGenItems) AppendDirPath(storageDirPath string) {
i.Items[i.index].StorageDirPaths = append(
i.Items[i.index].StorageDirPaths,
@ -45,6 +57,7 @@ func (i *CGenDaoInternalGenItems) AppendDirPath(storageDirPath string) {
)
}
// AppendGeneratedFilePath records a file path that was generated in the current item.
func (i *CGenDaoInternalGenItems) AppendGeneratedFilePath(generatedFilePath string) {
i.Items[i.index].GeneratedFilePaths = append(
i.Items[i.index].GeneratedFilePaths,

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,211 @@
// 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 gendao
import (
"fmt"
"strings"
"github.com/gogf/gf/v2/database/gdb"
)
// MSSQLParser implements SQLParser for SQL Server (T-SQL) DDL.
type MSSQLParser struct{}
// ParseCreateTable parses a single MSSQL CREATE TABLE statement.
func (p *MSSQLParser) ParseCreateTable(stmt string) (string, map[string]*gdb.TableField, error) {
body, _, ok := extractBodyAndTrailing(stmt)
if !ok {
return "", nil, nil
}
parenIdx := strings.Index(stmt, "(")
header := stmt[:parenIdx]
tableName := extractTableName(header)
if tableName == "" {
return "", nil, fmt.Errorf("cannot extract table name from: %s", header)
}
columnDefs := splitColumns(body)
fields := make(map[string]*gdb.TableField)
pkColumns := findPrimaryKeysFromConstraints(columnDefs)
fieldIndex := 0
for _, def := range columnDefs {
def = strings.TrimSpace(def)
if def == "" {
continue
}
firstWord := strings.ToUpper(strings.Fields(def)[0])
if isConstraintKeyword(firstWord) {
continue
}
field, err := p.parseColumnDef(def, fieldIndex)
if err != nil {
continue
}
if field != nil {
fields[field.Name] = field
fieldIndex++
}
}
for _, pkCol := range pkColumns {
if f, ok := fields[pkCol]; ok {
f.Key = "PRI"
}
}
return tableName, fields, nil
}
// ParseAlterTable parses MSSQL ALTER TABLE statements.
func (p *MSSQLParser) ParseAlterTable(stmt string, tables map[string]map[string]*gdb.TableField) error {
return parseAlterTableCommon(stmt, tables, p.parseColumnDef)
}
// ParseComment parses EXEC sp_addextendedproperty to extract column comments.
func (p *MSSQLParser) ParseComment(stmt string, tables map[string]map[string]*gdb.TableField) {
upper := strings.ToUpper(strings.TrimSpace(stmt))
if !strings.Contains(upper, "SP_ADDEXTENDEDPROPERTY") ||
!strings.Contains(upper, "MS_DESCRIPTION") {
return
}
// Extract quoted string values
var values []string
inQuote := false
var current strings.Builder
for i := 0; i < len(stmt); i++ {
ch := stmt[i]
if ch == '\'' {
if inQuote {
if i+1 < len(stmt) && stmt[i+1] == '\'' {
current.WriteByte('\'')
i++
continue
}
values = append(values, current.String())
current.Reset()
inQuote = false
} else {
inQuote = true
}
} else if inQuote {
current.WriteByte(ch)
}
}
if len(values) < 8 {
return
}
var (
comment string
tableName string
columnName string
)
for i := 0; i < len(values)-1; i++ {
switch strings.ToUpper(values[i]) {
case "MS_DESCRIPTION":
comment = values[i+1]
case "TABLE":
tableName = values[i+1]
case "COLUMN":
columnName = values[i+1]
}
}
if tableName != "" && columnName != "" && comment != "" {
if fields, ok := tables[tableName]; ok {
if field, ok := fields[columnName]; ok {
field.Comment = comment
}
}
}
}
// parseColumnDef parses a single MSSQL column definition string into a TableField.
// It handles MSSQL-specific syntax including bracket-quoted identifiers and
// type parameters like varchar(max).
func (p *MSSQLParser) parseColumnDef(def string, index int) (*gdb.TableField, error) {
tokens := mysqlTokenize(def)
if len(tokens) < 2 {
return nil, fmt.Errorf("invalid column definition: %s", def)
}
field := &gdb.TableField{
Index: index,
Name: unquoteIdentifier(tokens[0]),
Null: true,
}
field.Type = tokens[1]
rest := ""
if len(tokens) > 2 {
rest = strings.Join(tokens[2:], " ")
}
if !strings.Contains(field.Type, "(") && strings.HasPrefix(strings.TrimSpace(rest), "(") {
end := strings.Index(rest, ")")
if end >= 0 {
field.Type += rest[:end+1]
rest = strings.TrimSpace(rest[end+1:])
}
}
p.parseColumnAttributes(field, rest)
return field, nil
}
// parseColumnAttributes parses MSSQL column constraint keywords including
// NOT NULL, NULL, PRIMARY KEY, UNIQUE, IDENTITY (auto-increment), and DEFAULT.
func (p *MSSQLParser) parseColumnAttributes(field *gdb.TableField, attrs string) {
words := strings.Fields(attrs)
upperWords := strings.Fields(strings.ToUpper(attrs))
for i := 0; i < len(upperWords); i++ {
switch upperWords[i] {
case "NOT":
if i+1 < len(upperWords) && upperWords[i+1] == "NULL" {
field.Null = false
i++
}
case "NULL":
field.Null = true
case "PRIMARY":
if i+1 < len(upperWords) && upperWords[i+1] == "KEY" {
field.Key = "PRI"
i++
}
case "UNIQUE":
if field.Key == "" {
field.Key = "UNI"
}
case "IDENTITY":
field.Extra = "auto_increment"
if i+1 < len(words) && strings.HasPrefix(words[i+1], "(") {
i++
}
default:
if strings.HasPrefix(upperWords[i], "IDENTITY(") || strings.HasPrefix(upperWords[i], "IDENTITY (") {
field.Extra = "auto_increment"
}
case "DEFAULT":
if i+1 < len(words) {
defaultVal, _ := extractDefaultValue("DEFAULT " + strings.Join(words[i+1:], " "))
field.Default = defaultVal
if defaultVal != nil {
i++
}
}
}
}
}

View File

@ -0,0 +1,72 @@
// 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 gendao
import (
"testing"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/test/gtest"
)
func Test_MSSQL_CreateTable_Basic(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &MSSQLParser{}
sql := `
CREATE TABLE [dbo].[users] (
[id] INT IDENTITY(1,1) NOT NULL,
[name] NVARCHAR(100) NOT NULL,
[email] NVARCHAR(200) NULL,
[balance] DECIMAL(18,2) DEFAULT 0,
[created_at] DATETIME2 NOT NULL DEFAULT GETDATE(),
CONSTRAINT [PK_users] PRIMARY KEY CLUSTERED ([id])
);
EXEC sp_addextendedproperty 'MS_Description', 'User ID', 'SCHEMA', 'dbo', 'TABLE', 'users', 'COLUMN', 'id';
EXEC sp_addextendedproperty 'MS_Description', 'User name', 'SCHEMA', 'dbo', 'TABLE', 'users', 'COLUMN', 'name';
`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
fields := tables["users"]
t.Assert(len(fields), 5)
t.Assert(fields["id"].Extra, "auto_increment")
t.Assert(fields["id"].Null, false)
t.Assert(fields["id"].Key, "PRI")
t.Assert(fields["id"].Comment, "User ID")
t.Assert(fields["name"].Comment, "User name")
t.Assert(fields["name"].Null, false)
t.Assert(fields["email"].Null, true)
})
}
func Test_MSSQL_AlterTable(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &MSSQLParser{}
sql := `
CREATE TABLE users (
id INT IDENTITY(1,1) NOT NULL,
name NVARCHAR(100) NOT NULL,
CONSTRAINT PK_users PRIMARY KEY (id)
);
ALTER TABLE users ADD email NVARCHAR(200) NULL;
ALTER TABLE users DROP COLUMN name;
`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
fields := tables["users"]
t.Assert(len(fields), 2) // id, email
_, ok := fields["name"]
t.Assert(ok, false)
t.Assert(fields["email"].Null, true)
})
}

View File

@ -0,0 +1,199 @@
// 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 gendao
import (
"fmt"
"strings"
"github.com/gogf/gf/v2/database/gdb"
)
// MySQLParser implements SQLParser for MySQL/MariaDB/TiDB DDL.
type MySQLParser struct{}
// ParseCreateTable parses a single MySQL CREATE TABLE statement.
func (p *MySQLParser) ParseCreateTable(stmt string) (string, map[string]*gdb.TableField, error) {
body, trailing, ok := extractBodyAndTrailing(stmt)
if !ok {
return "", nil, nil
}
parenIdx := strings.Index(stmt, "(")
header := stmt[:parenIdx]
tableName := extractTableName(header)
if tableName == "" {
return "", nil, fmt.Errorf("cannot extract table name from: %s", header)
}
columnDefs := splitColumns(body)
fields := make(map[string]*gdb.TableField)
pkColumns := findPrimaryKeysFromConstraints(columnDefs)
fieldIndex := 0
for _, def := range columnDefs {
def = strings.TrimSpace(def)
if def == "" {
continue
}
firstWord := strings.ToUpper(strings.Fields(def)[0])
if isConstraintKeyword(firstWord) {
continue
}
field, err := p.parseColumnDef(def, fieldIndex)
if err != nil {
continue
}
if field != nil {
fields[field.Name] = field
fieldIndex++
}
}
for _, pkCol := range pkColumns {
if f, ok := fields[pkCol]; ok {
f.Key = "PRI"
}
}
// Extract inline comments from trailing table options (not used for field generation)
_ = trailing
return tableName, fields, nil
}
// ParseAlterTable parses MySQL ALTER TABLE statements.
func (p *MySQLParser) ParseAlterTable(stmt string, tables map[string]map[string]*gdb.TableField) error {
return parseAlterTableCommon(stmt, tables, p.parseColumnDef)
}
// ParseComment handles MySQL-style comments (inline COMMENT keyword is handled in parseColumnDef).
func (p *MySQLParser) ParseComment(stmt string, tables map[string]map[string]*gdb.TableField) {
// MySQL uses inline COMMENT 'xxx' in column definitions,
// which is already handled by parseColumnDef. No separate COMMENT ON statement.
}
// parseColumnDef parses a single MySQL column definition string into a TableField.
// It extracts the column name, data type (including UNSIGNED modifier), and delegates
// attribute parsing (NULL, DEFAULT, PRIMARY KEY, COMMENT, etc.) to parseColumnAttributes.
func (p *MySQLParser) parseColumnDef(def string, index int) (*gdb.TableField, error) {
tokens := mysqlTokenize(def)
if len(tokens) < 2 {
return nil, fmt.Errorf("invalid column definition: %s", def)
}
field := &gdb.TableField{
Index: index,
Name: unquoteIdentifier(tokens[0]),
Null: true,
}
typeStr := tokens[1]
rest := ""
if len(tokens) > 2 {
rest = strings.Join(tokens[2:], " ")
}
// Check if rest starts with '(' meaning the type params are in rest
if !strings.Contains(typeStr, "(") && strings.HasPrefix(strings.TrimSpace(rest), "(") {
endParen := strings.Index(rest, ")")
if endParen >= 0 {
typeStr += rest[:endParen+1]
rest = strings.TrimSpace(rest[endParen+1:])
}
}
field.Type = typeStr
// Handle UNSIGNED
upperRest := strings.ToUpper(rest)
if strings.HasPrefix(upperRest, "UNSIGNED") {
field.Type += " unsigned"
rest = strings.TrimSpace(rest[8:])
}
p.parseColumnAttributes(field, rest)
return field, nil
}
// parseColumnAttributes parses MySQL column constraint keywords from the attribute string
// following the column type. It handles NOT NULL, NULL, PRIMARY KEY, UNIQUE, AUTO_INCREMENT,
// DEFAULT, COMMENT, and ON UPDATE clauses.
func (p *MySQLParser) parseColumnAttributes(field *gdb.TableField, attrs string) {
words := strings.Fields(attrs)
upperWords := strings.Fields(strings.ToUpper(attrs))
for i := 0; i < len(upperWords); i++ {
switch upperWords[i] {
case "NOT":
if i+1 < len(upperWords) && upperWords[i+1] == "NULL" {
field.Null = false
i++
}
case "NULL":
field.Null = true
case "PRIMARY":
if i+1 < len(upperWords) && upperWords[i+1] == "KEY" {
field.Key = "PRI"
i++
}
case "UNIQUE":
if field.Key == "" {
field.Key = "UNI"
}
if i+1 < len(upperWords) && upperWords[i+1] == "KEY" {
i++
}
case "KEY":
if field.Key == "" {
field.Key = "MUL"
}
case "AUTO_INCREMENT":
field.Extra = "auto_increment"
case "DEFAULT":
if i+1 < len(words) {
defaultVal, _ := extractDefaultValue("DEFAULT " + strings.Join(words[i+1:], " "))
field.Default = defaultVal
if defaultVal != nil {
if strings.HasPrefix(words[i+1], "'") {
for j := i + 1; j < len(words); j++ {
if strings.HasSuffix(words[j], "'") {
i = j
break
}
}
} else {
i++
}
}
}
case "COMMENT":
if i+1 < len(words) {
comment := strings.Join(words[i+1:], " ")
comment = strings.TrimSpace(comment)
if len(comment) >= 2 && comment[0] == '\'' && comment[len(comment)-1] == '\'' {
comment = comment[1 : len(comment)-1]
comment = strings.ReplaceAll(comment, "''", "'")
}
field.Comment = comment
return
}
case "ON":
if i+1 < len(upperWords) && upperWords[i+1] == "UPDATE" {
if i+2 < len(upperWords) {
if field.Extra != "" {
field.Extra += ", "
}
field.Extra += "on update " + strings.ToLower(words[i+2])
i += 2
}
}
}
}
}

View File

@ -0,0 +1,300 @@
// 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 gendao
import (
"testing"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/test/gtest"
)
func Test_MySQL_CreateTable_Basic(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &MySQLParser{}
sql := `
CREATE TABLE users (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'User ID',
name VARCHAR(100) NOT NULL DEFAULT '' COMMENT 'User name',
email VARCHAR(200) NULL COMMENT 'Email address',
age INT(11) DEFAULT 0,
score DECIMAL(10,2) DEFAULT 0.00,
status TINYINT(1) NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='User table';
`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
t.Assert(len(tables), 1)
fields := tables["users"]
t.Assert(len(fields), 8)
// Check id field
t.Assert(fields["id"].Name, "id")
t.Assert(fields["id"].Type, "BIGINT(20) unsigned")
t.Assert(fields["id"].Null, false)
t.Assert(fields["id"].Key, "PRI")
t.Assert(fields["id"].Extra, "auto_increment")
t.Assert(fields["id"].Comment, "User ID")
t.Assert(fields["id"].Index, 0)
// Check name field
t.Assert(fields["name"].Name, "name")
t.Assert(fields["name"].Null, false)
t.Assert(fields["name"].Comment, "User name")
// Check email field
t.Assert(fields["email"].Null, true)
// Check created_at
t.Assert(fields["created_at"].Null, false)
})
}
func Test_MySQL_AlterTable_AddColumn(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &MySQLParser{}
sql := `
CREATE TABLE users (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
PRIMARY KEY (id)
);
ALTER TABLE users ADD COLUMN email VARCHAR(200) NULL COMMENT 'Email';
ALTER TABLE users ADD COLUMN age INT DEFAULT 0;
`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
fields := tables["users"]
t.Assert(len(fields), 4)
t.Assert(fields["email"].Name, "email")
t.Assert(fields["email"].Null, true)
t.Assert(fields["email"].Comment, "Email")
t.Assert(fields["age"].Name, "age")
})
}
func Test_MySQL_AlterTable_DropColumn(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &MySQLParser{}
sql := `
CREATE TABLE users (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(100),
old_field VARCHAR(50),
PRIMARY KEY (id)
);
ALTER TABLE users DROP COLUMN old_field;
`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
fields := tables["users"]
t.Assert(len(fields), 2)
_, ok := fields["old_field"]
t.Assert(ok, false)
})
}
func Test_MySQL_AlterTable_ModifyColumn(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &MySQLParser{}
sql := `
CREATE TABLE users (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(100),
PRIMARY KEY (id)
);
ALTER TABLE users MODIFY COLUMN name VARCHAR(200) NOT NULL COMMENT 'Full name';
`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
fields := tables["users"]
t.Assert(fields["name"].Type, "VARCHAR(200)")
t.Assert(fields["name"].Null, false)
t.Assert(fields["name"].Comment, "Full name")
})
}
func Test_MySQL_AlterTable_ChangeColumn(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &MySQLParser{}
sql := `
CREATE TABLE users (
id INT NOT NULL AUTO_INCREMENT,
old_name VARCHAR(100),
PRIMARY KEY (id)
);
ALTER TABLE users CHANGE COLUMN old_name new_name VARCHAR(200) NOT NULL;
`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
fields := tables["users"]
_, ok := fields["old_name"]
t.Assert(ok, false)
t.Assert(fields["new_name"].Name, "new_name")
t.Assert(fields["new_name"].Type, "VARCHAR(200)")
})
}
func Test_MySQL_AlterTable_AddPrimaryKey(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &MySQLParser{}
sql := `
CREATE TABLE users (
id INT NOT NULL,
name VARCHAR(100)
);
ALTER TABLE users ADD PRIMARY KEY (id);
`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
t.Assert(tables["users"]["id"].Key, "PRI")
})
}
func Test_MySQL_DropTable(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &MySQLParser{}
sql := `
CREATE TABLE temp_log (id INT, msg TEXT);
CREATE TABLE users (id INT, name VARCHAR(100));
DROP TABLE IF EXISTS temp_log;
`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
t.Assert(len(tables), 1)
_, ok := tables["temp_log"]
t.Assert(ok, false)
_, ok = tables["users"]
t.Assert(ok, true)
})
}
func Test_MySQL_MultipleMigrations(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &MySQLParser{}
// Simulate V1: initial schema
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, `
CREATE TABLE users (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
PRIMARY KEY (id)
);
`, tables)
t.AssertNil(err)
// Simulate V2: add columns
err = processSQL(parser, `
ALTER TABLE users ADD COLUMN email VARCHAR(200) NULL;
ALTER TABLE users ADD COLUMN phone VARCHAR(20) NULL;
`, tables)
t.AssertNil(err)
// Simulate V3: modify + drop
err = processSQL(parser, `
ALTER TABLE users MODIFY COLUMN name VARCHAR(100) NOT NULL COMMENT 'Full name';
ALTER TABLE users DROP COLUMN phone;
`, tables)
t.AssertNil(err)
fields := tables["users"]
t.Assert(len(fields), 3) // id, name, email
t.Assert(fields["name"].Type, "VARCHAR(100)")
t.Assert(fields["name"].Comment, "Full name")
_, ok := fields["phone"]
t.Assert(ok, false)
t.Assert(fields["email"].Null, true)
})
}
func Test_MySQL_FullMigrationScenario(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &MySQLParser{}
tables := make(map[string]map[string]*gdb.TableField)
// V001: Initial tables
err := processSQL(parser, `
CREATE TABLE IF NOT EXISTS users (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Primary key',
username VARCHAR(50) NOT NULL COMMENT 'Username',
password VARCHAR(128) NOT NULL COMMENT 'Hashed password',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_username (username)
);
CREATE TABLE IF NOT EXISTS orders (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
amount DECIMAL(10,2) NOT NULL DEFAULT 0.00,
PRIMARY KEY (id)
);
`, tables)
t.AssertNil(err)
t.Assert(len(tables), 2)
// V002: Add email, phone
err = processSQL(parser, `
ALTER TABLE users ADD COLUMN email VARCHAR(200) NULL COMMENT 'User email';
ALTER TABLE users ADD COLUMN phone VARCHAR(20) NULL COMMENT 'Phone number';
`, tables)
t.AssertNil(err)
t.Assert(len(tables["users"]), 6)
// V003: Modify, rename, drop
err = processSQL(parser, `
ALTER TABLE users MODIFY COLUMN username VARCHAR(100) NOT NULL COMMENT 'Login name';
ALTER TABLE users CHANGE COLUMN phone mobile VARCHAR(20) NULL COMMENT 'Mobile number';
ALTER TABLE users DROP COLUMN password;
ALTER TABLE orders ADD COLUMN status TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Order status';
`, tables)
t.AssertNil(err)
userFields := tables["users"]
t.Assert(len(userFields), 5) // id, username, email, mobile, created_at
t.Assert(userFields["username"].Type, "VARCHAR(100)")
t.Assert(userFields["username"].Comment, "Login name")
_, ok := userFields["password"]
t.Assert(ok, false)
_, ok = userFields["phone"]
t.Assert(ok, false)
t.Assert(userFields["mobile"].Name, "mobile")
t.Assert(userFields["mobile"].Comment, "Mobile number")
orderFields := tables["orders"]
t.Assert(len(orderFields), 4)
t.Assert(orderFields["status"].Default, "0")
// V004: Drop table
err = processSQL(parser, `
DROP TABLE IF EXISTS orders;
`, tables)
t.AssertNil(err)
t.Assert(len(tables), 1)
_, ok = tables["orders"]
t.Assert(ok, false)
})
}

View File

@ -0,0 +1,209 @@
// 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 gendao
import (
"fmt"
"strings"
"github.com/gogf/gf/v2/database/gdb"
)
// OracleParser implements SQLParser for Oracle/DM DDL.
type OracleParser struct{}
// ParseCreateTable parses a single Oracle CREATE TABLE statement.
func (p *OracleParser) ParseCreateTable(stmt string) (string, map[string]*gdb.TableField, error) {
body, _, ok := extractBodyAndTrailing(stmt)
if !ok {
return "", nil, nil
}
parenIdx := strings.Index(stmt, "(")
header := stmt[:parenIdx]
tableName := extractTableName(header)
if tableName == "" {
return "", nil, fmt.Errorf("cannot extract table name from: %s", header)
}
columnDefs := splitColumns(body)
fields := make(map[string]*gdb.TableField)
pkColumns := findPrimaryKeysFromConstraints(columnDefs)
fieldIndex := 0
for _, def := range columnDefs {
def = strings.TrimSpace(def)
if def == "" {
continue
}
firstWord := strings.ToUpper(strings.Fields(def)[0])
if isConstraintKeyword(firstWord) {
continue
}
field, err := p.parseColumnDef(def, fieldIndex)
if err != nil {
continue
}
if field != nil {
fields[field.Name] = field
fieldIndex++
}
}
for _, pkCol := range pkColumns {
if f, ok := fields[pkCol]; ok {
f.Key = "PRI"
}
upperPk := strings.ToUpper(pkCol)
if f, ok := fields[upperPk]; ok {
f.Key = "PRI"
}
}
return tableName, fields, nil
}
// ParseAlterTable parses Oracle ALTER TABLE statements.
func (p *OracleParser) ParseAlterTable(stmt string, tables map[string]map[string]*gdb.TableField) error {
return parseAlterTableCommon(stmt, tables, p.parseColumnDef)
}
// ParseComment parses COMMENT ON COLUMN table.column IS 'comment'.
func (p *OracleParser) ParseComment(stmt string, tables map[string]map[string]*gdb.TableField) {
upper := strings.ToUpper(strings.TrimSpace(stmt))
if !strings.HasPrefix(upper, "COMMENT ON COLUMN") {
return
}
rest := strings.TrimSpace(stmt[len("COMMENT ON COLUMN"):])
isIdx := strings.Index(strings.ToUpper(rest), " IS ")
if isIdx < 0 {
return
}
ref := strings.TrimSpace(rest[:isIdx])
comment := strings.TrimSpace(rest[isIdx+4:])
if len(comment) >= 2 && comment[0] == '\'' && comment[len(comment)-1] == '\'' {
comment = comment[1 : len(comment)-1]
comment = strings.ReplaceAll(comment, "''", "'")
}
parts := strings.Split(ref, ".")
var tableName, columnName string
switch len(parts) {
case 2:
tableName = unquoteIdentifier(parts[0])
columnName = unquoteIdentifier(parts[1])
case 3:
tableName = unquoteIdentifier(parts[1])
columnName = unquoteIdentifier(parts[2])
default:
return
}
if fields, ok := tables[tableName]; ok {
if field, ok := fields[columnName]; ok {
field.Comment = comment
}
}
}
// parseColumnDef parses a single Oracle column definition string into a TableField.
// It handles Oracle-specific types including TIMESTAMP WITH TIME ZONE and
// TIMESTAMP WITH LOCAL TIME ZONE.
func (p *OracleParser) parseColumnDef(def string, index int) (*gdb.TableField, error) {
tokens := mysqlTokenize(def)
if len(tokens) < 2 {
return nil, fmt.Errorf("invalid column definition: %s", def)
}
field := &gdb.TableField{
Index: index,
Name: unquoteIdentifier(tokens[0]),
Null: true,
}
field.Type = tokens[1]
rest := ""
if len(tokens) > 2 {
rest = strings.Join(tokens[2:], " ")
}
if !strings.Contains(field.Type, "(") && strings.HasPrefix(strings.TrimSpace(rest), "(") {
end := strings.Index(rest, ")")
if end >= 0 {
field.Type += rest[:end+1]
rest = strings.TrimSpace(rest[end+1:])
}
}
// Handle TIMESTAMP WITH TIME ZONE / WITH LOCAL TIME ZONE
upperType := strings.ToUpper(field.Type)
upperRest := strings.ToUpper(rest)
if upperType == "TIMESTAMP" {
if strings.HasPrefix(upperRest, "WITH LOCAL TIME ZONE") {
field.Type = "timestamp with local time zone"
rest = strings.TrimSpace(rest[len("WITH LOCAL TIME ZONE"):])
} else if strings.HasPrefix(upperRest, "WITH TIME ZONE") {
field.Type = "timestamp with time zone"
rest = strings.TrimSpace(rest[len("WITH TIME ZONE"):])
}
}
p.parseColumnAttributes(field, rest)
return field, nil
}
// parseColumnAttributes parses Oracle column constraint keywords including
// NOT NULL, NULL, PRIMARY KEY, UNIQUE, DEFAULT, and GENERATED ... AS IDENTITY.
func (p *OracleParser) parseColumnAttributes(field *gdb.TableField, attrs string) {
words := strings.Fields(attrs)
upperWords := strings.Fields(strings.ToUpper(attrs))
for i := 0; i < len(upperWords); i++ {
switch upperWords[i] {
case "NOT":
if i+1 < len(upperWords) && upperWords[i+1] == "NULL" {
field.Null = false
i++
}
case "NULL":
field.Null = true
case "PRIMARY":
if i+1 < len(upperWords) && upperWords[i+1] == "KEY" {
field.Key = "PRI"
i++
}
case "UNIQUE":
if field.Key == "" {
field.Key = "UNI"
}
case "DEFAULT":
if i+1 < len(words) {
defaultVal, _ := extractDefaultValue("DEFAULT " + strings.Join(words[i+1:], " "))
field.Default = defaultVal
if defaultVal != nil {
i++
}
}
case "GENERATED":
rest := strings.Join(upperWords[i:], " ")
if strings.Contains(rest, "AS IDENTITY") {
field.Extra = "auto_increment"
for j := i + 1; j < len(upperWords); j++ {
if upperWords[j] == "IDENTITY" {
i = j
break
}
}
}
}
}
}

View File

@ -0,0 +1,97 @@
// 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 gendao
import (
"testing"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/test/gtest"
)
func Test_Oracle_CreateTable_Basic(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &OracleParser{}
sql := `
CREATE TABLE users (
ID NUMBER(10) NOT NULL,
NAME VARCHAR2(100) NOT NULL,
EMAIL VARCHAR2(200),
CREATED_AT TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP,
CONSTRAINT PK_USERS PRIMARY KEY (ID)
);
COMMENT ON COLUMN users.ID IS 'User ID';
COMMENT ON COLUMN users.NAME IS 'User name';
`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
fields := tables["users"]
t.Assert(len(fields), 4)
t.Assert(fields["ID"].Key, "PRI")
t.Assert(fields["ID"].Null, false)
t.Assert(fields["ID"].Comment, "User ID")
t.Assert(fields["NAME"].Null, false)
t.Assert(fields["NAME"].Comment, "User name")
t.Assert(fields["CREATED_AT"].Type, "timestamp with time zone")
})
}
func Test_Oracle_AlterTable(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &OracleParser{}
sql := `
CREATE TABLE users (
ID NUMBER(10) NOT NULL,
NAME VARCHAR2(100),
CONSTRAINT PK_USERS PRIMARY KEY (ID)
);
ALTER TABLE users ADD EMAIL VARCHAR2(200);
ALTER TABLE users MODIFY NAME VARCHAR2(200) NOT NULL;
COMMENT ON COLUMN users.EMAIL IS 'Email address';
`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
fields := tables["users"]
t.Assert(len(fields), 3)
t.Assert(fields["EMAIL"].Comment, "Email address")
t.Assert(fields["NAME"].Type, "VARCHAR2(200)")
t.Assert(fields["NAME"].Null, false)
})
}
func Test_Oracle_AlterTable_DropColumn(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &OracleParser{}
sql := `
CREATE TABLE users (
ID NUMBER(10) NOT NULL,
NAME VARCHAR2(100) NOT NULL,
OLD_COL VARCHAR2(50),
EMAIL VARCHAR2(200),
CONSTRAINT PK_USERS PRIMARY KEY (ID)
);
ALTER TABLE users DROP COLUMN OLD_COL;
`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
fields := tables["users"]
t.Assert(len(fields), 3)
_, ok := fields["OLD_COL"]
t.Assert(ok, false)
t.Assert(fields["NAME"].Name, "NAME")
t.Assert(fields["EMAIL"].Name, "EMAIL")
})
}

View File

@ -0,0 +1,268 @@
// 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 gendao
import (
"fmt"
"strings"
"github.com/gogf/gf/v2/database/gdb"
)
// PgSQLParser implements SQLParser for PostgreSQL DDL.
type PgSQLParser struct{}
// ParseCreateTable parses a single PostgreSQL CREATE TABLE statement.
func (p *PgSQLParser) ParseCreateTable(stmt string) (string, map[string]*gdb.TableField, error) {
body, _, ok := extractBodyAndTrailing(stmt)
if !ok {
return "", nil, nil
}
parenIdx := strings.Index(stmt, "(")
header := stmt[:parenIdx]
tableName := extractTableName(header)
if tableName == "" {
return "", nil, fmt.Errorf("cannot extract table name from: %s", header)
}
columnDefs := splitColumns(body)
fields := make(map[string]*gdb.TableField)
pkColumns := findPrimaryKeysFromConstraints(columnDefs)
fieldIndex := 0
for _, def := range columnDefs {
def = strings.TrimSpace(def)
if def == "" {
continue
}
firstWord := strings.ToUpper(strings.Fields(def)[0])
if isConstraintKeyword(firstWord) {
continue
}
field, err := p.parseColumnDef(def, fieldIndex)
if err != nil {
continue
}
if field != nil {
fields[field.Name] = field
fieldIndex++
}
}
for _, pkCol := range pkColumns {
if f, ok := fields[pkCol]; ok {
f.Key = "PRI"
}
}
return tableName, fields, nil
}
// ParseAlterTable parses PostgreSQL ALTER TABLE statements.
func (p *PgSQLParser) ParseAlterTable(stmt string, tables map[string]map[string]*gdb.TableField) error {
return parseAlterTableCommon(stmt, tables, p.parseColumnDef)
}
// ParseComment parses COMMENT ON COLUMN schema.table.column IS 'comment' statements.
func (p *PgSQLParser) ParseComment(stmt string, tables map[string]map[string]*gdb.TableField) {
upper := strings.ToUpper(strings.TrimSpace(stmt))
if !strings.HasPrefix(upper, "COMMENT ON COLUMN") {
return
}
rest := strings.TrimSpace(stmt[len("COMMENT ON COLUMN"):])
isIdx := strings.Index(strings.ToUpper(rest), " IS ")
if isIdx < 0 {
return
}
ref := strings.TrimSpace(rest[:isIdx])
comment := strings.TrimSpace(rest[isIdx+4:])
if len(comment) >= 2 && comment[0] == '\'' && comment[len(comment)-1] == '\'' {
comment = comment[1 : len(comment)-1]
comment = strings.ReplaceAll(comment, "''", "'")
}
parts := strings.Split(ref, ".")
var tableName, columnName string
switch len(parts) {
case 2:
tableName = unquoteIdentifier(parts[0])
columnName = unquoteIdentifier(parts[1])
case 3:
tableName = unquoteIdentifier(parts[1])
columnName = unquoteIdentifier(parts[2])
default:
return
}
if fields, ok := tables[tableName]; ok {
if field, ok := fields[columnName]; ok {
field.Comment = comment
}
}
}
// parseColumnDef parses a single PostgreSQL column definition string into a TableField.
// It handles PostgreSQL-specific types like SERIAL/BIGSERIAL (auto-increment shorthand),
// CHARACTER VARYING, DOUBLE PRECISION, TIMESTAMP WITH TIME ZONE, and array types.
func (p *PgSQLParser) parseColumnDef(def string, index int) (*gdb.TableField, error) {
tokens := mysqlTokenize(def)
if len(tokens) < 2 {
return nil, fmt.Errorf("invalid column definition: %s", def)
}
field := &gdb.TableField{
Index: index,
Name: unquoteIdentifier(tokens[0]),
Null: true,
}
// Handle SERIAL types
typeToken := strings.ToUpper(tokens[1])
switch typeToken {
case "SERIAL":
field.Type = "int"
field.Extra = "auto_increment"
field.Null = false
case "BIGSERIAL":
field.Type = "bigint"
field.Extra = "auto_increment"
field.Null = false
case "SMALLSERIAL":
field.Type = "smallint"
field.Extra = "auto_increment"
field.Null = false
default:
field.Type = tokens[1]
}
rest := ""
if len(tokens) > 2 {
rest = strings.Join(tokens[2:], " ")
}
upperType := strings.ToUpper(field.Type)
upperRest := strings.ToUpper(rest)
switch {
case upperType == "CHARACTER" && strings.HasPrefix(upperRest, "VARYING"):
rest = strings.TrimSpace(rest[len("VARYING"):])
if strings.HasPrefix(rest, "(") {
end := strings.Index(rest, ")")
if end >= 0 {
field.Type = "character varying" + rest[:end+1]
rest = strings.TrimSpace(rest[end+1:])
}
} else {
field.Type = "character varying"
}
case upperType == "DOUBLE" && strings.HasPrefix(upperRest, "PRECISION"):
field.Type = "double precision"
rest = strings.TrimSpace(rest[len("PRECISION"):])
case (upperType == "TIMESTAMP" || upperType == "TIME") &&
(strings.HasPrefix(upperRest, "WITH TIME ZONE") || strings.HasPrefix(upperRest, "WITHOUT TIME ZONE")):
if strings.HasPrefix(upperRest, "WITH TIME ZONE") {
if upperType == "TIMESTAMP" {
field.Type = "timestamptz"
} else {
field.Type = "time with time zone"
}
rest = strings.TrimSpace(rest[len("WITH TIME ZONE"):])
} else {
field.Type = strings.ToLower(upperType)
rest = strings.TrimSpace(rest[len("WITHOUT TIME ZONE"):])
}
case !strings.Contains(field.Type, "(") && strings.HasPrefix(strings.TrimSpace(rest), "("):
end := strings.Index(rest, ")")
if end >= 0 {
field.Type += rest[:end+1]
rest = strings.TrimSpace(rest[end+1:])
}
}
// Handle array types
if strings.HasPrefix(rest, "[]") {
field.Type += "[]"
rest = strings.TrimSpace(rest[2:])
} else if strings.HasPrefix(strings.ToUpper(rest), "ARRAY") {
field.Type += "[]"
rest = strings.TrimSpace(rest[5:])
}
p.parseColumnAttributes(field, rest)
return field, nil
}
// parseColumnAttributes parses PostgreSQL column constraint keywords including
// NOT NULL, NULL, PRIMARY KEY, UNIQUE, DEFAULT, GENERATED ... AS IDENTITY, and REFERENCES.
func (p *PgSQLParser) parseColumnAttributes(field *gdb.TableField, attrs string) {
words := strings.Fields(attrs)
upperWords := strings.Fields(strings.ToUpper(attrs))
for i := 0; i < len(upperWords); i++ {
switch upperWords[i] {
case "NOT":
if i+1 < len(upperWords) && upperWords[i+1] == "NULL" {
field.Null = false
i++
}
case "NULL":
field.Null = true
case "PRIMARY":
if i+1 < len(upperWords) && upperWords[i+1] == "KEY" {
field.Key = "PRI"
i++
}
case "UNIQUE":
if field.Key == "" {
field.Key = "UNI"
}
case "DEFAULT":
if i+1 < len(words) {
defaultVal, _ := extractDefaultValue("DEFAULT " + strings.Join(words[i+1:], " "))
field.Default = defaultVal
if defaultVal != nil {
i++
}
}
case "GENERATED":
if containsSequence(upperWords[i:], "ALWAYS", "AS", "IDENTITY") ||
containsSequence(upperWords[i:], "BY", "DEFAULT", "AS", "IDENTITY") {
field.Extra = "auto_increment"
for j := i + 1; j < len(upperWords); j++ {
if upperWords[j] == "IDENTITY" {
i = j
break
}
}
}
case "REFERENCES":
for j := i + 1; j < len(upperWords); j++ {
i = j
if strings.Contains(words[j], ")") {
break
}
}
}
}
}
// containsSequence checks if words slice contains the given word sequence starting from index 1.
func containsSequence(words []string, seq ...string) bool {
if len(words) < len(seq)+1 {
return false
}
for i, s := range seq {
if words[i+1] != s {
return false
}
}
return true
}

View File

@ -0,0 +1,232 @@
// 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 gendao
import (
"testing"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/test/gtest"
)
func Test_PgSQL_CreateTable_Basic(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &PgSQLParser{}
sql := `
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email CHARACTER VARYING(200),
score DOUBLE PRECISION DEFAULT 0.0,
metadata JSONB,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN NOT NULL DEFAULT TRUE
);
COMMENT ON COLUMN users.name IS 'User full name';
COMMENT ON COLUMN users.email IS 'Email address';
`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
fields := tables["users"]
t.Assert(len(fields), 7)
// BIGSERIAL should be auto_increment bigint
t.Assert(fields["id"].Type, "bigint")
t.Assert(fields["id"].Extra, "auto_increment")
t.Assert(fields["id"].Key, "PRI")
// CHARACTER VARYING
t.AssertNE(fields["email"], nil)
// DOUBLE PRECISION
t.Assert(fields["score"].Type, "double precision")
// JSONB
t.Assert(fields["metadata"].Type, "JSONB")
// TIMESTAMP WITH TIME ZONE
t.Assert(fields["created_at"].Type, "timestamptz")
// COMMENT ON COLUMN
t.Assert(fields["name"].Comment, "User full name")
t.Assert(fields["email"].Comment, "Email address")
})
}
func Test_PgSQL_AlterTable_AddColumn(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &PgSQLParser{}
sql := `
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL
);
ALTER TABLE users ADD COLUMN email VARCHAR(200);
COMMENT ON COLUMN users.email IS 'User email';
`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
fields := tables["users"]
t.Assert(len(fields), 3)
t.Assert(fields["email"].Name, "email")
t.Assert(fields["email"].Comment, "User email")
})
}
func Test_PgSQL_AlterTable_AlterColumnType(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &PgSQLParser{}
sql := `
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100)
);
ALTER TABLE users ALTER COLUMN name TYPE VARCHAR(200);
ALTER TABLE users ALTER COLUMN name SET NOT NULL;
`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
fields := tables["users"]
t.Assert(fields["name"].Type, "VARCHAR(200)")
t.Assert(fields["name"].Null, false)
})
}
func Test_PgSQL_AlterTable_DropColumn(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &PgSQLParser{}
sql := `
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
old_col TEXT
);
ALTER TABLE users DROP COLUMN old_col;
`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
fields := tables["users"]
t.Assert(len(fields), 2)
_, ok := fields["old_col"]
t.Assert(ok, false)
})
}
func Test_PgSQL_AlterTable_RenameColumn(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &PgSQLParser{}
sql := `
CREATE TABLE users (
id SERIAL PRIMARY KEY,
old_name VARCHAR(100)
);
ALTER TABLE users RENAME COLUMN old_name TO new_name;
`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
fields := tables["users"]
_, ok := fields["old_name"]
t.Assert(ok, false)
t.Assert(fields["new_name"].Name, "new_name")
})
}
func Test_PgSQL_MultipleMigrations(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &PgSQLParser{}
tables := make(map[string]map[string]*gdb.TableField)
// V1
err := processSQL(parser, `
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
price NUMERIC(10,2) DEFAULT 0.00
);
`, tables)
t.AssertNil(err)
// V2: add, alter, comment
err = processSQL(parser, `
ALTER TABLE products ADD COLUMN category VARCHAR(50);
ALTER TABLE products ALTER COLUMN name TYPE VARCHAR(200);
ALTER TABLE products ALTER COLUMN name SET NOT NULL;
COMMENT ON COLUMN products.category IS 'Product category';
`, tables)
t.AssertNil(err)
// V3: rename, drop
err = processSQL(parser, `
ALTER TABLE products RENAME COLUMN category TO product_category;
`, tables)
t.AssertNil(err)
fields := tables["products"]
t.Assert(len(fields), 4)
t.Assert(fields["name"].Type, "VARCHAR(200)")
t.Assert(fields["name"].Null, false)
_, ok := fields["category"]
t.Assert(ok, false)
t.Assert(fields["product_category"].Name, "product_category")
t.Assert(fields["product_category"].Comment, "Product category")
})
}
func Test_PgSQL_FullMigrationScenario(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &PgSQLParser{}
tables := make(map[string]map[string]*gdb.TableField)
// V001: Initial
err := processSQL(parser, `
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(200) UNIQUE
);
COMMENT ON COLUMN users.name IS 'User name';
`, tables)
t.AssertNil(err)
// V002: Add, alter type, set not null
err = processSQL(parser, `
ALTER TABLE users ADD COLUMN avatar TEXT;
ALTER TABLE users ALTER COLUMN name TYPE VARCHAR(200);
ALTER TABLE users ALTER COLUMN email SET NOT NULL;
COMMENT ON COLUMN users.avatar IS 'Avatar URL';
`, tables)
t.AssertNil(err)
fields := tables["users"]
t.Assert(len(fields), 4)
t.Assert(fields["name"].Type, "VARCHAR(200)")
t.Assert(fields["email"].Null, false)
t.Assert(fields["avatar"].Comment, "Avatar URL")
// V003: Rename column, drop not null
err = processSQL(parser, `
ALTER TABLE users RENAME COLUMN avatar TO profile_image;
ALTER TABLE users ALTER COLUMN email DROP NOT NULL;
`, tables)
t.AssertNil(err)
_, ok := fields["avatar"]
t.Assert(ok, false)
t.Assert(fields["profile_image"].Name, "profile_image")
t.Assert(fields["email"].Null, true)
})
}

View File

@ -0,0 +1,159 @@
// 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 gendao
import (
"fmt"
"strings"
"github.com/gogf/gf/v2/database/gdb"
)
// SQLiteParser implements SQLParser for SQLite DDL.
type SQLiteParser struct{}
// ParseCreateTable parses a single SQLite CREATE TABLE statement.
func (p *SQLiteParser) ParseCreateTable(stmt string) (string, map[string]*gdb.TableField, error) {
body, _, ok := extractBodyAndTrailing(stmt)
if !ok {
return "", nil, nil
}
parenIdx := strings.Index(stmt, "(")
header := stmt[:parenIdx]
tableName := extractTableName(header)
if tableName == "" {
return "", nil, fmt.Errorf("cannot extract table name from: %s", header)
}
columnDefs := splitColumns(body)
fields := make(map[string]*gdb.TableField)
pkColumns := findPrimaryKeysFromConstraints(columnDefs)
fieldIndex := 0
for _, def := range columnDefs {
def = strings.TrimSpace(def)
if def == "" {
continue
}
firstWord := strings.ToUpper(strings.Fields(def)[0])
if isConstraintKeyword(firstWord) {
continue
}
field, err := p.parseColumnDef(def, fieldIndex)
if err != nil {
continue
}
if field != nil {
fields[field.Name] = field
fieldIndex++
}
}
for _, pkCol := range pkColumns {
if f, ok := fields[pkCol]; ok {
f.Key = "PRI"
}
}
return tableName, fields, nil
}
// ParseAlterTable parses SQLite ALTER TABLE statements.
// Note: SQLite only supports ADD COLUMN and RENAME COLUMN in ALTER TABLE.
func (p *SQLiteParser) ParseAlterTable(stmt string, tables map[string]map[string]*gdb.TableField) error {
return parseAlterTableCommon(stmt, tables, p.parseColumnDef)
}
// ParseComment is a no-op for SQLite as it doesn't support COMMENT ON statements.
func (p *SQLiteParser) ParseComment(stmt string, tables map[string]map[string]*gdb.TableField) {
// SQLite does not support comments on columns.
}
// parseColumnDef parses a single SQLite column definition string into a TableField.
// SQLite has flexible typing (type affinity), so columns may have no explicit type,
// in which case "text" is used as the default type.
func (p *SQLiteParser) parseColumnDef(def string, index int) (*gdb.TableField, error) {
tokens := mysqlTokenize(def)
if len(tokens) < 1 {
return nil, fmt.Errorf("invalid column definition: %s", def)
}
field := &gdb.TableField{
Index: index,
Name: unquoteIdentifier(tokens[0]),
Null: true,
}
if len(tokens) < 2 {
field.Type = "text"
return field, nil
}
field.Type = tokens[1]
rest := ""
if len(tokens) > 2 {
rest = strings.Join(tokens[2:], " ")
}
if !strings.Contains(field.Type, "(") && strings.HasPrefix(strings.TrimSpace(rest), "(") {
end := strings.Index(rest, ")")
if end >= 0 {
field.Type += rest[:end+1]
rest = strings.TrimSpace(rest[end+1:])
}
}
p.parseColumnAttributes(field, rest)
return field, nil
}
// parseColumnAttributes parses SQLite column constraint keywords including
// NOT NULL, NULL, PRIMARY KEY (with optional AUTOINCREMENT), UNIQUE, and DEFAULT.
func (p *SQLiteParser) parseColumnAttributes(field *gdb.TableField, attrs string) {
words := strings.Fields(attrs)
upperWords := strings.Fields(strings.ToUpper(attrs))
for i := 0; i < len(upperWords); i++ {
switch upperWords[i] {
case "NOT":
if i+1 < len(upperWords) && upperWords[i+1] == "NULL" {
field.Null = false
i++
}
case "NULL":
field.Null = true
case "PRIMARY":
if i+1 < len(upperWords) && upperWords[i+1] == "KEY" {
field.Key = "PRI"
field.Null = false
i++
if i+1 < len(upperWords) && upperWords[i+1] == "AUTOINCREMENT" {
field.Extra = "auto_increment"
i++
}
}
case "AUTOINCREMENT":
field.Extra = "auto_increment"
case "UNIQUE":
if field.Key == "" {
field.Key = "UNI"
}
case "DEFAULT":
if i+1 < len(words) {
defaultVal, _ := extractDefaultValue("DEFAULT " + strings.Join(words[i+1:], " "))
field.Default = defaultVal
if defaultVal != nil {
i++
}
}
}
}
}

View File

@ -0,0 +1,112 @@
// 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 gendao
import (
"testing"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/test/gtest"
)
func Test_SQLite_CreateTable_Basic(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &SQLiteParser{}
sql := `
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT,
age INTEGER DEFAULT 0,
score REAL DEFAULT 0.0,
is_active BOOLEAN NOT NULL DEFAULT 1
);
`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
fields := tables["users"]
t.Assert(len(fields), 6)
t.Assert(fields["id"].Key, "PRI")
t.Assert(fields["id"].Extra, "auto_increment")
t.Assert(fields["id"].Null, false)
t.Assert(fields["name"].Null, false)
t.Assert(fields["email"].Null, true)
t.Assert(fields["age"].Default, "0")
})
}
func Test_SQLite_AlterTable_AddColumn(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &SQLiteParser{}
sql := `
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
);
ALTER TABLE users ADD COLUMN email TEXT;
ALTER TABLE users ADD COLUMN phone TEXT DEFAULT '';
`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
fields := tables["users"]
t.Assert(len(fields), 4)
t.Assert(fields["email"].Name, "email")
t.Assert(fields["phone"].Name, "phone")
})
}
func Test_SQLite_AlterTable_DropColumn(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &SQLiteParser{}
sql := `
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
old_col TEXT,
email TEXT
);
ALTER TABLE users DROP COLUMN old_col;
`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
fields := tables["users"]
t.Assert(len(fields), 3)
_, ok := fields["old_col"]
t.Assert(ok, false)
t.Assert(fields["name"].Name, "name")
t.Assert(fields["email"].Name, "email")
})
}
func Test_SQLite_RenameColumn(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &SQLiteParser{}
sql := `
CREATE TABLE users (
id INTEGER PRIMARY KEY,
old_name TEXT NOT NULL
);
ALTER TABLE users RENAME COLUMN old_name TO new_name;
`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
fields := tables["users"]
_, ok := fields["old_name"]
t.Assert(ok, false)
t.Assert(fields["new_name"].Name, "new_name")
})
}

View File

@ -0,0 +1,302 @@
// 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 gendao
import (
"testing"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/test/gtest"
)
// ===========================
// Common parser utilities tests
// ===========================
func Test_splitSQLStatements(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
stmts := splitSQLStatements("CREATE TABLE t1 (id INT); ALTER TABLE t1 ADD COLUMN name VARCHAR(100);")
t.Assert(len(stmts), 2)
t.AssertIN("CREATE TABLE t1 (id INT)", stmts)
})
}
func Test_splitSQLStatements_WithComments(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
sql := `
-- This is a comment
CREATE TABLE t1 (id INT);
/* Block comment */
ALTER TABLE t1 ADD COLUMN name VARCHAR(100);
`
stmts := splitSQLStatements(sql)
t.Assert(len(stmts), 2)
})
}
func Test_splitSQLStatements_WithQuotedSemicolon(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
sql := `CREATE TABLE t1 (id INT, name VARCHAR(100) DEFAULT 'a;b');`
stmts := splitSQLStatements(sql)
t.Assert(len(stmts), 1)
})
}
func Test_classifyStatement(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
t.Assert(classifyStatement("CREATE TABLE users (id INT)"), SQLStatementCreateTable)
t.Assert(classifyStatement("CREATE TEMPORARY TABLE tmp (id INT)"), SQLStatementCreateTable)
t.Assert(classifyStatement("ALTER TABLE users ADD COLUMN email VARCHAR(100)"), SQLStatementAlterTable)
t.Assert(classifyStatement("ALTER TABLE users RENAME TO customers"), SQLStatementRenameTable)
t.Assert(classifyStatement("DROP TABLE IF EXISTS users"), SQLStatementDropTable)
t.Assert(classifyStatement("RENAME TABLE old_name TO new_name"), SQLStatementRenameTable)
t.Assert(classifyStatement("COMMENT ON COLUMN users.name IS 'User name'"), SQLStatementComment)
t.Assert(classifyStatement("SELECT * FROM users"), SQLStatementUnknown)
t.Assert(classifyStatement("INSERT INTO users VALUES (1)"), SQLStatementUnknown)
})
}
func Test_unquoteIdentifier(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
t.Assert(unquoteIdentifier("`users`"), "users")
t.Assert(unquoteIdentifier(`"users"`), "users")
t.Assert(unquoteIdentifier("[users]"), "users")
t.Assert(unquoteIdentifier("users"), "users")
})
}
func Test_extractTableName(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
t.Assert(extractTableName("CREATE TABLE users"), "users")
t.Assert(extractTableName("CREATE TABLE IF NOT EXISTS users"), "users")
t.Assert(extractTableName("CREATE TABLE `users`"), "users")
t.Assert(extractTableName("CREATE TABLE mydb.users"), "users")
t.Assert(extractTableName("CREATE TEMPORARY TABLE temp_users"), "temp_users")
})
}
func Test_applyDropTable(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
tables := map[string]map[string]*gdb.TableField{
"users": {},
"logs": {},
}
applyDropTable("DROP TABLE IF EXISTS users", tables)
t.Assert(len(tables), 1)
_, ok := tables["users"]
t.Assert(ok, false)
})
}
func Test_applyRenameTable_MySQL(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
tables := map[string]map[string]*gdb.TableField{
"old_name": {"id": {Index: 0, Name: "id", Type: "int"}},
}
applyRenameTable("RENAME TABLE old_name TO new_name", tables)
t.Assert(len(tables), 1)
_, ok := tables["new_name"]
t.Assert(ok, true)
})
}
func Test_applyRenameTable_PgSQL(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
tables := map[string]map[string]*gdb.TableField{
"old_name": {"id": {Index: 0, Name: "id", Type: "int"}},
}
applyRenameTable("ALTER TABLE old_name RENAME TO new_name", tables)
t.Assert(len(tables), 1)
_, ok := tables["new_name"]
t.Assert(ok, true)
})
}
// ===========================
// Abnormal/edge-case parsing tests
// ===========================
func Test_processSQL_OnlyDMLStatements(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &MySQLParser{}
sql := `
INSERT INTO users (id, name) VALUES (1, 'Alice');
INSERT INTO users (id, name) VALUES (2, 'Bob');
DELETE FROM users WHERE id = 1;
UPDATE users SET name = 'Charlie' WHERE id = 2;
SELECT * FROM users;
`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
t.Assert(len(tables), 0)
})
}
func Test_processSQL_EmptySQL(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &MySQLParser{}
tables := make(map[string]map[string]*gdb.TableField)
// Empty string
err := processSQL(parser, "", tables)
t.AssertNil(err)
t.Assert(len(tables), 0)
// Only whitespace and newlines
err = processSQL(parser, " \n\n \t ", tables)
t.AssertNil(err)
t.Assert(len(tables), 0)
})
}
func Test_processSQL_OnlyComments(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &MySQLParser{}
sql := `
-- This is a line comment
/* This is a block comment */
-- Another comment
`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
t.Assert(len(tables), 0)
})
}
func Test_processSQL_AlterNonExistentTable(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &MySQLParser{}
sql := `
ALTER TABLE non_existent ADD COLUMN email VARCHAR(200);
ALTER TABLE non_existent DROP COLUMN name;
ALTER TABLE non_existent MODIFY COLUMN name VARCHAR(200);
`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
t.Assert(len(tables), 0)
})
}
func Test_processSQL_DropNonExistentTable(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &MySQLParser{}
sql := `DROP TABLE IF EXISTS non_existent;`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
t.Assert(len(tables), 0)
})
}
func Test_processSQL_MixedDDLAndDML(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &MySQLParser{}
sql := `
INSERT INTO logs (msg) VALUES ('starting migration');
CREATE TABLE users (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
PRIMARY KEY (id)
);
INSERT INTO users (name) VALUES ('Alice');
ALTER TABLE users ADD COLUMN email VARCHAR(200);
UPDATE users SET email = 'alice@example.com' WHERE id = 1;
DELETE FROM logs WHERE msg = 'starting migration';
`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
// Only DDL statements should be processed; DML should be skipped.
t.Assert(len(tables), 1)
fields := tables["users"]
t.Assert(len(fields), 3)
t.Assert(fields["id"].Key, "PRI")
t.Assert(fields["email"].Name, "email")
})
}
func Test_processSQL_CommentOnNonExistentTable(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &PgSQLParser{}
sql := `COMMENT ON COLUMN non_existent.col1 IS 'some comment';`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
t.Assert(len(tables), 0)
})
}
func Test_processSQL_RenameNonExistentTable(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &MySQLParser{}
sql := `RENAME TABLE non_existent TO new_name;`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
t.Assert(len(tables), 0)
})
}
func Test_processSQL_DropColumnFromNonExistentTable(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
parser := &MySQLParser{}
sql := `
CREATE TABLE users (id INT, name VARCHAR(100), PRIMARY KEY (id));
ALTER TABLE orders DROP COLUMN status;
`
tables := make(map[string]map[string]*gdb.TableField)
err := processSQL(parser, sql, tables)
t.AssertNil(err)
// users table should still exist, orders ALTER should be silently ignored.
t.Assert(len(tables), 1)
t.Assert(len(tables["users"]), 2)
})
}
// ===========================
// CheckLocalTypeForFieldType Tests
// ===========================
func Test_CheckLocalTypeForFieldType(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
tests := []struct {
fieldType string
expected string
}{
{"int(10)", "int"},
{"int(10) unsigned", "uint"},
{"bigint(20)", "int64"},
{"bigint(20) unsigned", "uint64"},
{"tinyint(1)", "int"},
{"varchar(100)", "string"},
{"text", "string"},
{"datetime", "datetime"},
{"timestamp", "datetime"},
{"timestamptz", "datetime"},
{"date", "date"},
{"time", "time"},
{"json", "json"},
{"jsonb", "jsonb"},
{"float", "float64"},
{"double", "float64"},
{"decimal(10,2)", "string"},
{"bool", "bool"},
{"boolean", "bool"},
{"blob", "[]byte"},
{"binary(16)", "[]byte"},
{"bit(1)", "bool"},
}
for _, tt := range tests {
localType, err := gdb.CheckLocalTypeForFieldType(tt.fieldType)
t.AssertNil(err)
t.Assert(string(localType), tt.expected)
}
})
}

View File

@ -20,14 +20,20 @@ import (
"github.com/gogf/gf/v2/text/gstr"
)
// generateStructDefinitionInput holds parameters for generating a Go struct definition
// from database table fields.
type generateStructDefinitionInput struct {
CGenDaoInternalInput
TableName string // Table name.
StructName string // Struct name.
FieldMap map[string]*gdb.TableField // Table field map.
IsDo bool // Is generating DTO struct.
TableName string // Original database table name.
StructName string // Go struct name (CamelCase of table name).
FieldMap map[string]*gdb.TableField // Map of column name to field metadata.
IsDo bool // Whether generating a DO struct (uses g.Meta orm tag).
}
// generateStructDefinition generates a complete Go struct definition string from table fields.
// It returns the struct source code and a list of additional import paths needed
// by custom type mappings. The fields are rendered in a table-aligned format
// using tablewriter for consistent code formatting.
func generateStructDefinition(ctx context.Context, in generateStructDefinitionInput) (string, []string) {
var appendImports []string
buffer := bytes.NewBuffer(nil)
@ -59,6 +65,10 @@ func generateStructDefinition(ctx context.Context, in generateStructDefinitionIn
return buffer.String(), appendImports
}
// getTypeMappingInfo looks up a database field type in the type mapping configuration.
// It handles exact matches first, then tries to extract the base type name from
// parameterized types like "varchar(255)" or "numeric(10,2) unsigned".
// Returns the mapped Go type name and its import path (if any).
func getTypeMappingInfo(
ctx context.Context, fieldType string, inTypeMapping map[DBFieldTypeName]CustomAttributeType,
) (typeNameStr, importStr string) {
@ -105,9 +115,17 @@ func generateStructFieldDefinition(
}
if localTypeNameStr == "" {
localTypeName, err = in.DB.CheckLocalTypeForField(ctx, field.Type, nil)
if err != nil {
panic(err)
if in.DB != nil {
localTypeName, err = in.DB.CheckLocalTypeForField(ctx, field.Type, nil)
if err != nil {
panic(err)
}
} else {
// SQL file mode: use standalone type checking without database connection.
localTypeName, err = gdb.CheckLocalTypeForFieldType(field.Type)
if err != nil {
panic(err)
}
}
localTypeNameStr = string(localTypeName)
switch localTypeName {
@ -181,11 +199,12 @@ func generateStructFieldDefinition(
return attrLines, appendImport
}
// FieldNameCase defines the naming convention for converting field names to Go identifiers.
type FieldNameCase string
const (
FieldNameCaseCamel FieldNameCase = "CaseCamel"
FieldNameCaseCamelLower FieldNameCase = "CaseCamelLower"
FieldNameCaseCamel FieldNameCase = "CaseCamel" // PascalCase: "user_name" -> "UserName"
FieldNameCaseCamelLower FieldNameCase = "CaseCamelLower" // camelCase: "user_name" -> "userName"
)
// formatFieldName formats and returns a new field name that is used for golang codes generating.

View File

@ -62,7 +62,7 @@ type generateTableSingleInput struct {
// generateTableSingle generates dao files for a single table.
func generateTableSingle(ctx context.Context, in generateTableSingleInput) {
// Generating table data preparing.
fieldMap, err := in.DB.TableFields(ctx, in.TableName)
fieldMap, err := getTableFields(ctx, in.CGenDaoInternalInput, in.TableName)
if err != nil {
mlog.Fatalf(`fetching tables fields failed for table "%s": %+v`, in.TableName, err)
}

View File

@ -74,6 +74,8 @@ CONFIGURATION SUPPORT
CGenDaoBriefTypeMapping = `custom local type mapping for generated struct attributes relevant to fields of table`
CGenDaoBriefFieldMapping = `custom local type mapping for generated struct attributes relevant to specific fields of table`
CGenDaoBriefShardingPattern = `sharding pattern for table name, e.g. "users_?" will be replace tables "users_001,users_002,..." to "users" dao`
CGenDaoBriefSqlDir = `directory path of SQL DDL files for generating dao/do/entity without database connection`
CGenDaoBriefSqlType = `SQL dialect type when using sqlDir, options: mysql|pgsql|mssql|oracle|sqlite, default is "mysql"`
CGenDaoBriefGroup = `
specifying the configuration group name of database for generated ORM instance,
it's not necessary and the default value is "default"
@ -95,21 +97,23 @@ generated json tag case for model struct, cases are as follows:
CGenDaoBriefTplDaoDoPathPath = `template file path for dao do file`
CGenDaoBriefTplDaoEntityPath = `template file path for dao entity file`
tplVarTableName = `TplTableName`
tplVarTableNameCamelCase = `TplTableNameCamelCase`
tplVarTableNameCamelLowerCase = `TplTableNameCamelLowerCase`
tplVarTableSharding = `TplTableSharding`
tplVarTableShardingPrefix = `TplTableShardingPrefix`
tplVarTableFields = `TplTableFields`
tplVarPackageImports = `TplPackageImports`
tplVarImportPrefix = `TplImportPrefix`
tplVarStructDefine = `TplStructDefine`
tplVarColumnDefine = `TplColumnDefine`
tplVarColumnNames = `TplColumnNames`
tplVarGroupName = `TplGroupName`
tplVarDatetimeStr = `TplDatetimeStr`
tplVarCreatedAtDatetimeStr = `TplCreatedAtDatetimeStr`
tplVarPackageName = `TplPackageName`
// Template variable names used by gview for rendering Go file templates.
// These are passed to tplView.Assigns() and referenced in template files.
tplVarTableName = `TplTableName` // Original database table name.
tplVarTableNameCamelCase = `TplTableNameCamelCase` // PascalCase table name (e.g., "UserDetail").
tplVarTableNameCamelLowerCase = `TplTableNameCamelLowerCase` // camelCase table name (e.g., "userDetail").
tplVarTableSharding = `TplTableSharding` // Boolean: whether this is a sharding table.
tplVarTableShardingPrefix = `TplTableShardingPrefix` // Sharding table name prefix (e.g., "user_").
tplVarTableFields = `TplTableFields` // Generated table field definitions.
tplVarPackageImports = `TplPackageImports` // Generated import block string.
tplVarImportPrefix = `TplImportPrefix` // Go import path prefix for internal dao package.
tplVarStructDefine = `TplStructDefine` // Generated struct definition string.
tplVarColumnDefine = `TplColumnDefine` // Column struct field definitions for dao internal.
tplVarColumnNames = `TplColumnNames` // Column name-to-string assignments for dao internal.
tplVarGroupName = `TplGroupName` // Database configuration group name.
tplVarDatetimeStr = `TplDatetimeStr` // Current datetime string for file headers.
tplVarCreatedAtDatetimeStr = `TplCreatedAtDatetimeStr` // "Created at <datetime>" string (empty if WithTime is false).
tplVarPackageName = `TplPackageName` // Go package name for the generated file.
)
func init() {
@ -145,6 +149,8 @@ func init() {
`CGenDaoBriefTypeMapping`: CGenDaoBriefTypeMapping,
`CGenDaoBriefFieldMapping`: CGenDaoBriefFieldMapping,
`CGenDaoBriefShardingPattern`: CGenDaoBriefShardingPattern,
`CGenDaoBriefSqlDir`: CGenDaoBriefSqlDir,
`CGenDaoBriefSqlType`: CGenDaoBriefSqlType,
`CGenDaoBriefGroup`: CGenDaoBriefGroup,
`CGenDaoBriefJsonCase`: CGenDaoBriefJsonCase,
`CGenDaoBriefTplDaoIndexPath`: CGenDaoBriefTplDaoIndexPath,

View File

@ -12,7 +12,6 @@ import (
"github.com/gogf/gf/v2/container/garray"
"github.com/gogf/gf/v2/container/gmap"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
@ -37,21 +36,14 @@ func (c CGenService) calculateImportedItems(
}
for _, item := range pkgItems {
alias := item.Alias
// If the alias is _, it means that the package is not generated.
if alias == "_" {
// Skip anonymous imports
if item.Alias == "_" {
mlog.Debugf(`ignore anonymous package: %s`, item.RawImport)
continue
}
// If the alias is empty, it will use the package name as the alias.
if alias == "" {
alias = gfile.Basename(gstr.Trim(item.Path, `"`))
}
if !gstr.Contains(allFuncParamType.String(), alias) {
mlog.Debugf(`ignore unused package: %s`, item.RawImport)
continue
}
// Keep all imports, let gofmt clean up unused ones.
// We cannot accurately infer package name from import path
// (e.g., path "minio-go" but package name is "minio").
srcImportedPackages.Add(item.RawImport)
}
return nil

View File

@ -4,7 +4,7 @@ go 1.23.0
toolchain go1.24.6
require github.com/gogf/gf/v2 v2.9.8
require github.com/gogf/gf/v2 v2.10.0
require (
go.opentelemetry.io/otel v1.38.0 // indirect

View File

@ -0,0 +1,34 @@
package issue4242
import (
"context"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/issue/4242/service"
"github.com/gogf/gf/contrib/drivers/mysql/v2"
)
func init() {
service.RegisterIssue4242(New())
}
type sIssue4242 struct {
}
func New() *sIssue4242 {
return &sIssue4242{}
}
// GetDriver tests versioned import path is preserved.
func (s *sIssue4242) GetDriver(ctx context.Context) (d mysql.Driver, err error) {
return mysql.Driver{}, nil
}
// GetRequest tests another versioned import.
func (s *sIssue4242) GetRequest(ctx context.Context) (*ghttp.Request, error) {
g.Log().Info(ctx, "getting request")
return nil, nil
}

View File

@ -0,0 +1,37 @@
package issue4242alias
import (
"context"
// Anonymous import (should be skipped)
_ "github.com/gogf/gf/v2/os/gres"
// Versioned import without alias
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/issue/4242/service"
// Explicit alias import
mysqlDriver "github.com/gogf/gf/contrib/drivers/mysql/v2"
)
func init() {
service.RegisterIssue4242Alias(New())
}
type sIssue4242Alias struct {
}
func New() *sIssue4242Alias {
return &sIssue4242Alias{}
}
// GetDriver tests explicit alias import.
func (s *sIssue4242Alias) GetDriver(ctx context.Context) (d mysqlDriver.Driver, err error) {
return mysqlDriver.Driver{}, nil
}
// GetRequest tests versioned import.
func (s *sIssue4242Alias) GetRequest(ctx context.Context) (*ghttp.Request, error) {
return nil, nil
}

View File

@ -0,0 +1,10 @@
// ==========================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// ==========================================================================
package logic
import (
_ "github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/issue/4242/logic/issue4242"
_ "github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/issue/4242/logic/issue4242alias"
)

View File

@ -0,0 +1,37 @@
// ================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// You can delete these comments if you wish manually maintain this interface file.
// ================================================================================
package service
import (
"context"
"github.com/gogf/gf/contrib/drivers/mysql/v2"
"github.com/gogf/gf/v2/net/ghttp"
)
type (
IIssue4242 interface {
// GetDriver tests versioned import path is preserved.
GetDriver(ctx context.Context) (d mysql.Driver, err error)
// GetRequest tests another versioned import.
GetRequest(ctx context.Context) (*ghttp.Request, error)
}
)
var (
localIssue4242 IIssue4242
)
func Issue4242() IIssue4242 {
if localIssue4242 == nil {
panic("implement not found for interface IIssue4242, forgot register?")
}
return localIssue4242
}
func RegisterIssue4242(i IIssue4242) {
localIssue4242 = i
}

View File

@ -0,0 +1,37 @@
// ================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// You can delete these comments if you wish manually maintain this interface file.
// ================================================================================
package service
import (
"context"
mysqlDriver "github.com/gogf/gf/contrib/drivers/mysql/v2"
"github.com/gogf/gf/v2/net/ghttp"
)
type (
IIssue4242Alias interface {
// GetDriver tests explicit alias import.
GetDriver(ctx context.Context) (d mysqlDriver.Driver, err error)
// GetRequest tests versioned import.
GetRequest(ctx context.Context) (*ghttp.Request, error)
}
)
var (
localIssue4242Alias IIssue4242Alias
)
func Issue4242Alias() IIssue4242Alias {
if localIssue4242Alias == nil {
panic("implement not found for interface IIssue4242Alias, forgot register?")
}
return localIssue4242Alias
}
func RegisterIssue4242Alias(i IIssue4242Alias) {
localIssue4242Alias = i
}

View File

@ -1,3 +1,5 @@
module github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/issue/4387
go 1.20
go 1.23.0
toolchain go1.24.12

View File

View File

@ -749,7 +749,9 @@ func (a *TArray[T]) String() string {
}
// MarshalJSON implements the interface MarshalJSON for json.Marshal.
// Note that do not use pointer as its receiver here.
// DO NOT change this receiver to pointer type, as the TArray can be used as a var defined variable, like:
// var a TArray[int]
// Please refer to corresponding tests for more details.
func (a TArray[T]) MarshalJSON() ([]byte, error) {
a.mu.RLock()
defer a.mu.RUnlock()

View File

@ -46,7 +46,7 @@ func NewSortedTArray[T comparable](comparator func(a, b T) int, safe ...bool) *S
return NewSortedTArraySize(0, comparator, safe...)
}
// NewSortedTArraySize create and returns an sorted array with given size and cap.
// NewSortedTArraySize create and returns a sorted array with given size and cap.
// The parameter `safe` is used to specify whether using array in concurrent-safety,
// which is false in default.
func NewSortedTArraySize[T comparable](cap int, comparator func(a, b T) int, safe ...bool) *SortedTArray[T] {
@ -718,7 +718,9 @@ func (a *SortedTArray[T]) String() string {
}
// MarshalJSON implements the interface MarshalJSON for json.Marshal.
// Note that do not use pointer as its receiver here.
// DO NOT change this receiver to pointer type, as the TArray can be used as a var defined variable, like:
// var a SortedTArray[int]
// Please refer to corresponding tests for more details.
func (a SortedTArray[T]) MarshalJSON() ([]byte, error) {
a.mu.RLock()
defer a.mu.RUnlock()

View File

@ -22,8 +22,11 @@ type NilChecker[V any] func(V) bool
// KVMap wraps map type `map[K]V` and provides more map features.
type KVMap[K comparable, V any] struct {
mu rwmutex.RWMutex
data map[K]V
mu rwmutex.RWMutex
data map[K]V
// nilChecker is the custom nil checker function.
// It uses empty.IsNil if it's nil.
nilChecker NilChecker[V]
}
@ -58,15 +61,15 @@ func NewKVMapFrom[K comparable, V any](data map[K]V, safe ...bool) *KVMap[K, V]
// The parameter `safe` is used to specify whether to use the map in concurrent-safety mode, which is false by default.
func NewKVMapWithCheckerFrom[K comparable, V any](data map[K]V, checker NilChecker[V], safe ...bool) *KVMap[K, V] {
m := NewKVMapFrom[K, V](data, safe...)
m.RegisterNilChecker(checker)
m.SetNilChecker(checker)
return m
}
// RegisterNilChecker registers a custom nil checker function for the map values.
// SetNilChecker registers a custom nil checker function for the map values.
// This function is used to determine if a value should be considered as nil.
// The nil checker function takes a value of type V and returns a boolean indicating
// whether the value should be treated as nil.
func (m *KVMap[K, V]) RegisterNilChecker(nilChecker NilChecker[V]) {
func (m *KVMap[K, V]) SetNilChecker(nilChecker NilChecker[V]) {
m.mu.Lock()
defer m.mu.Unlock()
m.nilChecker = nilChecker
@ -74,12 +77,12 @@ func (m *KVMap[K, V]) RegisterNilChecker(nilChecker NilChecker[V]) {
// isNil checks whether the given value is nil.
// It first checks if a custom nil checker function is registered and uses it if available,
// otherwise it performs a standard nil check using any(v) == nil.
// otherwise it falls back to the default empty.IsNil function.
func (m *KVMap[K, V]) isNil(v V) bool {
if m.nilChecker != nil {
return m.nilChecker(v)
}
return any(v) == nil
return empty.IsNil(v)
}
// Iterator iterates the hash map readonly with custom callback function `f`.
@ -242,11 +245,12 @@ func (m *KVMap[K, V]) Pops(size int) map[K]V {
return newMap
}
// doSetWithLockCheck checks whether value of the key exists with mutex.Lock,
// if not exists, set value to the map with given `key`,
// or else just return the existing value.
// doSetWithLockCheck sets value with given `value` if it does not exist,
// and then returns this value and whether it exists.
//
// It returns value with given `key`.
// It is a helper function for GetOrSet* functions.
//
// Note that, it does not add the value to the map if the given `value` is nil.
func (m *KVMap[K, V]) doSetWithLockCheck(key K, value V) (val V, ok bool) {
m.mu.Lock()
defer m.mu.Unlock()
@ -274,6 +278,8 @@ func (m *KVMap[K, V]) GetOrSet(key K, value V) V {
// GetOrSetFunc returns the value by key,
// or sets value with returned value of callback function `f` if it does not exist
// and then returns this value.
//
// Note that, it does not add the value to the map if the returned value of `f` is nil.
func (m *KVMap[K, V]) GetOrSetFunc(key K, f func() V) V {
v, _ := m.doSetWithLockCheck(key, f())
return v
@ -285,6 +291,8 @@ func (m *KVMap[K, V]) GetOrSetFunc(key K, f func() V) V {
//
// GetOrSetFuncLock differs with GetOrSetFunc function is that it executes function `f`
// with mutex.Lock of the hash map.
//
// Note that, it does not add the value to the map if the returned value of `f` is nil.
func (m *KVMap[K, V]) GetOrSetFuncLock(key K, f func() V) V {
m.mu.Lock()
defer m.mu.Unlock()
@ -524,6 +532,9 @@ func (m *KVMap[K, V]) String() string {
}
// MarshalJSON implements the interface MarshalJSON for json.Marshal.
// DO NOT change this receiver to pointer type, as the KVMap can be used as a var defined variable, like:
// var m gmap.KVMap[int, string]
// Please refer to corresponding tests for more details.
func (m KVMap[K, V]) MarshalJSON() ([]byte, error) {
return json.Marshal(gconv.Map(m.Map()))
}

View File

@ -56,7 +56,7 @@ func NewListKVMap[K comparable, V any](safe ...bool) *ListKVMap[K, V] {
// which is false by default.
func NewListKVMapWithChecker[K comparable, V any](checker NilChecker[V], safe ...bool) *ListKVMap[K, V] {
m := NewListKVMap[K, V](safe...)
m.RegisterNilChecker(checker)
m.SetNilChecker(checker)
return m
}
@ -81,11 +81,11 @@ func NewListKVMapWithCheckerFrom[K comparable, V any](data map[K]V, nilChecker N
return m
}
// RegisterNilChecker registers a custom nil checker function for the map values.
// SetNilChecker registers a custom nil checker function for the map values.
// This function is used to determine if a value should be considered as nil.
// The nil checker function takes a value of type V and returns a boolean indicating
// whether the value should be treated as nil.
func (m *ListKVMap[K, V]) RegisterNilChecker(nilChecker NilChecker[V]) {
func (m *ListKVMap[K, V]) SetNilChecker(nilChecker NilChecker[V]) {
m.mu.Lock()
defer m.mu.Unlock()
m.nilChecker = nilChecker
@ -93,12 +93,12 @@ func (m *ListKVMap[K, V]) RegisterNilChecker(nilChecker NilChecker[V]) {
// isNil checks whether the given value is nil.
// It first checks if a custom nil checker function is registered and uses it if available,
// otherwise it performs a standard nil check using any(v) == nil.
// otherwise it falls back to the default empty.IsNil function.
func (m *ListKVMap[K, V]) isNil(v V) bool {
if m.nilChecker != nil {
return m.nilChecker(v)
}
return any(v) == nil
return empty.IsNil(v)
}
// Iterator is alias of IteratorAsc.
@ -402,6 +402,8 @@ func (m *ListKVMap[K, V]) GetVarOrSetFuncLock(key K, f func() V) *gvar.Var {
// SetIfNotExist sets `value` to the map if the `key` does not exist, and then returns true.
// It returns false if `key` exists, and `value` would be ignored.
//
// Note that it does not add the value to the map if `value` is nil.
func (m *ListKVMap[K, V]) SetIfNotExist(key K, value V) bool {
m.mu.Lock()
defer m.mu.Unlock()
@ -421,6 +423,8 @@ func (m *ListKVMap[K, V]) SetIfNotExist(key K, value V) bool {
// SetIfNotExistFunc sets value with return value of callback function `f`, and then returns true.
// It returns false if `key` exists, and `value` would be ignored.
//
// Note that, it does not add the value to the map if the returned value of `f` is nil.
func (m *ListKVMap[K, V]) SetIfNotExistFunc(key K, f func() V) bool {
m.mu.Lock()
defer m.mu.Unlock()
@ -444,6 +448,8 @@ func (m *ListKVMap[K, V]) SetIfNotExistFunc(key K, f func() V) bool {
//
// SetIfNotExistFuncLock differs with SetIfNotExistFunc function is that
// it executes function `f` with mutex.Lock of the map.
//
// Note that, it does not add the value to the map if the returned value of `f` is nil.
func (m *ListKVMap[K, V]) SetIfNotExistFuncLock(key K, f func() V) bool {
m.mu.Lock()
defer m.mu.Unlock()
@ -609,6 +615,9 @@ func (m *ListKVMap[K, V]) String() string {
}
// MarshalJSON implements the interface MarshalJSON for json.Marshal.
// DO NOT change this receiver to pointer type, as the ListKVMap can be used as a var defined variable, like:
// var m gmap.ListKVMap[string]string
// Please refer to corresponding tests for more details.
func (m ListKVMap[K, V]) MarshalJSON() (jsonBytes []byte, err error) {
if m.data == nil {
return []byte("{}"), nil

View File

@ -774,6 +774,13 @@ func Test_KVMap_MarshalJSON(t *testing.T) {
t.Assert(data["a"], 1)
t.Assert(data["b"], 2)
})
gtest.C(t, func(t *gtest.T) {
var m gmap.KVMap[int, int]
m.Set(1, 10)
b, err := json.Marshal(m)
t.AssertNil(err)
t.Assert(string(b), `{"1":10}`)
})
}
func Test_KVMap_UnmarshalJSON(t *testing.T) {
@ -1647,9 +1654,10 @@ func Test_KVMap_TypedNil(t *testing.T) {
return nil
})
}
t.Assert(m1.Size(), 10)
t.Assert(m1.Size(), 5)
m2 := gmap.NewKVMap[int, *Student](true)
m2.RegisterNilChecker(func(student *Student) bool {
m2.SetNilChecker(func(student *Student) bool {
return student == nil
})
for i := 0; i < 10; i++ {
@ -1679,7 +1687,8 @@ func Test_NewKVMapWithChecker_TypedNil(t *testing.T) {
return nil
})
}
t.Assert(m1.Size(), 10)
t.Assert(m1.Size(), 5)
m2 := gmap.NewKVMapWithChecker[int, *Student](func(student *Student) bool {
return student == nil
}, true)

View File

@ -183,77 +183,6 @@ func Test_ListKVMap_SetIfNotExistFuncLock_MultipleKeys(t *testing.T) {
})
}
// Test_ListKVMap_GetOrSetFuncLock_NilValue tests that nil values are handled correctly.
func Test_ListKVMap_GetOrSetFuncLock_NilValue(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
m := gmap.NewListKVMap[string, *int](true)
key := "nilKey"
callCount := int32(0)
var wg sync.WaitGroup
goroutines := 50
wg.Add(goroutines)
for i := 0; i < goroutines; i++ {
go func() {
defer wg.Done()
m.GetOrSetFuncLock(key, func() *int {
atomic.AddInt32(&callCount, 1)
return nil
})
}()
}
wg.Wait()
// Callback should be called once
t.Assert(atomic.LoadInt32(&callCount), 1)
// Typed nil pointer (*int)(nil) is stored because any(value) != nil for typed nil
// This is a Go language feature: typed nil is not the same as interface nil
t.Assert(m.Contains(key), true)
t.Assert(m.Get(key), (*int)(nil))
t.Assert(m.Size(), 1)
})
}
// Test_ListKVMap_SetIfNotExistFuncLock_NilValue tests that nil values are handled correctly.
func Test_ListKVMap_SetIfNotExistFuncLock_NilValue(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
m := gmap.NewListKVMap[string, *string](true)
key := "nilKey"
callCount := int32(0)
successCount := int32(0)
var wg sync.WaitGroup
goroutines := 50
wg.Add(goroutines)
for i := 0; i < goroutines; i++ {
go func() {
defer wg.Done()
success := m.SetIfNotExistFuncLock(key, func() *string {
atomic.AddInt32(&callCount, 1)
return nil
})
if success {
atomic.AddInt32(&successCount, 1)
}
}()
}
wg.Wait()
// Callback should be called once
t.Assert(atomic.LoadInt32(&callCount), 1)
// Should report success once
t.Assert(atomic.LoadInt32(&successCount), 1)
// Typed nil pointer (*string)(nil) is stored because any(value) != nil for typed nil
t.Assert(m.Contains(key), true)
t.Assert(m.Get(key), (*string)(nil))
t.Assert(m.Size(), 1)
})
}
// Test_ListKVMap_GetOrSetFuncLock_ExistingKey tests behavior when key already exists.
func Test_ListKVMap_GetOrSetFuncLock_ExistingKey(t *testing.T) {
gtest.C(t, func(t *gtest.T) {

View File

@ -1159,6 +1159,13 @@ func Test_ListKVMap_MarshalJSON_Error(t *testing.T) {
t.AssertNil(err)
t.Assert(string(b), `{"a":"1"}`)
})
gtest.C(t, func(t *gtest.T) {
var m gmap.ListKVMap[int, int]
m.Set(1, 10)
b, err := json.Marshal(m)
t.AssertNil(err)
t.Assert(string(b), `{"1":10}`)
})
}
// Test empty map operations
@ -1358,9 +1365,10 @@ func Test_ListKVMap_TypedNil(t *testing.T) {
return nil
})
}
t.Assert(m1.Size(), 10)
t.Assert(m1.Size(), 5)
m2 := gmap.NewListKVMap[int, *Student](true)
m2.RegisterNilChecker(func(student *Student) bool {
m2.SetNilChecker(func(student *Student) bool {
return student == nil
})
for i := 0; i < 10; i++ {
@ -1390,7 +1398,8 @@ func Test_NewListKVMapWithChecker_TypedNil(t *testing.T) {
return nil
})
}
t.Assert(m1.Size(), 10)
t.Assert(m1.Size(), 5)
m2 := gmap.NewListKVMapWithChecker[int, *Student](func(student *Student) bool {
return student == nil
}, true)

View File

@ -9,6 +9,7 @@ package gset
import (
"bytes"
"github.com/gogf/gf/v2/internal/empty"
"github.com/gogf/gf/v2/internal/json"
"github.com/gogf/gf/v2/internal/rwmutex"
"github.com/gogf/gf/v2/text/gstr"
@ -39,7 +40,7 @@ func NewTSet[T comparable](safe ...bool) *TSet[T] {
// The parameter `safe` is used to specify whether using set in concurrent-safety mode.
func NewTSetWithChecker[T comparable](checker NilChecker[T], safe ...bool) *TSet[T] {
s := NewTSet[T](safe...)
s.RegisterNilChecker(checker)
s.SetNilChecker(checker)
return s
}
@ -66,11 +67,11 @@ func NewTSetWithCheckerFrom[T comparable](items []T, checker NilChecker[T], safe
return set
}
// RegisterNilChecker registers a custom nil checker function for the set elements.
// SetNilChecker registers a custom nil checker function for the set elements.
// This function is used to determine if an element should be considered as nil.
// The nil checker function takes an element of type T and returns a boolean indicating
// whether the element should be treated as nil.
func (set *TSet[T]) RegisterNilChecker(nilChecker NilChecker[T]) {
func (set *TSet[T]) SetNilChecker(nilChecker NilChecker[T]) {
set.mu.Lock()
defer set.mu.Unlock()
set.nilChecker = nilChecker
@ -78,12 +79,12 @@ func (set *TSet[T]) RegisterNilChecker(nilChecker NilChecker[T]) {
// isNil checks whether the given value is nil.
// It first checks if a custom nil checker function is registered and uses it if available,
// otherwise it performs a standard nil check using any(v) == nil.
// otherwise it falls back to the default empty.IsNil function.
func (set *TSet[T]) isNil(v T) bool {
if set.nilChecker != nil {
return set.nilChecker(v)
}
return any(v) == nil
return empty.IsNil(v)
}
// Iterator iterates the set readonly with given callback function `f`,
@ -109,7 +110,7 @@ func (set *TSet[T]) Add(items ...T) {
}
// AddIfNotExist checks whether item exists in the set,
// it adds the item to set and returns true if it does not exists in the set,
// it adds the item to set and returns true if it does not exist in the set,
// or else it does nothing and returns false.
//
// Note that, if `item` is nil, it does nothing and returns false.

View File

@ -601,10 +601,10 @@ func Test_TSet_TypedNil(t *testing.T) {
set := gset.NewTSet[*Student](true)
var s *Student = nil
exist := set.AddIfNotExist(s)
t.Assert(exist, true)
t.Assert(exist, false)
set2 := gset.NewTSet[*Student](true)
set2.RegisterNilChecker(func(student *Student) bool {
set2.SetNilChecker(func(student *Student) bool {
return student == nil
})
exist2 := set2.AddIfNotExist(s)
@ -621,7 +621,7 @@ func Test_NewTSetWithChecker_TypedNil(t *testing.T) {
set := gset.NewTSet[*Student](true)
var s *Student = nil
exist := set.AddIfNotExist(s)
t.Assert(exist, true)
t.Assert(exist, false)
set2 := gset.NewTSetWithChecker[*Student](func(student *Student) bool {
return student == nil

View File

@ -12,6 +12,7 @@ import (
"github.com/emirpasic/gods/v2/trees/avltree"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/internal/empty"
"github.com/gogf/gf/v2/internal/json"
"github.com/gogf/gf/v2/internal/rwmutex"
"github.com/gogf/gf/v2/text/gstr"
@ -52,7 +53,7 @@ func NewAVLKVTree[K comparable, V any](comparator func(v1, v2 K) int, safe ...bo
// The parameter `checker` is used to specify whether the given value is nil.
func NewAVLKVTreeWithChecker[K comparable, V any](comparator func(v1, v2 K) int, checker NilChecker[V], safe ...bool) *AVLKVTree[K, V] {
t := NewAVLKVTree[K, V](comparator, safe...)
t.RegisterNilChecker(checker)
t.SetNilChecker(checker)
return t
}
@ -78,11 +79,11 @@ func NewAVLKVTreeWithCheckerFrom[K comparable, V any](comparator func(v1, v2 K)
return tree
}
// RegisterNilChecker registers a custom nil checker function for the map values.
// SetNilChecker registers a custom nil checker function for the map values.
// This function is used to determine if a value should be considered as nil.
// The nil checker function takes a value of type V and returns a boolean indicating
// whether the value should be treated as nil.
func (tree *AVLKVTree[K, V]) RegisterNilChecker(nilChecker NilChecker[V]) {
func (tree *AVLKVTree[K, V]) SetNilChecker(nilChecker NilChecker[V]) {
tree.mu.Lock()
defer tree.mu.Unlock()
tree.nilChecker = nilChecker
@ -90,12 +91,12 @@ func (tree *AVLKVTree[K, V]) RegisterNilChecker(nilChecker NilChecker[V]) {
// isNil checks whether the given value is nil.
// It first checks if a custom nil checker function is registered and uses it if available,
// otherwise it performs a standard nil check using any(v) == nil.
func (tree *AVLKVTree[K, V]) isNil(value V) bool {
// otherwise it falls back to the default empty.IsNil function.
func (tree *AVLKVTree[K, V]) isNil(v V) bool {
if tree.nilChecker != nil {
return tree.nilChecker(value)
return tree.nilChecker(v)
}
return any(value) == nil
return empty.IsNil(v)
}
// Clone clones and returns a new tree from current tree.

View File

@ -12,6 +12,7 @@ import (
"github.com/emirpasic/gods/v2/trees/btree"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/internal/empty"
"github.com/gogf/gf/v2/internal/json"
"github.com/gogf/gf/v2/internal/rwmutex"
"github.com/gogf/gf/v2/text/gstr"
@ -51,7 +52,7 @@ func NewBKVTree[K comparable, V any](m int, comparator func(v1, v2 K) int, safe
// The parameter `checker` is used to specify whether the given value is nil.
func NewBKVTreeWithChecker[K comparable, V any](m int, comparator func(v1, v2 K) int, checker NilChecker[V], safe ...bool) *BKVTree[K, V] {
t := NewBKVTree[K, V](m, comparator, safe...)
t.RegisterNilChecker(checker)
t.SetNilChecker(checker)
return t
}
@ -77,11 +78,11 @@ func NewBKVTreeWithCheckerFrom[K comparable, V any](m int, comparator func(v1, v
return tree
}
// RegisterNilChecker registers a custom nil checker function for the map values.
// SetNilChecker registers a custom nil checker function for the map values.
// This function is used to determine if a value should be considered as nil.
// The nil checker function takes a value of type V and returns a boolean indicating
// whether the value should be treated as nil.
func (tree *BKVTree[K, V]) RegisterNilChecker(nilChecker NilChecker[V]) {
func (tree *BKVTree[K, V]) SetNilChecker(nilChecker NilChecker[V]) {
tree.mu.Lock()
defer tree.mu.Unlock()
tree.nilChecker = nilChecker
@ -89,12 +90,12 @@ func (tree *BKVTree[K, V]) RegisterNilChecker(nilChecker NilChecker[V]) {
// isNil checks whether the given value is nil.
// It first checks if a custom nil checker function is registered and uses it if available,
// otherwise it performs a standard nil check using any(v) == nil.
func (tree *BKVTree[K, V]) isNil(value V) bool {
// otherwise it falls back to the default empty.IsNil function.
func (tree *BKVTree[K, V]) isNil(v V) bool {
if tree.nilChecker != nil {
return tree.nilChecker(value)
return tree.nilChecker(v)
}
return any(value) == nil
return empty.IsNil(v)
}
// Clone clones and returns a new tree from current tree.

View File

@ -12,6 +12,7 @@ import (
"github.com/emirpasic/gods/v2/trees/redblacktree"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/internal/empty"
"github.com/gogf/gf/v2/internal/json"
"github.com/gogf/gf/v2/internal/rwmutex"
"github.com/gogf/gf/v2/text/gstr"
@ -47,7 +48,7 @@ func NewRedBlackKVTree[K comparable, V any](comparator func(v1, v2 K) int, safe
// The parameter `checker` is used to specify whether the given value is nil.
func NewRedBlackKVTreeWithChecker[K comparable, V any](comparator func(v1, v2 K) int, checker NilChecker[V], safe ...bool) *RedBlackKVTree[K, V] {
t := NewRedBlackKVTree[K, V](comparator, safe...)
t.RegisterNilChecker(checker)
t.SetNilChecker(checker)
return t
}
@ -96,11 +97,11 @@ func RedBlackKVTreeInitFrom[K comparable, V any](tree *RedBlackKVTree[K, V], com
}
}
// RegisterNilChecker registers a custom nil checker function for the map values.
// SetNilChecker registers a custom nil checker function for the map values.
// This function is used to determine if a value should be considered as nil.
// The nil checker function takes a value of type V and returns a boolean indicating
// whether the value should be treated as nil.
func (tree *RedBlackKVTree[K, V]) RegisterNilChecker(nilChecker NilChecker[V]) {
func (tree *RedBlackKVTree[K, V]) SetNilChecker(nilChecker NilChecker[V]) {
tree.mu.Lock()
defer tree.mu.Unlock()
tree.nilChecker = nilChecker
@ -108,12 +109,12 @@ func (tree *RedBlackKVTree[K, V]) RegisterNilChecker(nilChecker NilChecker[V]) {
// isNil checks whether the given value is nil.
// It first checks if a custom nil checker function is registered and uses it if available,
// otherwise it performs a standard nil check using any(v) == nil.
func (tree *RedBlackKVTree[K, V]) isNil(value V) bool {
// otherwise it falls back to the default empty.IsNil function.
func (tree *RedBlackKVTree[K, V]) isNil(v V) bool {
if tree.nilChecker != nil {
return tree.nilChecker(value)
return tree.nilChecker(v)
}
return any(value) == nil
return empty.IsNil(v)
}
// SetComparator sets/changes the comparator for sorting.

View File

@ -29,9 +29,10 @@ func Test_KVAVLTree_TypedNil(t *testing.T) {
avlTree.Set(i, s)
}
}
t.Assert(avlTree.Size(), 10)
t.Assert(avlTree.Size(), 5)
avlTree2 := gtree.NewAVLKVTree[int, *Student](gutil.ComparatorTStr[int], true)
avlTree2.RegisterNilChecker(func(student *Student) bool {
avlTree2.SetNilChecker(func(student *Student) bool {
return student == nil
})
for i := 0; i < 10; i++ {
@ -62,9 +63,10 @@ func Test_KVBTree_TypedNil(t *testing.T) {
btree.Set(i, s)
}
}
t.Assert(btree.Size(), 10)
t.Assert(btree.Size(), 5)
btree2 := gtree.NewBKVTree[int, *Student](100, gutil.ComparatorTStr[int], true)
btree2.RegisterNilChecker(func(student *Student) bool {
btree2.SetNilChecker(func(student *Student) bool {
return student == nil
})
for i := 0; i < 10; i++ {
@ -95,10 +97,10 @@ func Test_KVRedBlackTree_TypedNil(t *testing.T) {
redBlackTree.Set(i, s)
}
}
t.Assert(redBlackTree.Size(), 10)
redBlackTree2 := gtree.NewRedBlackKVTree[int, *Student](gutil.ComparatorTStr[int], true)
t.Assert(redBlackTree.Size(), 5)
redBlackTree2.RegisterNilChecker(func(student *Student) bool {
redBlackTree2 := gtree.NewRedBlackKVTree[int, *Student](gutil.ComparatorTStr[int], true)
redBlackTree2.SetNilChecker(func(student *Student) bool {
return student == nil
})
for i := 0; i < 10; i++ {
@ -128,7 +130,8 @@ func Test_NewKVAVLTreeWithChecker_TypedNil(t *testing.T) {
avlTree.Set(i, s)
}
}
t.Assert(avlTree.Size(), 10)
t.Assert(avlTree.Size(), 5)
avlTree2 := gtree.NewAVLKVTreeWithChecker[int, *Student](gutil.ComparatorTStr[int], func(student *Student) bool {
return student == nil
}, true)
@ -160,7 +163,8 @@ func Test_NewKVBTreeWithChecker_TypedNil(t *testing.T) {
btree.Set(i, s)
}
}
t.Assert(btree.Size(), 10)
t.Assert(btree.Size(), 5)
btree2 := gtree.NewBKVTreeWithChecker[int, *Student](100, gutil.ComparatorTStr[int], func(student *Student) bool {
return student == nil
}, true)
@ -192,7 +196,8 @@ func Test_NewRedBlackKVTreeWithChecker_TypedNil(t *testing.T) {
redBlackTree.Set(i, s)
}
}
t.Assert(redBlackTree.Size(), 10)
t.Assert(redBlackTree.Size(), 5)
redBlackTree2 := gtree.NewRedBlackKVTreeWithChecker[int, *Student](gutil.ComparatorTStr[int], func(student *Student) bool {
return student == nil
}, true)

View File

@ -179,7 +179,7 @@ func (c *Client) updateLocalValue(ctx context.Context) (err error) {
}
// AddWatcher adds a watcher for the specified configuration file.
func (c *Client) AddWatcher(name string, f func(ctx context.Context)) {
func (c *Client) AddWatcher(name string, f gcfg.WatcherFunc) {
c.watchers.Add(name, f)
}
@ -193,6 +193,11 @@ func (c *Client) GetWatcherNames() []string {
return c.watchers.GetNames()
}
// IsWatching checks whether the watcher with the specified name is registered.
func (c *Client) IsWatching(name string) bool {
return c.watchers.IsWatching(name)
}
// notifyWatchers notifies all watchers.
func (c *Client) notifyWatchers(ctx context.Context) {
c.watchers.Notify(ctx)

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.8
github.com/gogf/gf/v2 v2.10.0
)
require (

View File

@ -207,7 +207,7 @@ func (c *Client) startAsynchronousWatch(plan *watch.Plan) {
}
// AddWatcher adds a watcher for the specified configuration file.
func (c *Client) AddWatcher(name string, f func(ctx context.Context)) {
func (c *Client) AddWatcher(name string, f gcfg.WatcherFunc) {
c.watchers.Add(name, f)
}
@ -221,6 +221,11 @@ func (c *Client) GetWatcherNames() []string {
return c.watchers.GetNames()
}
// IsWatching checks whether the watcher with the specified name is registered.
func (c *Client) IsWatching(name string) bool {
return c.watchers.IsWatching(name)
}
// notifyWatchers notifies all watchers.
func (c *Client) notifyWatchers(ctx context.Context) {
c.watchers.Notify(ctx)

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.8
github.com/gogf/gf/v2 v2.10.0
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.8
github.com/gogf/gf/v2 v2.10.0
k8s.io/api v0.33.4
k8s.io/apimachinery v0.33.4
k8s.io/client-go v0.33.4

View File

@ -199,7 +199,7 @@ func (c *Client) startAsynchronousWatch(ctx context.Context, namespace string, w
}
// AddWatcher adds a watcher for the specified configuration file.
func (c *Client) AddWatcher(name string, f func(ctx context.Context)) {
func (c *Client) AddWatcher(name string, f gcfg.WatcherFunc) {
c.watchers.Add(name, f)
}
@ -213,6 +213,11 @@ func (c *Client) GetWatcherNames() []string {
return c.watchers.GetNames()
}
// IsWatching checks whether the watcher with the specified name is registered.
func (c *Client) IsWatching(name string) bool {
return c.watchers.IsWatching(name)
}
// notifyWatchers notifies all watchers.
func (c *Client) notifyWatchers(ctx context.Context) {
c.watchers.Notify(ctx)

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.8
github.com/gogf/gf/v2 v2.10.0
github.com/nacos-group/nacos-sdk-go/v2 v2.3.3
)

View File

@ -152,7 +152,7 @@ func (c *Client) addWatcher() error {
}
// AddWatcher adds a watcher for the specified configuration file.
func (c *Client) AddWatcher(name string, f func(ctx context.Context)) {
func (c *Client) AddWatcher(name string, f gcfg.WatcherFunc) {
c.watchers.Add(name, f)
}
@ -166,6 +166,11 @@ func (c *Client) GetWatcherNames() []string {
return c.watchers.GetNames()
}
// IsWatching checks whether the watcher with the specified name is registered.
func (c *Client) IsWatching(name string) bool {
return c.watchers.IsWatching(name)
}
// notifyWatchers notifies all watchers.
func (c *Client) notifyWatchers(ctx context.Context) {
c.watchers.Notify(ctx)

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.8
github.com/gogf/gf/v2 v2.10.0
github.com/polarismesh/polaris-go v1.6.1
)

View File

@ -187,7 +187,7 @@ func (c *Client) startAsynchronousWatch(ctx context.Context, changeChan <-chan m
}
// AddWatcher adds a watcher for the specified configuration file.
func (c *Client) AddWatcher(name string, f func(ctx context.Context)) {
func (c *Client) AddWatcher(name string, f gcfg.WatcherFunc) {
c.watchers.Add(name, f)
}
@ -201,6 +201,11 @@ func (c *Client) GetWatcherNames() []string {
return c.watchers.GetNames()
}
// IsWatching checks whether the watcher with the specified name is registered.
func (c *Client) IsWatching(name string) bool {
return c.watchers.IsWatching(name)
}
// notifyWatchers notifies all watchers.
func (c *Client) notifyWatchers(ctx context.Context) {
c.watchers.Notify(ctx)

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.8
github.com/gogf/gf/v2 v2.10.0
github.com/google/uuid v1.6.0
github.com/shopspring/decimal v1.3.1
)

View File

@ -108,7 +108,7 @@ func (d *Driver) doMergeInsert(
one = list[0]
oneLen = len(one)
charL, charR = d.GetChars()
conflictKeySet = gset.New(false)
conflictKeySet = gset.NewStrSet(false)
// queryHolders: Handle data with Holder that need to be merged
// queryValues: Handle data that need to be merged

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.8
github.com/gogf/gf/v2 v2.10.0
)
require (

View File

@ -307,7 +307,7 @@ func (d *Driver) doMergeInsert(
one = list[0]
oneLen = len(one)
charL, charR = d.GetChars()
conflictKeySet = gset.New(false)
conflictKeySet = gset.NewStrSet(false)
// queryHolders: Handle data with Holder that need to be merged
// queryValues: Handle data that need to be merged

View File

@ -0,0 +1,102 @@
// 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"
"time"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/os/glog"
"github.com/gogf/gf/v2/test/gtest"
)
func Test_Ctx(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
db, err := gdb.Instance()
t.AssertNil(err)
err1 := db.PingMaster()
err2 := db.PingSlave()
t.Assert(err1, nil)
t.Assert(err2, nil)
newDb := db.Ctx(context.Background())
t.AssertNE(newDb, nil)
})
}
func Test_Ctx_Query(t *testing.T) {
db.GetLogger().(*glog.Logger).SetCtxKeys("SpanId", "TraceId")
gtest.C(t, func(t *gtest.T) {
db.SetDebug(true)
defer db.SetDebug(false)
ctx := context.WithValue(context.Background(), "TraceId", "12345678")
ctx = context.WithValue(ctx, "SpanId", "0.1")
db.Query(ctx, "select 1")
})
gtest.C(t, func(t *gtest.T) {
db.SetDebug(true)
defer db.SetDebug(false)
db.Query(ctx, "select 2")
})
}
func Test_Ctx_Model(t *testing.T) {
table := createInitTable()
defer dropTable(table)
db.GetLogger().(*glog.Logger).SetCtxKeys("SpanId", "TraceId")
gtest.C(t, func(t *gtest.T) {
db.SetDebug(true)
defer db.SetDebug(false)
ctx := context.WithValue(context.Background(), "TraceId", "12345678")
ctx = context.WithValue(ctx, "SpanId", "0.1")
db.Model(table).Ctx(ctx).All()
})
gtest.C(t, func(t *gtest.T) {
db.SetDebug(true)
defer db.SetDebug(false)
db.Model(table).All()
})
}
func Test_Ctx_Transaction(t *testing.T) {
table := createInitTable()
defer dropTable(table)
db.GetLogger().(*glog.Logger).SetCtxKeys("SpanId", "TraceId")
gtest.C(t, func(t *gtest.T) {
db.SetDebug(true)
defer db.SetDebug(false)
ctx := context.WithValue(context.Background(), "TraceId", "tx_trace_123")
ctx = context.WithValue(ctx, "SpanId", "0.2")
err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
_, err := tx.Model(table).Ctx(ctx).Where("id", 1).One()
return err
})
t.AssertNil(err)
})
}
func Test_Ctx_Timeout(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*10)
defer cancel()
// Wait for the context to expire
time.Sleep(time.Millisecond * 50)
// Query with expired context should return error
_, err := db.Model(table).Ctx(ctx).All()
t.AssertNE(err, nil)
})
}

View File

@ -0,0 +1,216 @@
// 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"
"database/sql"
"fmt"
"testing"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/test/gtest"
)
func Test_Model_Hook_Select(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
m := db.Model(table).Hook(gdb.HookHandler{
Select: func(ctx context.Context, in *gdb.HookSelectInput) (result gdb.Result, err error) {
result, err = in.Next(ctx)
if err != nil {
return
}
for i, record := range result {
record["test"] = gvar.New(100 + record["id"].Int())
result[i] = record
}
return
},
})
all, err := m.Where("id > ?", 6).OrderAsc("id").All()
t.AssertNil(err)
t.Assert(len(all), 4)
t.Assert(all[0]["id"].Int(), 7)
t.Assert(all[0]["test"].Int(), 107)
t.Assert(all[1]["test"].Int(), 108)
t.Assert(all[2]["test"].Int(), 109)
t.Assert(all[3]["test"].Int(), 110)
})
}
func Test_Model_Hook_Insert(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
m := db.Model(table).Hook(gdb.HookHandler{
Insert: func(ctx context.Context, in *gdb.HookInsertInput) (result sql.Result, err error) {
for i, item := range in.Data {
item["passport"] = fmt.Sprintf(`test_port_%d`, item["id"])
item["nickname"] = fmt.Sprintf(`test_name_%d`, item["id"])
item["password"] = fmt.Sprintf(`test_pass_%d`, item["id"])
item["create_time"] = CreateTime
in.Data[i] = item
}
return in.Next(ctx)
},
})
_, err := m.Insert(g.Map{
"id": 1,
"nickname": "name_1",
})
t.AssertNil(err)
one, err := m.One()
t.AssertNil(err)
t.Assert(one["id"].Int(), 1)
t.Assert(one["passport"], `test_port_1`)
t.Assert(one["nickname"], `test_name_1`)
})
}
func Test_Model_Hook_Update(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
m := db.Model(table).Hook(gdb.HookHandler{
Update: func(ctx context.Context, in *gdb.HookUpdateInput) (result sql.Result, err error) {
switch value := in.Data.(type) {
case gdb.List:
for i, data := range value {
data["passport"] = `port`
data["nickname"] = `name`
value[i] = data
}
in.Data = value
case gdb.Map:
value["passport"] = `port`
value["nickname"] = `name`
in.Data = value
}
return in.Next(ctx)
},
})
_, err := m.Data(g.Map{
"nickname": "name_1",
}).WherePri(1).Update()
t.AssertNil(err)
one, err := m.One()
t.AssertNil(err)
t.Assert(one["id"].Int(), 1)
t.Assert(one["passport"], `port`)
t.Assert(one["nickname"], `name`)
})
}
func Test_Model_Hook_Delete(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
m := db.Model(table).Hook(gdb.HookHandler{
Delete: func(ctx context.Context, in *gdb.HookDeleteInput) (result sql.Result, err error) {
return db.Model(table).Data(g.Map{
"nickname": `deleted`,
}).Where(in.Condition).Update()
},
})
_, err := m.Where("1=1").Delete()
t.AssertNil(err)
all, err := m.All()
t.AssertNil(err)
for _, item := range all {
t.Assert(item["nickname"].String(), `deleted`)
}
})
}
func Test_Model_Hook_Select_Count(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
m := db.Model(table).Hook(gdb.HookHandler{
Select: func(ctx context.Context, in *gdb.HookSelectInput) (result gdb.Result, err error) {
result, err = in.Next(ctx)
if err != nil {
return
}
// Adding extra fields should not affect Count operations
for i, record := range result {
record["extra"] = gvar.New("extra_value")
result[i] = record
}
return
},
})
count, err := m.Count()
t.AssertNil(err)
t.Assert(count, TableSize)
})
}
func Test_Model_Hook_Chain(t *testing.T) {
table := createInitTable()
defer dropTable(table)
// Normal chain: two hooks both modify data
gtest.C(t, func(t *gtest.T) {
m := db.Model(table).Hook(gdb.HookHandler{
Select: func(ctx context.Context, in *gdb.HookSelectInput) (result gdb.Result, err error) {
result, err = in.Next(ctx)
if err != nil {
return
}
for i, record := range result {
record["hook1"] = gvar.New("value1")
result[i] = record
}
return
},
}).Hook(gdb.HookHandler{
Select: func(ctx context.Context, in *gdb.HookSelectInput) (result gdb.Result, err error) {
result, err = in.Next(ctx)
if err != nil {
return
}
for i, record := range result {
record["hook2"] = gvar.New("value2")
result[i] = record
}
return
},
})
all, err := m.Where("id", 1).All()
t.AssertNil(err)
t.Assert(len(all), 1)
t.Assert(all[0]["id"].Int(), 1)
// The last Hook should take effect (Hook replaces previous one)
t.Assert(all[0]["hook2"].String(), "value2")
})
// Error chain: hook returns error
gtest.C(t, func(t *gtest.T) {
m := db.Model(table).Hook(gdb.HookHandler{
Select: func(ctx context.Context, in *gdb.HookSelectInput) (result gdb.Result, err error) {
return nil, gerror.New("hook error")
},
})
_, err := m.Where("id", 1).All()
t.AssertNE(err, nil)
t.Assert(err.Error(), "hook error")
})
}

View File

@ -0,0 +1,141 @@
// 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/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/test/gtest"
"github.com/gogf/gf/v2/util/gmeta"
)
func Test_Model_Builder(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
m := db.Model(table)
b := m.Builder()
all, err := m.Where(
b.Where("id", g.Slice{1, 2, 3}).WhereOr("id", g.Slice{4, 5, 6}),
).All()
t.AssertNil(err)
t.Assert(len(all), 6)
})
// Where And
gtest.C(t, func(t *gtest.T) {
m := db.Model(table)
b := m.Builder()
all, err := m.Where(
b.Where("id", g.Slice{1, 2, 3}).WhereOr("id", g.Slice{4, 5, 6}),
).Where(
b.Where("id", g.Slice{2, 3}).WhereOr("id", g.Slice{5, 6}),
).Where(
b.Where("id", g.Slice{3}).Where("id", g.Slice{1, 2, 3}),
).All()
t.AssertNil(err)
t.Assert(len(all), 1)
})
// Where Or
gtest.C(t, func(t *gtest.T) {
m := db.Model(table)
b := m.Builder()
all, err := m.WhereOr(
b.Where("id", g.Slice{1, 2, 3}).WhereOr("id", g.Slice{4, 5, 6}),
).WhereOr(
b.Where("id", g.Slice{2, 3}).WhereOr("id", g.Slice{5, 6}),
).WhereOr(
b.Where("id", g.Slice{3}).Where("id", g.Slice{1, 2, 3}),
).All()
t.AssertNil(err)
t.Assert(len(all), 6)
})
// Where with struct which has a field type of *gtime.Time
gtest.C(t, func(t *gtest.T) {
m := db.Model(table)
b := m.Builder()
type Query struct {
Id any
Nickname *gtime.Time
}
where, args := b.Where(&Query{Id: 1}).Build()
t.Assert(where, `"id"=? AND "nickname" IS NULL`)
t.Assert(args, []any{1})
})
// Where with struct which has a field type of *gjson.Json
gtest.C(t, func(t *gtest.T) {
m := db.Model(table)
b := m.Builder()
type Query struct {
Id any
Nickname *gjson.Json
}
where, args := b.Where(&Query{Id: 1}).Build()
t.Assert(where, `"id"=? AND "nickname" IS NULL`)
t.Assert(args, []any{1})
})
// Where with do struct which has a field type of *gtime.Time and generated by gf cli
gtest.C(t, func(t *gtest.T) {
m := db.Model(table)
b := m.Builder()
type Query struct {
gmeta.Meta `orm:"do:true"`
Id any
Nickname *gtime.Time
}
where, args := b.Where(&Query{Id: 1}).Build()
t.Assert(where, `"id"=?`)
t.Assert(args, []any{1})
})
// Where with do struct which has a field type of *gjson.Json and generated by gf cli
gtest.C(t, func(t *gtest.T) {
m := db.Model(table)
b := m.Builder()
type Query struct {
gmeta.Meta `orm:"do:true"`
Id any
Nickname *gjson.Json
}
where, args := b.Where(&Query{Id: 1}).Build()
t.Assert(where, `"id"=?`)
t.Assert(args, []any{1})
})
}
func Test_Safe_Builder(t *testing.T) {
// test whether m.Builder() is chain safe
gtest.C(t, func(t *gtest.T) {
b := db.Model().Builder()
b.Where("id", 1)
_, args := b.Build()
t.AssertNil(args)
b = b.Where("id", 1)
_, args = b.Build()
t.Assert(args, g.Slice{1})
})
}

View File

@ -0,0 +1,410 @@
// 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/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/test/gtest"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
)
// createTableDO creates a table with nullable columns (no NOT NULL constraints)
// suitable for DO (Data Object) partial insert tests.
func createTableDO(table ...string) (name string) {
if len(table) > 0 {
name = table[0]
} else {
name = fmt.Sprintf(`%s_%d`, TablePrefix+"do_test", gtime.TimestampNano())
}
dropTable(name)
if _, err := db.Exec(ctx, fmt.Sprintf(`
CREATE TABLE %s (
id bigserial NOT NULL,
passport varchar(45) DEFAULT '',
password varchar(32) DEFAULT '',
nickname varchar(45) DEFAULT '',
create_time timestamp DEFAULT NULL,
PRIMARY KEY (id)
);`, name,
)); err != nil {
gtest.Fatal(err)
}
return
}
func Test_Model_Insert_Data_DO(t *testing.T) {
table := createTableDO()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type User struct {
g.Meta `orm:"do:true"`
Id any
Passport any
Password any
Nickname any
CreateTime any
}
data := User{
Id: 1,
Passport: "user_1",
Password: "pass_1",
}
result, err := db.Model(table).Data(data).Insert()
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 1)
one, err := db.Model(table).WherePri(1).One()
t.AssertNil(err)
t.Assert(one[`id`], 1)
t.Assert(one[`passport`], `user_1`)
t.Assert(one[`password`], `pass_1`)
t.Assert(one[`nickname`], ``)
})
}
func Test_Model_Insert_Data_List_DO(t *testing.T) {
table := createTableDO()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type User struct {
g.Meta `orm:"do:true"`
Id any
Passport any
Password any
Nickname any
CreateTime any
}
data := g.Slice{
User{
Id: 1,
Passport: "user_1",
Password: "pass_1",
},
User{
Id: 2,
Passport: "user_2",
Password: "pass_2",
},
}
result, err := db.Model(table).Data(data).Insert()
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 2)
one, err := db.Model(table).WherePri(1).One()
t.AssertNil(err)
t.Assert(one[`id`], 1)
t.Assert(one[`passport`], `user_1`)
t.Assert(one[`password`], `pass_1`)
t.Assert(one[`nickname`], ``)
one, err = db.Model(table).WherePri(2).One()
t.AssertNil(err)
t.Assert(one[`id`], 2)
t.Assert(one[`passport`], `user_2`)
t.Assert(one[`password`], `pass_2`)
t.Assert(one[`nickname`], ``)
})
}
func Test_Model_Update_Data_DO(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type User struct {
g.Meta `orm:"do:true"`
Id any
Passport any
Password any
Nickname any
CreateTime any
}
data := User{
Id: 1,
Passport: "user_100",
Password: "pass_100",
}
_, err := db.Model(table).Data(data).WherePri(1).Update()
t.AssertNil(err)
one, err := db.Model(table).WherePri(1).One()
t.AssertNil(err)
t.Assert(one[`id`], 1)
t.Assert(one[`passport`], `user_100`)
t.Assert(one[`password`], `pass_100`)
t.Assert(one[`nickname`], `name_1`)
})
}
func Test_Model_Update_Pointer_Data_DO(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type NN string
type Req struct {
Id int
Passport *string
Password *string
Nickname *NN
}
type UserDo struct {
g.Meta `orm:"do:true"`
Id any
Passport any
Password any
Nickname any
CreateTime any
}
var (
nickname = NN("nickname_111")
req = Req{
Password: gconv.PtrString("12345678"),
Nickname: &nickname,
}
data = UserDo{
Passport: req.Passport,
Password: req.Password,
Nickname: req.Nickname,
}
)
_, err := db.Model(table).Data(data).WherePri(1).Update()
t.AssertNil(err)
one, err := db.Model(table).WherePri(1).One()
t.AssertNil(err)
t.Assert(one[`id`], 1)
t.Assert(one[`password`], `12345678`)
t.Assert(one[`nickname`], `nickname_111`)
})
}
func Test_Model_Where_DO(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type User struct {
g.Meta `orm:"do:true"`
Id any
Passport any
Password any
Nickname any
CreateTime any
}
where := User{
Id: 1,
Passport: "user_1",
Password: "pass_1",
}
one, err := db.Model(table).Where(where).One()
t.AssertNil(err)
t.Assert(one[`id`], 1)
t.Assert(one[`passport`], `user_1`)
t.Assert(one[`password`], `pass_1`)
t.Assert(one[`nickname`], `name_1`)
})
}
func Test_Model_Insert_Data_ForDao(t *testing.T) {
table := createTableDO()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type UserForDao struct {
Id any
Passport any
Password any
Nickname any
CreateTime any
}
data := UserForDao{
Id: 1,
Passport: "user_1",
Password: "pass_1",
}
result, err := db.Model(table).Data(data).Insert()
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 1)
one, err := db.Model(table).WherePri(1).One()
t.AssertNil(err)
t.Assert(one[`id`], 1)
t.Assert(one[`passport`], `user_1`)
t.Assert(one[`password`], `pass_1`)
t.Assert(one[`nickname`], ``)
})
}
func Test_Model_Insert_Data_List_ForDao(t *testing.T) {
table := createTableDO()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type UserForDao struct {
Id any
Passport any
Password any
Nickname any
CreateTime any
}
data := g.Slice{
UserForDao{
Id: 1,
Passport: "user_1",
Password: "pass_1",
},
UserForDao{
Id: 2,
Passport: "user_2",
Password: "pass_2",
},
}
result, err := db.Model(table).Data(data).Insert()
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 2)
one, err := db.Model(table).WherePri(1).One()
t.AssertNil(err)
t.Assert(one[`id`], 1)
t.Assert(one[`passport`], `user_1`)
t.Assert(one[`password`], `pass_1`)
t.Assert(one[`nickname`], ``)
one, err = db.Model(table).WherePri(2).One()
t.AssertNil(err)
t.Assert(one[`id`], 2)
t.Assert(one[`passport`], `user_2`)
t.Assert(one[`password`], `pass_2`)
t.Assert(one[`nickname`], ``)
})
}
func Test_Model_Update_Data_ForDao(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type UserForDao struct {
Id any
Passport any
Password any
Nickname any
CreateTime any
}
data := UserForDao{
Id: 1,
Passport: "user_100",
Password: "pass_100",
}
_, err := db.Model(table).Data(data).WherePri(1).Update()
t.AssertNil(err)
one, err := db.Model(table).WherePri(1).One()
t.AssertNil(err)
t.Assert(one[`id`], 1)
t.Assert(one[`passport`], `user_100`)
t.Assert(one[`password`], `pass_100`)
t.Assert(one[`nickname`], `name_1`)
})
}
func Test_Model_Where_ForDao(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type UserForDao struct {
Id any
Passport any
Password any
Nickname any
CreateTime any
}
where := UserForDao{
Id: 1,
Passport: "user_1",
Password: "pass_1",
}
one, err := db.Model(table).Where(where).One()
t.AssertNil(err)
t.Assert(one[`id`], 1)
t.Assert(one[`passport`], `user_1`)
t.Assert(one[`password`], `pass_1`)
t.Assert(one[`nickname`], `name_1`)
})
}
func Test_Model_Where_FieldPrefix(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
array := gstr.SplitAndTrim(gtest.DataContent(`table_with_prefix.sql`), ";")
for _, v := range array {
if _, err := db.Exec(ctx, v); err != nil {
gtest.Error(err)
}
}
defer dropTable("instance")
type Instance struct {
ID int `orm:"f_id"`
Name string
}
type InstanceDo struct {
g.Meta `orm:"table:instance, do:true"`
ID any `orm:"f_id"`
}
var instance *Instance
err := db.Model("instance").Where(InstanceDo{
ID: 1,
}).Scan(&instance)
t.AssertNil(err)
t.AssertNE(instance, nil)
t.Assert(instance.ID, 1)
t.Assert(instance.Name, "john")
})
// With omitempty.
gtest.C(t, func(t *gtest.T) {
array := gstr.SplitAndTrim(gtest.DataContent(`table_with_prefix.sql`), ";")
for _, v := range array {
if _, err := db.Exec(ctx, v); err != nil {
gtest.Error(err)
}
}
defer dropTable("instance")
type Instance struct {
ID int `orm:"f_id,omitempty"`
Name string
}
type InstanceDo struct {
g.Meta `orm:"table:instance, do:true"`
ID any `orm:"f_id,omitempty"`
}
var instance *Instance
err := db.Model("instance").Where(InstanceDo{
ID: 1,
}).Scan(&instance)
t.AssertNil(err)
t.AssertNE(instance, nil)
t.Assert(instance.ID, 1)
t.Assert(instance.Name, "john")
})
}

View File

@ -0,0 +1,177 @@
// 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/gtime"
"github.com/gogf/gf/v2/test/gtest"
)
func Test_Model_LeftJoinOnField(t *testing.T) {
var (
table1 = "t_" + gtime.TimestampNanoStr() + "_table1"
table2 = "t_" + gtime.TimestampNanoStr() + "_table2"
)
createInitTable(table1)
defer dropTable(table1)
createInitTable(table2)
defer dropTable(table2)
gtest.C(t, func(t *gtest.T) {
r, err := db.Model(table1).
FieldsPrefix(table1, "*").
LeftJoinOnField(table2, "id").
WhereIn("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)
})
}
func Test_Model_RightJoinOnField(t *testing.T) {
var (
table1 = "t_" + gtime.TimestampNanoStr() + "_table1"
table2 = "t_" + gtime.TimestampNanoStr() + "_table2"
)
createInitTable(table1)
defer dropTable(table1)
createInitTable(table2)
defer dropTable(table2)
gtest.C(t, func(t *gtest.T) {
r, err := db.Model(table1).
FieldsPrefix(table1, "*").
RightJoinOnField(table2, "id").
WhereIn("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)
})
}
func Test_Model_InnerJoinOnField(t *testing.T) {
var (
table1 = "t_" + gtime.TimestampNanoStr() + "_table1"
table2 = "t_" + gtime.TimestampNanoStr() + "_table2"
)
createInitTable(table1)
defer dropTable(table1)
createInitTable(table2)
defer dropTable(table2)
gtest.C(t, func(t *gtest.T) {
r, err := db.Model(table1).
FieldsPrefix(table1, "*").
InnerJoinOnField(table2, "id").
WhereIn("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)
})
}
func Test_Model_LeftJoinOnFields(t *testing.T) {
var (
table1 = "t_" + gtime.TimestampNanoStr() + "_table1"
table2 = "t_" + gtime.TimestampNanoStr() + "_table2"
)
createInitTable(table1)
defer dropTable(table1)
createInitTable(table2)
defer dropTable(table2)
gtest.C(t, func(t *gtest.T) {
r, err := db.Model(table1).
FieldsPrefix(table1, "*").
LeftJoinOnFields(table2, "id", "=", "id").
WhereIn("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)
})
}
func Test_Model_RightJoinOnFields(t *testing.T) {
var (
table1 = "t_" + gtime.TimestampNanoStr() + "_table1"
table2 = "t_" + gtime.TimestampNanoStr() + "_table2"
)
createInitTable(table1)
defer dropTable(table1)
createInitTable(table2)
defer dropTable(table2)
gtest.C(t, func(t *gtest.T) {
r, err := db.Model(table1).
FieldsPrefix(table1, "*").
RightJoinOnFields(table2, "id", "=", "id").
WhereIn("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)
})
}
func Test_Model_InnerJoinOnFields(t *testing.T) {
var (
table1 = "t_" + gtime.TimestampNanoStr() + "_table1"
table2 = "t_" + gtime.TimestampNanoStr() + "_table2"
)
createInitTable(table1)
defer dropTable(table1)
createInitTable(table2)
defer dropTable(table2)
gtest.C(t, func(t *gtest.T) {
r, err := db.Model(table1).
FieldsPrefix(table1, "*").
InnerJoinOnFields(table2, "id", "=", "id").
WhereIn("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)
})
}
func Test_Model_FieldsPrefix(t *testing.T) {
var (
table1 = "t_" + gtime.TimestampNanoStr() + "_table1"
table2 = "t_" + gtime.TimestampNanoStr() + "_table2"
)
createInitTable(table1)
defer dropTable(table1)
createInitTable(table2)
defer dropTable(table2)
gtest.C(t, func(t *gtest.T) {
r, err := db.Model(table1).
FieldsPrefix(table1, "id").
FieldsPrefix(table2, "nickname").
LeftJoinOnField(table2, "id").
WhereIn("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[0]["nickname"], "name_1")
})
}

View File

@ -0,0 +1,477 @@
// 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"
"reflect"
"testing"
"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/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/test/gtest"
"github.com/gogf/gf/v2/util/gconv"
)
func Test_Model_Embedded_Insert(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type Base struct {
Id int `json:"id"`
CreateTime string `json:"create_time"`
}
type User struct {
Base
Passport string `json:"passport"`
Password string `json:"password"`
Nickname string `json:"nickname"`
}
result, err := db.Model(table).Data(User{
Passport: "john-test",
Password: "123456",
Nickname: "John",
Base: Base{
Id: 100,
CreateTime: gtime.Now().String(),
},
}).Insert()
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 1)
value, err := db.Model(table).Fields("passport").Where("id=100").Value()
t.AssertNil(err)
t.Assert(value.String(), "john-test")
})
}
func Test_Model_Embedded_MapToStruct(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type Ids struct {
Id int `json:"id"`
}
type Base struct {
Ids
CreateTime string `json:"create_time"`
}
type User struct {
Base
Passport string `json:"passport"`
Password string `json:"password"`
Nickname string `json:"nickname"`
}
data := g.Map{
"id": 100,
"passport": "t1",
"password": "123456",
"nickname": "T1",
"create_time": gtime.Now().String(),
}
result, err := db.Model(table).Data(data).Insert()
t.AssertNil(err)
n, _ := result.RowsAffected()
t.Assert(n, 1)
one, err := db.Model(table).Where("id=100").One()
t.AssertNil(err)
user := new(User)
t.Assert(one.Struct(user), nil)
t.Assert(user.Id, data["id"])
t.Assert(user.Passport, data["passport"])
t.Assert(user.Password, data["password"])
t.Assert(user.Nickname, data["nickname"])
t.Assert(user.CreateTime, data["create_time"])
})
}
func Test_Struct_Pointer_Attribute(t *testing.T) {
table := createInitTable()
defer dropTable(table)
type User struct {
Id *int
Passport *string
Password *string
Nickname string
}
gtest.C(t, func(t *gtest.T) {
one, err := db.Model(table).WherePri(1).One()
t.AssertNil(err)
user := new(User)
err = one.Struct(user)
t.AssertNil(err)
t.Assert(*user.Id, 1)
t.Assert(*user.Passport, "user_1")
t.Assert(*user.Password, "pass_1")
t.Assert(user.Nickname, "name_1")
})
gtest.C(t, func(t *gtest.T) {
user := new(User)
err := db.Model(table).Scan(user, "id=1")
t.AssertNil(err)
t.Assert(*user.Id, 1)
t.Assert(*user.Passport, "user_1")
t.Assert(*user.Password, "pass_1")
t.Assert(user.Nickname, "name_1")
})
gtest.C(t, func(t *gtest.T) {
var user *User
err := db.Model(table).Scan(&user, "id=1")
t.AssertNil(err)
t.Assert(*user.Id, 1)
t.Assert(*user.Passport, "user_1")
t.Assert(*user.Password, "pass_1")
t.Assert(user.Nickname, "name_1")
})
}
func Test_Structs_Pointer_Attribute(t *testing.T) {
table := createInitTable()
defer dropTable(table)
type User struct {
Id *int
Passport *string
Password *string
Nickname string
}
// All
gtest.C(t, func(t *gtest.T) {
one, err := db.Model(table).All("id < 3")
t.AssertNil(err)
users := make([]User, 0)
err = one.Structs(&users)
t.AssertNil(err)
t.Assert(len(users), 2)
t.Assert(*users[0].Id, 1)
t.Assert(*users[0].Passport, "user_1")
t.Assert(*users[0].Password, "pass_1")
t.Assert(users[0].Nickname, "name_1")
})
gtest.C(t, func(t *gtest.T) {
one, err := db.Model(table).All("id < 3")
t.AssertNil(err)
users := make([]*User, 0)
err = one.Structs(&users)
t.AssertNil(err)
t.Assert(len(users), 2)
t.Assert(*users[0].Id, 1)
t.Assert(*users[0].Passport, "user_1")
t.Assert(*users[0].Password, "pass_1")
t.Assert(users[0].Nickname, "name_1")
})
gtest.C(t, func(t *gtest.T) {
var users []User
one, err := db.Model(table).All("id < 3")
t.AssertNil(err)
err = one.Structs(&users)
t.AssertNil(err)
t.Assert(len(users), 2)
t.Assert(*users[0].Id, 1)
t.Assert(*users[0].Passport, "user_1")
t.Assert(*users[0].Password, "pass_1")
t.Assert(users[0].Nickname, "name_1")
})
gtest.C(t, func(t *gtest.T) {
var users []*User
one, err := db.Model(table).All("id < 3")
t.AssertNil(err)
err = one.Structs(&users)
t.AssertNil(err)
t.Assert(len(users), 2)
t.Assert(*users[0].Id, 1)
t.Assert(*users[0].Passport, "user_1")
t.Assert(*users[0].Password, "pass_1")
t.Assert(users[0].Nickname, "name_1")
})
// Structs
gtest.C(t, func(t *gtest.T) {
users := make([]User, 0)
err := db.Model(table).Scan(&users, "id < 3")
t.AssertNil(err)
t.Assert(len(users), 2)
t.Assert(*users[0].Id, 1)
t.Assert(*users[0].Passport, "user_1")
t.Assert(*users[0].Password, "pass_1")
t.Assert(users[0].Nickname, "name_1")
})
gtest.C(t, func(t *gtest.T) {
users := make([]*User, 0)
err := db.Model(table).Scan(&users, "id < 3")
t.AssertNil(err)
t.Assert(len(users), 2)
t.Assert(*users[0].Id, 1)
t.Assert(*users[0].Passport, "user_1")
t.Assert(*users[0].Password, "pass_1")
t.Assert(users[0].Nickname, "name_1")
})
gtest.C(t, func(t *gtest.T) {
var users []User
err := db.Model(table).Scan(&users, "id < 3")
t.AssertNil(err)
t.Assert(len(users), 2)
t.Assert(*users[0].Id, 1)
t.Assert(*users[0].Passport, "user_1")
t.Assert(*users[0].Password, "pass_1")
t.Assert(users[0].Nickname, "name_1")
})
gtest.C(t, func(t *gtest.T) {
var users []*User
err := db.Model(table).Scan(&users, "id < 3")
t.AssertNil(err)
t.Assert(len(users), 2)
t.Assert(*users[0].Id, 1)
t.Assert(*users[0].Passport, "user_1")
t.Assert(*users[0].Password, "pass_1")
t.Assert(users[0].Nickname, "name_1")
})
}
func Test_Struct_Empty(t *testing.T) {
table := createTable()
defer dropTable(table)
type User struct {
Id int
Passport string
Password string
Nickname string
}
gtest.C(t, func(t *gtest.T) {
user := new(User)
err := db.Model(table).Where("id=100").Scan(user)
t.Assert(err, sql.ErrNoRows)
t.AssertNE(user, nil)
})
gtest.C(t, func(t *gtest.T) {
one, err := db.Model(table).Where("id=100").One()
t.AssertNil(err)
var user *User
t.Assert(one.Struct(&user), nil)
t.Assert(user, nil)
})
gtest.C(t, func(t *gtest.T) {
var user *User
err := db.Model(table).Where("id=100").Scan(&user)
t.AssertNil(err)
t.Assert(user, nil)
})
}
func Test_Structs_Empty(t *testing.T) {
table := createTable()
defer dropTable(table)
type User struct {
Id int
Passport string
Password string
Nickname string
}
gtest.C(t, func(t *gtest.T) {
all, err := db.Model(table).Where("id>100").All()
t.AssertNil(err)
users := make([]User, 0)
t.Assert(all.Structs(&users), nil)
})
gtest.C(t, func(t *gtest.T) {
all, err := db.Model(table).Where("id>100").All()
t.AssertNil(err)
users := make([]User, 10)
t.Assert(all.Structs(&users), sql.ErrNoRows)
})
gtest.C(t, func(t *gtest.T) {
all, err := db.Model(table).Where("id>100").All()
t.AssertNil(err)
var users []User
t.Assert(all.Structs(&users), nil)
})
gtest.C(t, func(t *gtest.T) {
all, err := db.Model(table).Where("id>100").All()
t.AssertNil(err)
users := make([]*User, 0)
t.Assert(all.Structs(&users), nil)
})
gtest.C(t, func(t *gtest.T) {
all, err := db.Model(table).Where("id>100").All()
t.AssertNil(err)
users := make([]*User, 10)
t.Assert(all.Structs(&users), sql.ErrNoRows)
})
gtest.C(t, func(t *gtest.T) {
all, err := db.Model(table).Where("id>100").All()
t.AssertNil(err)
var users []*User
t.Assert(all.Structs(&users), nil)
})
}
type MyTime struct {
gtime.Time
}
type MyTimeSt struct {
CreateTime MyTime
}
func (st *MyTimeSt) UnmarshalValue(v any) error {
m := gconv.Map(v)
t, err := gtime.StrToTime(gconv.String(m["create_time"]))
if err != nil {
return err
}
st.CreateTime = MyTime{*t}
return nil
}
func Test_Model_Scan_CustomType_Time(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
st := new(MyTimeSt)
err := db.Model(table).Fields("create_time").Scan(st)
t.AssertNil(err)
t.Assert(st.CreateTime.String(), "2018-10-24 10:00:00")
})
gtest.C(t, func(t *gtest.T) {
var stSlice []*MyTimeSt
err := db.Model(table).Fields("create_time").Scan(&stSlice)
t.AssertNil(err)
t.Assert(len(stSlice), TableSize)
t.Assert(stSlice[0].CreateTime.String(), "2018-10-24 10:00:00")
t.Assert(stSlice[9].CreateTime.String(), "2018-10-24 10:00:00")
})
}
func Test_Model_Scan_CustomType_String(t *testing.T) {
type MyString string
type MyStringSt struct {
Passport MyString
}
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
st := new(MyStringSt)
err := db.Model(table).Fields("Passport").WherePri(1).Scan(st)
t.AssertNil(err)
t.Assert(st.Passport, "user_1")
})
gtest.C(t, func(t *gtest.T) {
var sts []MyStringSt
err := db.Model(table).Fields("Passport").Order("id asc").Scan(&sts)
t.AssertNil(err)
t.Assert(len(sts), TableSize)
t.Assert(sts[0].Passport, "user_1")
})
}
type User struct {
Id int
Passport string
Password string
Nickname string
CreateTime *gtime.Time
}
func (user *User) UnmarshalValue(value any) error {
if record, ok := value.(gdb.Record); ok {
*user = User{
Id: record["id"].Int(),
Passport: record["passport"].String(),
Password: "",
Nickname: record["nickname"].String(),
CreateTime: record["create_time"].GTime(),
}
return nil
}
return gerror.NewCodef(gcode.CodeInvalidParameter, `unsupported value type for UnmarshalValue: %v`, reflect.TypeOf(value))
}
func Test_Model_Scan_UnmarshalValue(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
var users []*User
err := db.Model(table).Order("id asc").Scan(&users)
t.AssertNil(err)
t.Assert(len(users), TableSize)
t.Assert(users[0].Id, 1)
t.Assert(users[0].Passport, "user_1")
t.Assert(users[0].Password, "")
t.Assert(users[0].Nickname, "name_1")
t.Assert(users[0].CreateTime.String(), CreateTime)
t.Assert(users[9].Id, 10)
t.Assert(users[9].Passport, "user_10")
t.Assert(users[9].Password, "")
t.Assert(users[9].Nickname, "name_10")
t.Assert(users[9].CreateTime.String(), CreateTime)
})
}
func Test_Model_Scan_Map(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
var users []*User
err := db.Model(table).Order("id asc").Scan(&users)
t.AssertNil(err)
t.Assert(len(users), TableSize)
t.Assert(users[0].Id, 1)
t.Assert(users[0].Passport, "user_1")
t.Assert(users[0].Password, "")
t.Assert(users[0].Nickname, "name_1")
t.Assert(users[0].CreateTime.String(), CreateTime)
t.Assert(users[9].Id, 10)
t.Assert(users[9].Passport, "user_10")
t.Assert(users[9].Password, "")
t.Assert(users[9].Nickname, "name_10")
t.Assert(users[9].CreateTime.String(), CreateTime)
})
}
func Test_Scan_AutoFilteringByStructAttributes(t *testing.T) {
table := createInitTable()
defer dropTable(table)
type User struct {
Id int
Passport string
}
gtest.C(t, func(t *gtest.T) {
var user *User
err := db.Model(table).OrderAsc("id").Scan(&user)
t.AssertNil(err)
t.Assert(user.Id, 1)
})
gtest.C(t, func(t *gtest.T) {
var users []User
err := db.Model(table).OrderAsc("id").Scan(&users)
t.AssertNil(err)
t.Assert(len(users), TableSize)
t.Assert(users[0].Id, 1)
})
}

View File

@ -0,0 +1,66 @@
// 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/test/gtest"
)
func Test_Model_SubQuery_Where(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
r, err := db.Model(table).Where(
"id in ?",
db.Model(table).Fields("id").Where("id", g.Slice{1, 3, 5}),
).OrderAsc("id").All()
t.AssertNil(err)
t.Assert(len(r), 3)
t.Assert(r[0]["id"], 1)
t.Assert(r[1]["id"], 3)
t.Assert(r[2]["id"], 5)
})
}
func Test_Model_SubQuery_Having(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
r, err := db.Model(table).Where(
"id in ?",
db.Model(table).Fields("id").Where("id", g.Slice{1, 3, 5}),
).Group("id").Having(
"id > ?",
db.Model(table).Fields("MAX(id)").Where("id", g.Slice{1, 3}),
).OrderAsc("id").All()
t.AssertNil(err)
t.Assert(len(r), 1)
t.Assert(r[0]["id"], 5)
})
}
func Test_Model_SubQuery_Model(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
subQuery1 := db.Model(table).Where("id", g.Slice{1, 3, 5})
subQuery2 := db.Model(table).Where("id", g.Slice{5, 7, 9})
r, err := db.Model("? AS a, ? AS b", subQuery1, subQuery2).Fields("a.id").Where("a.id=b.id").OrderAsc("id").All()
t.AssertNil(err)
t.Assert(len(r), 1)
t.Assert(r[0]["id"], 5)
})
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,146 @@
// 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/test/gtest"
)
func Test_Union(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
r, err := db.Union(
db.Model(table).Where("id", 1),
db.Model(table).Where("id", 2),
db.Model(table).WhereIn("id", g.Slice{1, 2, 3}).OrderDesc("id"),
).OrderDesc("id").All()
t.AssertNil(err)
t.Assert(len(r), 3)
t.Assert(r[0]["id"], 3)
t.Assert(r[1]["id"], 2)
t.Assert(r[2]["id"], 1)
})
gtest.C(t, func(t *gtest.T) {
r, err := db.Union(
db.Model(table).Where("id", 1),
db.Model(table).Where("id", 2),
db.Model(table).WhereIn("id", g.Slice{1, 2, 3}).OrderDesc("id"),
).OrderDesc("id").One()
t.AssertNil(err)
t.Assert(r["id"], 3)
})
}
func Test_UnionAll(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
r, err := db.UnionAll(
db.Model(table).Where("id", 1),
db.Model(table).Where("id", 2),
db.Model(table).WhereIn("id", g.Slice{1, 2, 3}).OrderDesc("id"),
).OrderDesc("id").All()
t.AssertNil(err)
t.Assert(len(r), 5)
t.Assert(r[0]["id"], 3)
t.Assert(r[1]["id"], 2)
t.Assert(r[2]["id"], 2)
t.Assert(r[3]["id"], 1)
t.Assert(r[4]["id"], 1)
})
gtest.C(t, func(t *gtest.T) {
r, err := db.UnionAll(
db.Model(table).Where("id", 1),
db.Model(table).Where("id", 2),
db.Model(table).WhereIn("id", g.Slice{1, 2, 3}).OrderDesc("id"),
).OrderDesc("id").One()
t.AssertNil(err)
t.Assert(r["id"], 3)
})
}
func Test_Model_Union(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
r, err := db.Model(table).Union(
db.Model(table).Where("id", 1),
db.Model(table).Where("id", 2),
db.Model(table).WhereIn("id", g.Slice{1, 2, 3}).OrderDesc("id"),
).OrderDesc("id").All()
t.AssertNil(err)
t.Assert(len(r), 3)
t.Assert(r[0]["id"], 3)
t.Assert(r[1]["id"], 2)
t.Assert(r[2]["id"], 1)
})
gtest.C(t, func(t *gtest.T) {
r, err := db.Model(table).Union(
db.Model(table).Where("id", 1),
db.Model(table).Where("id", 2),
db.Model(table).WhereIn("id", g.Slice{1, 2, 3}).OrderDesc("id"),
).OrderDesc("id").One()
t.AssertNil(err)
t.Assert(r["id"], 3)
})
}
func Test_Model_UnionAll(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
r, err := db.Model(table).UnionAll(
db.Model(table).Where("id", 1),
db.Model(table).Where("id", 2),
db.Model(table).WhereIn("id", g.Slice{1, 2, 3}).OrderDesc("id"),
).OrderDesc("id").All()
t.AssertNil(err)
t.Assert(len(r), 5)
t.Assert(r[0]["id"], 3)
t.Assert(r[1]["id"], 2)
t.Assert(r[2]["id"], 2)
t.Assert(r[3]["id"], 1)
t.Assert(r[4]["id"], 1)
})
gtest.C(t, func(t *gtest.T) {
r, err := db.Model(table).UnionAll(
db.Model(table).Where("id", 1),
db.Model(table).Where("id", 2),
db.Model(table).WhereIn("id", g.Slice{1, 2, 3}).OrderDesc("id"),
).OrderDesc("id").One()
t.AssertNil(err)
t.Assert(r["id"], 3)
})
}

File diff suppressed because it is too large Load Diff

View File

@ -239,37 +239,6 @@ func Test_Model_Exist(t *testing.T) {
})
}
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)

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@
package gaussdb_test
import (
"context"
"testing"
"github.com/gogf/gf/v2/database/gdb"
@ -97,3 +98,45 @@ func Test_Raw_Update(t *testing.T) {
t.Assert(n, int64(1))
})
}
func Test_Raw_Where(t *testing.T) {
table1 := createTable("test_raw_where_table1")
table2 := createTable("test_raw_where_table2")
defer dropTable(table1)
defer dropTable(table2)
// https://github.com/gogf/gf/issues/3922
gtest.C(t, func(t *gtest.T) {
expectSql := `SELECT * FROM "test_raw_where_table1" AS A WHERE NOT EXISTS (SELECT B.id FROM "test_raw_where_table2" AS B WHERE "B"."id"=A.id) LIMIT 1`
sql, err := gdb.ToSQL(ctx, func(ctx context.Context) error {
s := db.Model(table2).As("B").Ctx(ctx).Fields("B.id").Where("B.id", gdb.Raw("A.id"))
m := db.Model(table1).As("A").Ctx(ctx).Where("NOT EXISTS ?", s).Limit(1)
_, err := m.All()
return err
})
t.AssertNil(err)
t.Assert(expectSql, sql)
})
gtest.C(t, func(t *gtest.T) {
expectSql := `SELECT * FROM "test_raw_where_table1" AS A WHERE NOT EXISTS (SELECT B.id FROM "test_raw_where_table2" AS B WHERE B.id=A.id) LIMIT 1`
sql, err := gdb.ToSQL(ctx, func(ctx context.Context) error {
s := db.Model(table2).As("B").Ctx(ctx).Fields("B.id").Where(gdb.Raw("B.id=A.id"))
m := db.Model(table1).As("A").Ctx(ctx).Where("NOT EXISTS ?", s).Limit(1)
_, err := m.All()
return err
})
t.AssertNil(err)
t.Assert(expectSql, sql)
})
// https://github.com/gogf/gf/issues/3915
gtest.C(t, func(t *gtest.T) {
expectSql := `SELECT * FROM "test_raw_where_table1" WHERE "passport" < "nickname"`
sql, err := gdb.ToSQL(ctx, func(ctx context.Context) error {
m := db.Model(table1).Ctx(ctx).WhereLT("passport", gdb.Raw(`"nickname"`))
_, err := m.All()
return err
})
t.AssertNil(err)
t.Assert(expectSql, sql)
})
}

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ go 1.23.0
require (
gitee.com/opengauss/openGauss-connector-go-pq v1.0.7
github.com/gogf/gf/v2 v2.9.8
github.com/gogf/gf/v2 v2.10.0
github.com/google/uuid v1.6.0
)

View File

@ -0,0 +1,6 @@
DROP TABLE IF EXISTS instance;
CREATE TABLE instance (
f_id SERIAL NOT NULL PRIMARY KEY,
name varchar(255) DEFAULT ''
);
INSERT INTO instance VALUES (1, 'john');

View File

@ -0,0 +1,30 @@
CREATE TABLE table_a (
id SERIAL PRIMARY KEY,
alias varchar(255) DEFAULT ''
);
INSERT INTO table_a VALUES (1, 'table_a_test1');
INSERT INTO table_a VALUES (2, 'table_a_test2');
CREATE TABLE table_b (
id SERIAL PRIMARY KEY,
table_a_id integer NOT NULL,
alias varchar(255) DEFAULT ''
);
INSERT INTO table_b VALUES (10, 1, 'table_b_test1');
INSERT INTO table_b VALUES (20, 2, 'table_b_test2');
INSERT INTO table_b VALUES (30, 1, 'table_b_test3');
INSERT INTO table_b VALUES (40, 2, 'table_b_test4');
CREATE TABLE table_c (
id SERIAL PRIMARY KEY,
table_b_id integer NOT NULL,
alias varchar(255) DEFAULT ''
);
INSERT INTO table_c VALUES (100, 10, 'table_c_test1');
INSERT INTO table_c VALUES (200, 10, 'table_c_test2');
INSERT INTO table_c VALUES (300, 20, 'table_c_test3');
INSERT INTO table_c VALUES (400, 30, 'table_c_test4');

View File

@ -0,0 +1,4 @@
CREATE TABLE IF NOT EXISTS %s (
id SERIAL PRIMARY KEY,
name varchar(45) NOT NULL
);

View File

@ -0,0 +1,4 @@
CREATE TABLE IF NOT EXISTS %s (
uid SERIAL PRIMARY KEY,
address varchar(45) NOT NULL
);

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