mirror of
https://gitee.com/johng/gf
synced 2026-06-07 10:22:11 +08:00
Compare commits
44 Commits
contrib/dr
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| fb89f8bdf8 | |||
| cae8ce3b51 | |||
| 9a91bd203b | |||
| 72733e0bad | |||
| d44e082ff5 | |||
| 078c1bc7f9 | |||
| 94623a19d1 | |||
| cb7cfa58ab | |||
| 1878202625 | |||
| bb71ccfd4c | |||
| f67b2dca26 | |||
| 68b02218d7 | |||
| 766579d868 | |||
| 030cd84836 | |||
| 6314cd4c89 | |||
| 0588009c40 | |||
| 6204c132c7 | |||
| a4b80e8680 | |||
| 0e1cb15dc0 | |||
| 612e545ae2 | |||
| bbdd442954 | |||
| 6686bd65a2 | |||
| 319a812934 | |||
| 307c6ec307 | |||
| bac637570d | |||
| c8a11f7f6e | |||
| e0c032d1b1 | |||
| 063264ebff | |||
| 02abc515a3 | |||
| be7851c664 | |||
| dc08920a7f | |||
| 1ab0b18115 | |||
| ebd78fb533 | |||
| 841003eeb3 | |||
| 1739d4dfb2 | |||
| 46cc4cef9e | |||
| 90331d85bf | |||
| 98fd2a1973 | |||
| d5633ebad7 | |||
| 58d6410291 | |||
| 54087de518 | |||
| fc39fffe9c | |||
| 6a3ea897a8 | |||
| 91f9864b25 |
21
.agents/instructions/markdown-format.instructions.md
Normal file
21
.agents/instructions/markdown-format.instructions.md
Normal file
@ -0,0 +1,21 @@
|
||||
---
|
||||
name: "Standardize markdown document formatting"
|
||||
description: "Standardize the formatting of all markdown documents to keep structure clear, content readable, and the overall quality and user experience consistent. This document explains requirements for heading levels, paragraph formatting, code block usage, list formatting, and image and link insertion so authors can follow a unified style that is easier to read and maintain."
|
||||
applyTo: "*.{md,MD}"
|
||||
---
|
||||
|
||||
# Primary Formatting Requirements
|
||||
|
||||
- Keywords or specialized terms in the document must be formatted with inline code, for example `RuntimeClass`, `containerd`, `GPU`, and `AI`.
|
||||
- In Chinese text, do not add spaces around inline code.
|
||||
- For technical articles, review the generated content before finalizing it to ensure the material is technically accurate and contains no incorrect technical descriptions.
|
||||
- When the generated content is too large, split it into multiple tasks to avoid exceeding model output limits and causing the workflow to fail.
|
||||
|
||||
|
||||
# Detailed Content Requirements
|
||||
|
||||
- When documenting parameters or configuration items for a component or project, prefer tables when practical, and keep tables short enough to avoid horizontal scrolling during normal reading.
|
||||
- In Chinese paragraphs, use full-width punctuation rather than half-width punctuation, for example `,` instead of `,` and `;` instead of `;`.
|
||||
- Use `mermaid` for architecture diagrams, flowcharts, and similar visuals. If you need line breaks inside `mermaid`, use `<br/>` instead of `\n`.
|
||||
- If a code block is not a `mermaid` diagram and instead uses box-drawing characters such as `┌─`, `┐`, `┤`, or `│`, keep the content in English so the layout stays aligned.
|
||||
- Do not use `---` as a separator between paragraphs.
|
||||
152
.agents/prompts/opsx/apply.md
Normal file
152
.agents/prompts/opsx/apply.md
Normal file
@ -0,0 +1,152 @@
|
||||
---
|
||||
name: "OPSX: Apply"
|
||||
description: Implement tasks from an OpenSpec change (Experimental)
|
||||
category: Workflow
|
||||
tags: [workflow, artifacts, experimental]
|
||||
---
|
||||
|
||||
Implement tasks from an OpenSpec change.
|
||||
|
||||
**Input**: Optionally specify a change name (e.g., `/opsx:apply add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Select the change**
|
||||
|
||||
If a name is provided, use it. Otherwise:
|
||||
- Infer from conversation context if the user mentioned a change
|
||||
- Auto-select if only one active change exists
|
||||
- If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
|
||||
|
||||
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
|
||||
|
||||
2. **Check status to understand the schema**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
||||
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
||||
|
||||
3. **Get apply instructions**
|
||||
|
||||
```bash
|
||||
openspec instructions apply --change "<name>" --json
|
||||
```
|
||||
|
||||
This returns:
|
||||
- Context file paths (varies by schema)
|
||||
- Progress (total, complete, remaining)
|
||||
- Task list with status
|
||||
- Dynamic instruction based on current state
|
||||
|
||||
**Handle states:**
|
||||
- If `state: "blocked"` (missing artifacts): show message, suggest using `/opsx:continue`
|
||||
- If `state: "all_done"`: congratulate, suggest archive
|
||||
- Otherwise: proceed to implementation
|
||||
|
||||
4. **Read context files**
|
||||
|
||||
Read the files listed in `contextFiles` from the apply instructions output.
|
||||
The files depend on the schema being used:
|
||||
- **spec-driven**: proposal, specs, design, tasks
|
||||
- Other schemas: follow the contextFiles from CLI output
|
||||
|
||||
5. **Show current progress**
|
||||
|
||||
Display:
|
||||
- Schema being used
|
||||
- Progress: "N/M tasks complete"
|
||||
- Remaining tasks overview
|
||||
- Dynamic instruction from CLI
|
||||
|
||||
6. **Implement tasks (loop until done or blocked)**
|
||||
|
||||
For each pending task:
|
||||
- Show which task is being worked on
|
||||
- Make the code changes required
|
||||
- Keep changes minimal and focused
|
||||
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
|
||||
- Continue to next task
|
||||
|
||||
**Pause if:**
|
||||
- Task is unclear → ask for clarification
|
||||
- Implementation reveals a design issue → suggest updating artifacts
|
||||
- Error or blocker encountered → report and wait for guidance
|
||||
- User interrupts
|
||||
|
||||
7. **On completion or pause, show status**
|
||||
|
||||
Display:
|
||||
- Tasks completed this session
|
||||
- Overall progress: "N/M tasks complete"
|
||||
- If all done: suggest archive
|
||||
- If paused: explain why and wait for guidance
|
||||
|
||||
**Output During Implementation**
|
||||
|
||||
```
|
||||
## Implementing: <change-name> (schema: <schema-name>)
|
||||
|
||||
Working on task 3/7: <task description>
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
|
||||
Working on task 4/7: <task description>
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
```
|
||||
|
||||
**Output On Completion**
|
||||
|
||||
```
|
||||
## Implementation Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Progress:** 7/7 tasks complete ✓
|
||||
|
||||
### Completed This Session
|
||||
- [x] Task 1
|
||||
- [x] Task 2
|
||||
...
|
||||
|
||||
All tasks complete! You can archive this change with `/opsx:archive`.
|
||||
```
|
||||
|
||||
**Output On Pause (Issue Encountered)**
|
||||
|
||||
```
|
||||
## Implementation Paused
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Progress:** 4/7 tasks complete
|
||||
|
||||
### Issue Encountered
|
||||
<description of the issue>
|
||||
|
||||
**Options:**
|
||||
1. <option 1>
|
||||
2. <option 2>
|
||||
3. Other approach
|
||||
|
||||
What would you like to do?
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Keep going through tasks until done or blocked
|
||||
- Always read context files before starting (from the apply instructions output)
|
||||
- If task is ambiguous, pause and ask before implementing
|
||||
- If implementation reveals issues, pause and suggest artifact updates
|
||||
- Keep code changes minimal and scoped to each task
|
||||
- Update task checkbox immediately after completing each task
|
||||
- Pause on errors, blockers, or unclear requirements - don't guess
|
||||
- Use contextFiles from CLI output, don't assume specific file names
|
||||
|
||||
**Fluid Workflow Integration**
|
||||
|
||||
This skill supports the "actions on a change" model:
|
||||
|
||||
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
|
||||
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly
|
||||
157
.agents/prompts/opsx/archive.md
Normal file
157
.agents/prompts/opsx/archive.md
Normal file
@ -0,0 +1,157 @@
|
||||
---
|
||||
name: "OPSX: Archive"
|
||||
description: Archive a completed change in the experimental workflow
|
||||
category: Workflow
|
||||
tags: [workflow, archive, experimental]
|
||||
---
|
||||
|
||||
Archive a completed change in the experimental workflow.
|
||||
|
||||
**Input**: Optionally specify a change name after `/opsx:archive` (e.g., `/opsx:archive add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||
|
||||
Show only active changes (not already archived).
|
||||
Include the schema used for each change if available.
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Check artifact completion status**
|
||||
|
||||
Run `openspec status --change "<name>" --json` to check artifact completion.
|
||||
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used
|
||||
- `artifacts`: List of artifacts with their status (`done` or other)
|
||||
|
||||
**If any artifacts are not `done`:**
|
||||
- Display warning listing incomplete artifacts
|
||||
- Prompt user for confirmation to continue
|
||||
- Proceed if user confirms
|
||||
|
||||
3. **Check task completion status**
|
||||
|
||||
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
|
||||
|
||||
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
|
||||
|
||||
**If incomplete tasks found:**
|
||||
- Display warning showing count of incomplete tasks
|
||||
- Prompt user for confirmation to continue
|
||||
- Proceed if user confirms
|
||||
|
||||
**If no tasks file exists:** Proceed without task-related warning.
|
||||
|
||||
4. **Assess delta spec sync state**
|
||||
|
||||
Check for delta specs at `openspec/changes/<name>/specs/`. If none exist, proceed without sync prompt.
|
||||
|
||||
**If delta specs exist:**
|
||||
- Compare each delta spec with its corresponding main spec at `openspec/specs/<capability>/spec.md`
|
||||
- Determine what changes would be applied (adds, modifications, removals, renames)
|
||||
- Show a combined summary before prompting
|
||||
|
||||
**Prompt options:**
|
||||
- If changes needed: "Sync now (recommended)", "Archive without syncing"
|
||||
- If already synced: "Archive now", "Sync anyway", "Cancel"
|
||||
|
||||
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
|
||||
|
||||
5. **Perform the archive**
|
||||
|
||||
Create the archive directory if it doesn't exist:
|
||||
```bash
|
||||
mkdir -p openspec/changes/archive
|
||||
```
|
||||
|
||||
Generate target name using current date: `YYYY-MM-DD-<change-name>`
|
||||
|
||||
**Check if target already exists:**
|
||||
- If yes: Fail with error, suggest renaming existing archive or using different date
|
||||
- If no: Move the change directory to archive
|
||||
|
||||
```bash
|
||||
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
||||
```
|
||||
|
||||
6. **Display summary**
|
||||
|
||||
Show archive completion summary including:
|
||||
- Change name
|
||||
- Schema that was used
|
||||
- Archive location
|
||||
- Spec sync status (synced / sync skipped / no delta specs)
|
||||
- Note about any warnings (incomplete artifacts/tasks)
|
||||
|
||||
**Output On Success**
|
||||
|
||||
```
|
||||
## Archive Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
**Specs:** ✓ Synced to main specs
|
||||
|
||||
All artifacts complete. All tasks complete.
|
||||
```
|
||||
|
||||
**Output On Success (No Delta Specs)**
|
||||
|
||||
```
|
||||
## Archive Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
**Specs:** No delta specs
|
||||
|
||||
All artifacts complete. All tasks complete.
|
||||
```
|
||||
|
||||
**Output On Success With Warnings**
|
||||
|
||||
```
|
||||
## Archive Complete (with warnings)
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
**Specs:** Sync skipped (user chose to skip)
|
||||
|
||||
**Warnings:**
|
||||
- Archived with 2 incomplete artifacts
|
||||
- Archived with 3 incomplete tasks
|
||||
- Delta spec sync was skipped (user chose to skip)
|
||||
|
||||
Review the archive if this was not intentional.
|
||||
```
|
||||
|
||||
**Output On Error (Archive Exists)**
|
||||
|
||||
```
|
||||
## Archive Failed
|
||||
|
||||
**Change:** <change-name>
|
||||
**Target:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
|
||||
Target archive directory already exists.
|
||||
|
||||
**Options:**
|
||||
1. Rename the existing archive
|
||||
2. Delete the existing archive if it's a duplicate
|
||||
3. Wait until a different date to archive
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
- Always prompt for change selection if not provided
|
||||
- Use artifact graph (openspec status --json) for completion checking
|
||||
- Don't block archive on warnings - just inform and confirm
|
||||
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
|
||||
- Show clear summary of what happened
|
||||
- If sync is requested, use the Skill tool to invoke `openspec-sync-specs` (agent-driven)
|
||||
- If delta specs exist, always run the sync assessment and show the combined summary before prompting
|
||||
173
.agents/prompts/opsx/explore.md
Normal file
173
.agents/prompts/opsx/explore.md
Normal file
@ -0,0 +1,173 @@
|
||||
---
|
||||
name: "OPSX: Explore"
|
||||
description: "Enter explore mode - think through ideas, investigate problems, clarify requirements"
|
||||
category: Workflow
|
||||
tags: [workflow, explore, experimental, thinking]
|
||||
---
|
||||
|
||||
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
|
||||
|
||||
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
|
||||
|
||||
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
|
||||
|
||||
**Input**: The argument after `/opsx:explore` is whatever the user wants to think about. Could be:
|
||||
- A vague idea: "real-time collaboration"
|
||||
- A specific problem: "the auth system is getting unwieldy"
|
||||
- A change name: "add-dark-mode" (to explore in context of that change)
|
||||
- A comparison: "postgres vs sqlite for this"
|
||||
- Nothing (just enter explore mode)
|
||||
|
||||
---
|
||||
|
||||
## The Stance
|
||||
|
||||
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
|
||||
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
|
||||
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
|
||||
- **Adaptive** - Follow interesting threads, pivot when new information emerges
|
||||
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
|
||||
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
|
||||
|
||||
---
|
||||
|
||||
## What You Might Do
|
||||
|
||||
Depending on what the user brings, you might:
|
||||
|
||||
**Explore the problem space**
|
||||
- Ask clarifying questions that emerge from what they said
|
||||
- Challenge assumptions
|
||||
- Reframe the problem
|
||||
- Find analogies
|
||||
|
||||
**Investigate the codebase**
|
||||
- Map existing architecture relevant to the discussion
|
||||
- Find integration points
|
||||
- Identify patterns already in use
|
||||
- Surface hidden complexity
|
||||
|
||||
**Compare options**
|
||||
- Brainstorm multiple approaches
|
||||
- Build comparison tables
|
||||
- Sketch tradeoffs
|
||||
- Recommend a path (if asked)
|
||||
|
||||
**Visualize**
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Use ASCII diagrams liberally │
|
||||
├─────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌────────┐ ┌────────┐ │
|
||||
│ │ State │────────▶│ State │ │
|
||||
│ │ A │ │ B │ │
|
||||
│ └────────┘ └────────┘ │
|
||||
│ │
|
||||
│ System diagrams, state machines, │
|
||||
│ data flows, architecture sketches, │
|
||||
│ dependency graphs, comparison tables │
|
||||
│ │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Surface risks and unknowns**
|
||||
- Identify what could go wrong
|
||||
- Find gaps in understanding
|
||||
- Suggest spikes or investigations
|
||||
|
||||
---
|
||||
|
||||
## OpenSpec Awareness
|
||||
|
||||
You have full context of the OpenSpec system. Use it naturally, don't force it.
|
||||
|
||||
### Check for context
|
||||
|
||||
At the start, quickly check what exists:
|
||||
```bash
|
||||
openspec list --json
|
||||
```
|
||||
|
||||
This tells you:
|
||||
- If there are active changes
|
||||
- Their names, schemas, and status
|
||||
- What the user might be working on
|
||||
|
||||
If the user mentioned a specific change name, read its artifacts for context.
|
||||
|
||||
### When no change exists
|
||||
|
||||
Think freely. When insights crystallize, you might offer:
|
||||
|
||||
- "This feels solid enough to start a change. Want me to create a proposal?"
|
||||
- Or keep exploring - no pressure to formalize
|
||||
|
||||
### When a change exists
|
||||
|
||||
If the user mentions a change or you detect one is relevant:
|
||||
|
||||
1. **Read existing artifacts for context**
|
||||
- `openspec/changes/<name>/proposal.md`
|
||||
- `openspec/changes/<name>/design.md`
|
||||
- `openspec/changes/<name>/tasks.md`
|
||||
- etc.
|
||||
|
||||
2. **Reference them naturally in conversation**
|
||||
- "Your design mentions using Redis, but we just realized SQLite fits better..."
|
||||
- "The proposal scopes this to premium users, but we're now thinking everyone..."
|
||||
|
||||
3. **Offer to capture when decisions are made**
|
||||
|
||||
| Insight Type | Where to Capture |
|
||||
|--------------|------------------|
|
||||
| New requirement discovered | `specs/<capability>/spec.md` |
|
||||
| Requirement changed | `specs/<capability>/spec.md` |
|
||||
| Design decision made | `design.md` |
|
||||
| Scope changed | `proposal.md` |
|
||||
| New work identified | `tasks.md` |
|
||||
| Assumption invalidated | Relevant artifact |
|
||||
|
||||
Example offers:
|
||||
- "That's a design decision. Capture it in design.md?"
|
||||
- "This is a new requirement. Add it to specs?"
|
||||
- "This changes scope. Update the proposal?"
|
||||
|
||||
4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
|
||||
|
||||
---
|
||||
|
||||
## What You Don't Have To Do
|
||||
|
||||
- Follow a script
|
||||
- Ask the same questions every time
|
||||
- Produce a specific artifact
|
||||
- Reach a conclusion
|
||||
- Stay on topic if a tangent is valuable
|
||||
- Be brief (this is thinking time)
|
||||
|
||||
---
|
||||
|
||||
## Ending Discovery
|
||||
|
||||
There's no required ending. Discovery might:
|
||||
|
||||
- **Flow into a proposal**: "Ready to start? I can create a change proposal."
|
||||
- **Result in artifact updates**: "Updated design.md with these decisions"
|
||||
- **Just provide clarity**: User has what they need, moves on
|
||||
- **Continue later**: "We can pick this up anytime"
|
||||
|
||||
When things crystallize, you might offer a summary - but it's optional. Sometimes the thinking IS the value.
|
||||
|
||||
---
|
||||
|
||||
## Guardrails
|
||||
|
||||
- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
|
||||
- **Don't fake understanding** - If something is unclear, dig deeper
|
||||
- **Don't rush** - Discovery is thinking time, not task time
|
||||
- **Don't force structure** - Let patterns emerge naturally
|
||||
- **Don't auto-capture** - Offer to save insights, don't just do it
|
||||
- **Do visualize** - A good diagram is worth many paragraphs
|
||||
- **Do explore the codebase** - Ground discussions in reality
|
||||
- **Do question assumptions** - Including the user's and your own
|
||||
106
.agents/prompts/opsx/propose.md
Normal file
106
.agents/prompts/opsx/propose.md
Normal file
@ -0,0 +1,106 @@
|
||||
---
|
||||
name: "OPSX: Propose"
|
||||
description: Propose a new change - create it and generate all artifacts in one step
|
||||
category: Workflow
|
||||
tags: [workflow, artifacts, experimental]
|
||||
---
|
||||
|
||||
Propose a new change - create the change and generate all artifacts in one step.
|
||||
|
||||
I'll create a change with artifacts:
|
||||
- proposal.md (what & why)
|
||||
- design.md (how)
|
||||
- tasks.md (implementation steps)
|
||||
|
||||
When ready to implement, run /opsx:apply
|
||||
|
||||
---
|
||||
|
||||
**Input**: The argument after `/opsx:propose` is the change name (kebab-case), OR a description of what the user wants to build.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no input provided, ask what they want to build**
|
||||
|
||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||
> "What change do you want to work on? Describe what you want to build or fix."
|
||||
|
||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||
|
||||
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||
|
||||
2. **Create the change directory**
|
||||
```bash
|
||||
openspec new change "<name>"
|
||||
```
|
||||
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
|
||||
|
||||
3. **Get the artifact build order**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to get:
|
||||
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
||||
- `artifacts`: list of all artifacts with their status and dependencies
|
||||
|
||||
4. **Create artifacts in sequence until apply-ready**
|
||||
|
||||
Use the **TodoWrite tool** to track progress through the artifacts.
|
||||
|
||||
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
||||
|
||||
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
||||
- Get instructions:
|
||||
```bash
|
||||
openspec instructions <artifact-id> --change "<name>" --json
|
||||
```
|
||||
- The instructions JSON includes:
|
||||
- `context`: Project background (constraints for you - do NOT include in output)
|
||||
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
||||
- `template`: The structure to use for your output file
|
||||
- `instruction`: Schema-specific guidance for this artifact type
|
||||
- `outputPath`: Where to write the artifact
|
||||
- `dependencies`: Completed artifacts to read for context
|
||||
- Read any completed dependency files for context
|
||||
- Create the artifact file using `template` as the structure
|
||||
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
||||
- Show brief progress: "Created <artifact-id>"
|
||||
|
||||
b. **Continue until all `applyRequires` artifacts are complete**
|
||||
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
||||
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
||||
- Stop when all `applyRequires` artifacts are done
|
||||
|
||||
c. **If an artifact requires user input** (unclear context):
|
||||
- Use **AskUserQuestion tool** to clarify
|
||||
- Then continue with creation
|
||||
|
||||
5. **Show final status**
|
||||
```bash
|
||||
openspec status --change "<name>"
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
After completing all artifacts, summarize:
|
||||
- Change name and location
|
||||
- List of artifacts created with brief descriptions
|
||||
- What's ready: "All artifacts created! Ready for implementation."
|
||||
- Prompt: "Run `/opsx:apply` to start implementing."
|
||||
|
||||
**Artifact Creation Guidelines**
|
||||
|
||||
- Follow the `instruction` field from `openspec instructions` for each artifact type
|
||||
- The schema defines what each artifact should contain - follow it
|
||||
- Read dependency artifacts for context before creating new ones
|
||||
- Use `template` as the structure for your output file - fill in its sections
|
||||
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
||||
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
||||
- These guide what you write, but should never appear in the output
|
||||
|
||||
**Guardrails**
|
||||
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
||||
- Always read dependency artifacts before creating a new one
|
||||
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
||||
- If a change with that name already exists, ask if user wants to continue it or create a new one
|
||||
- Verify each artifact file exists after writing before proceeding to next
|
||||
280
.agents/skills/gf-feedback/SKILL.md
Normal file
280
.agents/skills/gf-feedback/SKILL.md
Normal file
@ -0,0 +1,280 @@
|
||||
---
|
||||
name: gf-feedback
|
||||
description: >-
|
||||
Track, fix, verify, and test any bugs, improvements, or gaps reported against an OpenSpec change.
|
||||
MUST use this skill whenever user reports problems, defects, issues, bugs, or gaps related to
|
||||
existing implementations, even if they don't explicitly say "feedback" or mention OpenSpec.
|
||||
compatibility: Requires openspec CLI, Go toolchain, and gf-review skill.
|
||||
---
|
||||
|
||||
# Feedback: Structured Fix, Verification & Test Coverage Loop
|
||||
|
||||
When users discover bugs or improvement points after implementation, this skill captures those issues, organizes them into a traceable task list in `tasks.md`, systematically fixes and verifies each one, and ensures every behavior-changing fix is covered by focused unit tests.
|
||||
|
||||
**Core principles:**
|
||||
1. **Spec is the source of truth** — Spec-level changes require spec update before task recording
|
||||
2. **Write it down first, then fix it** — Every issue gets recorded before any code change
|
||||
3. **Every fix deserves a test** — Behavior-changing code changes require unit test coverage in the affected package
|
||||
|
||||
---
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Identify Target Change
|
||||
|
||||
**CRITICAL:**
|
||||
1. Always append to existing active changes. Only create new change when none exist.
|
||||
2. An **active change** is any change directory that still exists directly under `openspec/changes/` and has **not** been moved into `openspec/changes/archive/`. Do **not** treat `status: complete`, all tasks checked off, or similar completion signals as "inactive" until archive actually happens.
|
||||
3. Regardless of whether the feedback content is related to the main functionality of the current active iteration, it MUST be appended to the current active iteration. This ensures all changes are tracked in a single change record for unified management and archiving.
|
||||
|
||||
```bash
|
||||
openspec list --json
|
||||
# Or: ls openspec/changes/ | grep -v archive
|
||||
```
|
||||
|
||||
When the two signals disagree, prefer the filesystem rule:
|
||||
|
||||
- If a change directory still exists under `openspec/changes/` and is not inside `archive/`, it is active.
|
||||
- `openspec list --json` may still report such a change as `status: complete`; that only means implementation tasks are done, **not** that the change is inactive.
|
||||
- Only archived changes under `openspec/changes/archive/` are inactive.
|
||||
|
||||
| Active Changes | Action |
|
||||
|----------------|--------|
|
||||
| None | Create new change (see below) |
|
||||
| One | Auto-select it, announce and proceed |
|
||||
| Multiple | Ask user to select from list |
|
||||
|
||||
**When multiple active changes exist:**
|
||||
```
|
||||
Multiple active changes detected. Which change should this feedback be appended to?
|
||||
|
||||
1. config-management — System config CRUD management
|
||||
2. user-auth — User authentication enhancement
|
||||
|
||||
Please select 1 or 2:
|
||||
```
|
||||
|
||||
**When no active change exists:**
|
||||
1. Derive kebab-case name from feedback (e.g., "fix-menu-circular-ref")
|
||||
2. If name exists, append suffix ("-2")
|
||||
3. Create: `openspec new change "<name>"`
|
||||
4. Generate minimal `proposal.md` (one paragraph summarizing context)
|
||||
5. Skip `design.md` for pure bug fixes unless architectural changes needed
|
||||
|
||||
Announce: "Applying feedback fixes to change: **<name>**"
|
||||
|
||||
---
|
||||
|
||||
### 2. Read Current Context
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `tasks.md` | Task structure, naming conventions, numbering |
|
||||
| `design.md` | Architectural context |
|
||||
| `proposal.md` | Feature scope and intent |
|
||||
| `specs/` | Delta spec definitions |
|
||||
|
||||
```bash
|
||||
# Locate existing unit tests in the impacted package before adding a new one
|
||||
rg --files <pkg-dir> | rg '_test\\.go$'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Analyze and Organize Issues
|
||||
|
||||
For each reported issue:
|
||||
|
||||
**Classify by type:**
|
||||
- **bug** — Incorrect behavior, code doesn't match spec
|
||||
- **missing** — Feature incomplete, gaps in implementation
|
||||
- **ux** — UX improvement, no spec change needed
|
||||
- **test-gap** — Missing test coverage only
|
||||
|
||||
**Classify by spec impact:**
|
||||
|
||||
| Level | Definition | Action |
|
||||
|-------|------------|--------|
|
||||
| **implementation** | Spec is correct, code is wrong | Fix code only |
|
||||
| **spec-level** | Requirement missing/incomplete/changed | Update spec first, then fix |
|
||||
| **internal** | No user-observable change | Fix code, test optional |
|
||||
|
||||
**Group related issues** — Same root cause → single task with multiple verification points.
|
||||
|
||||
---
|
||||
|
||||
### 4. Update Delta Specs (for Spec-Level Issues Only)
|
||||
|
||||
For spec-level issues, update specs **before** recording tasks:
|
||||
|
||||
1. Identify affected capability: `specs/<capability>/spec.md`
|
||||
2. Apply delta operation:
|
||||
|
||||
```markdown
|
||||
<!-- ADDED: New requirement -->
|
||||
### Requirement: Parent Selector Circular Prevention
|
||||
The system SHALL disable the current menu and all its descendants in the parent selector
|
||||
to prevent circular references.
|
||||
|
||||
#### Scenario: Edit menu with children
|
||||
WHEN user edits a menu that has child menus
|
||||
THEN the parent selector SHALL disable the current menu and all descendant menus
|
||||
|
||||
<!-- MODIFIED: Changed requirement (include full original block) -->
|
||||
### Requirement: Import Error Handling
|
||||
The system SHALL display error messages when import fails.
|
||||
**MODIFIED:** Error messages SHALL include row number, field name, and validation failure reason.
|
||||
|
||||
<!-- REMOVED: Deprecated requirement -->
|
||||
### Requirement: Legacy Import Format
|
||||
The system SHALL support legacy CSV format.
|
||||
**REMOVED:** This format is no longer supported.
|
||||
**Migration:** Use the new CSV format with header row.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Write Task List to tasks.md
|
||||
|
||||
Append a **Feedback section** to `tasks.md`:
|
||||
|
||||
```markdown
|
||||
## Feedback
|
||||
|
||||
- [ ] **FB-1**: Parent selector allows circular references in menu edit
|
||||
- [ ] **FB-2**: Import error messages lack row and field details
|
||||
- [ ] **FB-3**: No test coverage for reset password feature
|
||||
```
|
||||
|
||||
**Numbering:** Sequential `FB-1`, `FB-2`, etc. Continue from last number if section exists.
|
||||
|
||||
**One line per task** — No sub-fields. Analysis happens during fix phase.
|
||||
|
||||
**Confirm with user** before writing to file.
|
||||
|
||||
**Test coverage planning (internal):**
|
||||
- Behavior-changing code change → Unit test required
|
||||
- Internal-only optimization → Unit test optional unless logic risk increased
|
||||
- Prefer extending the nearest `*_z_unit*_test.go` or `*_test.go` in the same package
|
||||
|
||||
---
|
||||
|
||||
### 6. Execute Fixes (Loop)
|
||||
|
||||
For each task:
|
||||
|
||||
**a. Announce:** `## Fixing FB-X: <issue title>`
|
||||
|
||||
**b. Investigate** — Read source files, confirm root cause
|
||||
|
||||
**c. Implement** — Minimal, focused fix following existing patterns
|
||||
|
||||
**d. Write/update unit tests** — Prefer the affected package's existing `*_z_unit*_test.go` or `*_test.go` files and keep assertions focused on the changed logic
|
||||
|
||||
**e. Assess Impact Scope (MANDATORY)**
|
||||
|
||||
After implementing, identify regression risk:
|
||||
|
||||
| Change Type | Map To Tests |
|
||||
|-------------|--------------|
|
||||
| Package-level logic | Targeted test for changed function/method + package regression tests |
|
||||
| Shared utility | Utility package unit tests + highest-value dependent package tests already covering reuse |
|
||||
| DB/DAO logic | DAO/model package unit tests with focused fixtures, mocks, or test helpers |
|
||||
| Public API validation | Handler/service package unit tests that assert the changed validation path |
|
||||
| Refactor without behavior change | Existing package tests that prove behavior parity |
|
||||
|
||||
```bash
|
||||
# Example: Find unit tests related to a changed symbol or package
|
||||
rg -l "GenDao|gdao" . -g '*_test.go'
|
||||
```
|
||||
|
||||
Announce:
|
||||
```
|
||||
### Impact Analysis for FB-X
|
||||
- Modified: cmd/gf/internal/cmd/gendao/gendao.go
|
||||
- Affected package: cmd/gf/internal/cmd/gendao
|
||||
- Unit tests: cmd/gf/internal/cmd/gendao/gendao_test.go
|
||||
- Regression command: go test ./cmd/gf/internal/cmd/gendao -run 'TestGenDao'
|
||||
```
|
||||
|
||||
**f. Verify (MANDATORY before marking complete)**
|
||||
|
||||
1. Run new/updated unit tests for this task → **must pass**
|
||||
2. Run ALL identified package-level regression tests → **must pass**
|
||||
3. Only then: mark task `[x]` in tasks.md
|
||||
|
||||
If regression fails:
|
||||
- Fix inline if related to current change
|
||||
- Add as new FB task if separate issue
|
||||
|
||||
**g. Run review** — Invoke `gf-review` skill after completion
|
||||
|
||||
---
|
||||
|
||||
### 7. Comprehensive Verification
|
||||
|
||||
After all fixes:
|
||||
|
||||
1. Aggregate regression tests from all tasks
|
||||
2. Run full set in single pass
|
||||
3. Report:
|
||||
```
|
||||
### Comprehensive Verification Results
|
||||
- Total tests: N
|
||||
- Passed: N
|
||||
- Failed: N (list with details)
|
||||
- Regression tests: all passed ✓ / X failures
|
||||
```
|
||||
|
||||
If failures → add new FB tasks, loop to Step 6.
|
||||
|
||||
---
|
||||
|
||||
### 8. Report Completion
|
||||
|
||||
```markdown
|
||||
## Feedback Complete
|
||||
|
||||
**Change:** <name>
|
||||
**Issues reported:** X
|
||||
**Issues fixed:** Y/X
|
||||
**Tests added:** Z unit tests / focused assertions
|
||||
**Regression tests run:** R tests across N packages
|
||||
**Verification:** all passed / N issues remaining
|
||||
|
||||
### Fixed This Session
|
||||
- [x] FB-1: <title> ✓ (unit test: TestGenDao_FiltersInvalidTables | package: ./cmd/gf/internal/cmd/gendao ✓)
|
||||
- [x] FB-2: <title> ✓ (unit test: existing package coverage extended | package: ./cmd/gf/internal/cmd ✓)
|
||||
|
||||
### Remaining (if any)
|
||||
- [ ] FB-3: <title> — blocked by <reason>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Edge Cases
|
||||
|
||||
| Situation | Handling |
|
||||
|-----------|----------|
|
||||
| Single issue | Still follow full workflow |
|
||||
| Missing test cases only | Classify as test-gap, implement tests |
|
||||
| Fix reveals more issues | Add as new FB tasks |
|
||||
| "Bug" is actually feature request | Re-classify as spec-level, update specs first |
|
||||
| Unit test not feasible (docs/spec only) | Note reason explicitly and skip only when no runtime code changes exist |
|
||||
| Multiple feedback rounds | All tasks in single Feedback section, sequential numbering |
|
||||
|
||||
---
|
||||
|
||||
## Guardrails
|
||||
|
||||
- **Append to active change if exists** — Never create new change when active ones exist
|
||||
- **Specs before tasks for spec-level issues** — Update delta specs first
|
||||
- **Write tasks before fixing** — Never code without recording
|
||||
- **Confirm task list with user** — User validates analysis
|
||||
- **Minimal fixes** — No refactoring beyond issue scope
|
||||
- **Behavior-changing fix needs unit test** — No exceptions unless the change is docs/spec only
|
||||
- **No green check without green unit tests** — Mark `[x]` only after tests pass
|
||||
- **Impact analysis mandatory** — Every fix requires package-level regression test identification
|
||||
- **Regression failures block completion** — Must resolve before marking done
|
||||
- **Update tasks.md in real time** — Mark complete immediately after verification
|
||||
- **Match file language** — Use same language as existing content in target file
|
||||
168
.agents/skills/gf-review/SKILL.md
Normal file
168
.agents/skills/gf-review/SKILL.md
Normal file
@ -0,0 +1,168 @@
|
||||
---
|
||||
name: gf-review
|
||||
description: >-
|
||||
Code and specification review for OpenSpec workflow. Triggers automatically after /opsx:apply
|
||||
task completion, after /gf-feedback task completion, and before /opsx:archive. Use when
|
||||
user requests code review, spec compliance check, or when explicitly invoked via /gf-review.
|
||||
compatibility: Requires OpenSpec CLI and GoFrame v2 skill.
|
||||
---
|
||||
|
||||
# GF Review
|
||||
|
||||
Structured code and specification review for the OpenSpec development workflow.
|
||||
|
||||
**Spec Source**: `CLAUDE.md` is the single source of truth for all review criteria.
|
||||
|
||||
---
|
||||
|
||||
## When This Skill Activates
|
||||
|
||||
**Automatic triggers:**
|
||||
- After completing each task in `/opsx:apply`
|
||||
- After completing each task in `/gf-feedback`
|
||||
- Before executing `/opsx:archive`
|
||||
|
||||
**Manual trigger:**
|
||||
- User explicitly requests: "review this code", "check spec compliance", "/gf-review"
|
||||
|
||||
---
|
||||
|
||||
## Review Workflow
|
||||
|
||||
### 1. Identify Scope
|
||||
|
||||
Determine what needs to be reviewed:
|
||||
|
||||
1. **After task completion** — Review files modified/created by the completed task
|
||||
2. **Before archive** — Review all changes in the current OpenSpec change
|
||||
3. **Manual invocation** — Ask user to specify scope or use current change
|
||||
|
||||
**Mandatory scope collection rules:**
|
||||
|
||||
1. Start with repository status, not `git diff` alone:
|
||||
```bash
|
||||
git status --short
|
||||
git ls-files --others --exclude-standard
|
||||
```
|
||||
2. Treat **all tracked and untracked changes** as review candidates, including:
|
||||
- staged files
|
||||
- unstaged files
|
||||
- untracked files shown as `??`
|
||||
- untracked directories shown as `?? path/`
|
||||
3. When `git status --short` reports an untracked directory, expand it to concrete files before review:
|
||||
```bash
|
||||
find <path> -type f
|
||||
# Or prefer:
|
||||
rg --files <path>
|
||||
```
|
||||
4. If the task ran generators such as `make ctrl`, `make dao`, codegen scripts, or produced new test files, explicitly include the generated untracked files in review scope even if they do not appear in `git diff`.
|
||||
5. `git diff` may be used only as a secondary narrowing aid after status collection. It is **never sufficient by itself** for review scope definition.
|
||||
|
||||
Run `openspec status --change "<name>" --json` to understand the current change state.
|
||||
|
||||
### 2. Load Specifications
|
||||
|
||||
Read `CLAUDE.md` to load all specifications. This is the single source of truth.
|
||||
|
||||
### 3. Backend Code Review
|
||||
|
||||
**Trigger**: Changes to files under `apps/lina-core` directory
|
||||
|
||||
1. Invoke `goframe-v2` skill for GoFrame framework conventions
|
||||
2. Check against `CLAUDE.md` backend code specifications
|
||||
|
||||
### 4. RESTful API Review
|
||||
|
||||
**Trigger**: Any API endpoint changes
|
||||
|
||||
Check against `CLAUDE.md` API design specifications.
|
||||
|
||||
### 5. Project Specification Review
|
||||
|
||||
**Trigger**: Any implementation changes
|
||||
|
||||
Check against `CLAUDE.md` architecture design specifications and code development specifications.
|
||||
|
||||
### 6. SQL Review
|
||||
|
||||
**Trigger**: New or modified files under `apps/lina-core/manifest/sql/`、`apps/lina-core/manifest/sql/mock-data/`、`apps/lina-plugins/**/manifest/sql/` or SQL snippets embedded in related delivery docs
|
||||
|
||||
Check against `CLAUDE.md` SQL file management specifications, at minimum covering:
|
||||
1. File naming, versioning, and single-iteration single-file rules
|
||||
2. Seed DML vs mock data separation
|
||||
3. **Idempotent execution safety** — SQL must be safe to run multiple times without duplicate-object errors or duplicate seed data; verify use of `IF [NOT] EXISTS`, `IF EXISTS`, `INSERT IGNORE`, or equivalent safe re-entry patterns
|
||||
4. **Seed write style compliance** — delivered SQL must reject `INSERT ... ON DUPLICATE KEY UPDATE` and reject explicit writes to `AUTO_INCREMENT` `id` columns in seed/mock/install data
|
||||
5. Whether schema/data changes still match the current change scope and deployment path
|
||||
|
||||
### 7. Unit Test Review
|
||||
|
||||
**Trigger**: New or modified Go implementation files, or new/modified Go unit test files matching `*_test.go`
|
||||
|
||||
Check at minimum:
|
||||
1. Behavior-changing Go code includes focused unit coverage in the same package, preferably by extending existing `*_z_unit*_test.go` or `*_test.go`
|
||||
2. Tests assert the changed logic directly instead of relying on broad workflow-level coverage when a package-level test is sufficient
|
||||
3. Verification uses targeted `go test ./path/to/pkg -run TestName` during development and package-level `go test ./path/to/pkg` for regression
|
||||
|
||||
### 8. Generate Review Report
|
||||
|
||||
```markdown
|
||||
## GF Review Report
|
||||
|
||||
**Change:** <change-name>
|
||||
**Scope:** <task-specific / full change>
|
||||
**Files Reviewed:** <count>
|
||||
**Scope Source:** `git status --short` + `git ls-files --others --exclude-standard` + task/change context
|
||||
|
||||
### Backend Code Review
|
||||
✓ All checks passed / ⚠ N issues found
|
||||
|
||||
### RESTful API Review
|
||||
✓ All endpoints compliant / ⚠ N violations found
|
||||
|
||||
### Project Spec Review
|
||||
✓ Compliant with CLAUDE.md / ⚠ N violations found
|
||||
|
||||
### SQL Review
|
||||
✓ No SQL changes / ✓ SQL changes compliant / ⚠ N SQL issues found
|
||||
|
||||
### Unit Test Review
|
||||
✓ Unit tests are focused and sufficient / ⚠ N issues found
|
||||
|
||||
### Summary
|
||||
- **Critical:** N (must fix before archive)
|
||||
- **Warnings:** N (recommended to fix)
|
||||
|
||||
### Recommended Actions
|
||||
1. [Specific action with CLAUDE.md reference]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Issue Severity
|
||||
|
||||
| Level | Behavior |
|
||||
|-------|----------|
|
||||
| **Critical** | Block archive, must fix |
|
||||
| **Warning** | Show but allow proceed |
|
||||
|
||||
---
|
||||
|
||||
## Integration Points
|
||||
|
||||
| Workflow Step | Behavior |
|
||||
|---------------|----------|
|
||||
| `/opsx:apply` task done | Review, offer to fix issues before next task |
|
||||
| `/gf-feedback` task done | Review, fix before marking complete |
|
||||
| `/opsx:archive` | Review all changes, block on critical issues |
|
||||
|
||||
---
|
||||
|
||||
## Guardrails
|
||||
|
||||
- **CLAUDE.md is the single source of truth** — All spec references point to it
|
||||
- Only check categories relevant to changed files
|
||||
- Scope identification MUST include untracked files and expanded untracked directories; never rely on `git diff` alone
|
||||
- Behavior-changing Go code without focused unit tests is a review finding unless the author documents why tests are not applicable
|
||||
- Don't block on warnings — only critical issues block archive
|
||||
- Include file paths and line numbers in issue reports
|
||||
- Offer to fix issues automatically when straightforward
|
||||
148
.agents/skills/git-commit-push/SKILL.md
Normal file
148
.agents/skills/git-commit-push/SKILL.md
Normal file
@ -0,0 +1,148 @@
|
||||
---
|
||||
name: git-commit-push
|
||||
description: Review the current git working tree, generate a commit message from the actual diff using the repository's commit or PR-title convention, commit all current changes on the active branch, and push that branch to `origin`. Use this whenever the user asks to "commit", "push", "commit and push", "generate a commit message", "commit the current changes", or wants the current branch changes sent upstream without hand-writing the git commands.
|
||||
---
|
||||
|
||||
# Git Commit Push
|
||||
|
||||
Inspect the current repository changes, derive a concise commit subject that matches the repository convention, commit every current modification on the active branch, and push that branch to `origin`.
|
||||
|
||||
This skill is for execution, not just advice. When it triggers, actually run the git workflow unless the repository state makes that unsafe or impossible.
|
||||
|
||||
## When To Use
|
||||
|
||||
- The user asks you to commit the current changes, with or without asking for push
|
||||
- The user wants you to write the commit message from the diff instead of inventing one up front
|
||||
- The user mentions the repo's PR or commit naming convention and wants you to follow it
|
||||
- The user says things like "commit the current branch", "help me commit", "commit and push", "generate a commit message and push", or "send these changes to origin"
|
||||
|
||||
## Core Behavior
|
||||
|
||||
1. Confirm you are inside a Git repository and detect the active branch with `git branch --show-current`.
|
||||
2. Inspect the working tree before committing:
|
||||
- `git status --short --branch`
|
||||
- `git diff --stat`
|
||||
- `git diff --cached --stat`
|
||||
- `git diff -- . ':(exclude)package-lock.json'` or narrower path filters only when needed for readability
|
||||
3. If the repository contains `.github/PULL_REQUEST_TEMPLATE.MD`, read it and treat its PR-title rules as the default commit-subject convention.
|
||||
4. Generate a commit subject from the actual changed files and diff content, not from the user prompt alone.
|
||||
5. Stage every current modification on the branch with `git add -A`.
|
||||
6. Commit once with the generated message.
|
||||
7. Push the current branch to `origin` with `git push origin <current-branch>`.
|
||||
|
||||
## Commit Message Rules
|
||||
|
||||
The commit message is formatted as follows: `<type>[optional scope]: <description>` For example, `fix(os/gtime): fix time zone issue`
|
||||
+ `<type>` is mandatory and can be one of `fix`, `feat`, `build`, `ci`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`
|
||||
+ `fix`: Used when a bug has been fixed.
|
||||
+ `feat`: Used when a new feature has been added.
|
||||
+ `build`: Used for modifications to the project build system, such as changes to dependencies, external interfaces, or upgrading Node version.
|
||||
+ `ci`: Used for modifications to continuous integration processes, such as changes to Travis, Jenkins workflow configurations.
|
||||
+ `docs`: Used for modifications to documentation, such as changes to README files, API documentation, etc.
|
||||
+ `style`: Used for changes to code style, such as adjustments to indentation, spaces, blank lines, etc.
|
||||
+ `refactor`: Used for code refactoring, such as changes to code structure, variable names, function names, without altering functionality.
|
||||
+ `perf`: Used for performance optimization, such as improving code performance, reducing memory usage, etc.
|
||||
+ `test`: Used for modifications to test cases, such as adding, deleting, or modifying test cases for code.
|
||||
+ `chore`: Used for modifications to non-business-related code, such as changes to build processes or tool configurations.
|
||||
+ After `<type>`, specify the affected package name or scope in parentheses, for example, `(os/gtime)`.
|
||||
+ The part after the colon uses the verb tense + phrase that completes the blank in
|
||||
+ Lowercase verb after the colon
|
||||
+ No trailing period
|
||||
+ Keep the title as short as possible. ideally under 76 characters or shorter
|
||||
+ If there is a corresponding issue, add either `fixes #1234` (the latter if this is not a complete fix) to this comment
|
||||
|
||||
### Examples
|
||||
#### Commit message with description and breaking change footer
|
||||
```
|
||||
feat: allow provided config object to extend other configs
|
||||
BREAKING CHANGE: `extends` key in config file is now used for extending other config files
|
||||
```
|
||||
|
||||
#### Commit message with ! to draw attention to breaking change
|
||||
```
|
||||
feat!: send an email to the customer when a product is shipped
|
||||
```
|
||||
|
||||
#### Commit message with scope and ! to draw attention to breaking change
|
||||
```
|
||||
feat(api)!: send an email to the customer when a product is shipped
|
||||
```
|
||||
|
||||
#### Commit message with both ! and BREAKING CHANGE footer
|
||||
```
|
||||
feat!: drop support for Node 6
|
||||
BREAKING CHANGE: use JavaScript features not available in Node 6.
|
||||
```
|
||||
|
||||
#### Commit message with no body
|
||||
```
|
||||
docs: correct spelling of CHANGELOG
|
||||
```
|
||||
|
||||
#### Commit message with scope
|
||||
```
|
||||
feat(lang): add Polish language
|
||||
```
|
||||
|
||||
#### Commit message with multi-paragraph body and multiple footers
|
||||
```
|
||||
fix: prevent racing of requests
|
||||
|
||||
Introduce a request id and a reference to latest request. Dismiss
|
||||
incoming responses other than from latest request.
|
||||
|
||||
Remove timeouts which were used to mitigate the racing issue but are
|
||||
obsolete now.
|
||||
|
||||
Reviewed-by: Z
|
||||
Refs: #123
|
||||
```
|
||||
|
||||
## Execution Rules
|
||||
|
||||
- Commit all current tracked and untracked changes in the working tree, because this skill is for "commit the current state" requests
|
||||
- If there are no changes, say so clearly and stop before commit or push
|
||||
- If `git branch --show-current` is empty, explain that `HEAD` is detached and stop unless the user explicitly asks you to commit from detached `HEAD`
|
||||
- Never use `--force`, `--force-with-lease`, or history-rewriting commands unless the user explicitly asks
|
||||
- If push fails because the remote branch moved, report the exact failure and stop instead of auto-rebasing or auto-merging
|
||||
- Do not silently drop files from the commit unless the user asked to exclude them
|
||||
|
||||
## Suggested Command Flow
|
||||
|
||||
```bash
|
||||
git status --short --branch
|
||||
git diff --stat
|
||||
git diff --cached --stat
|
||||
test -f .github/PULL_REQUEST_TEMPLATE.MD && sed -n '1,220p' .github/PULL_REQUEST_TEMPLATE.MD
|
||||
branch_name=$(git branch --show-current)
|
||||
git add -A
|
||||
git commit -m "<generated-subject>"
|
||||
git push origin "$branch_name"
|
||||
```
|
||||
|
||||
Inspect `git diff --cached` again after staging if the pre-stage diff was noisy or if untracked files materially change the scope.
|
||||
|
||||
## Output Contract
|
||||
|
||||
When you use this skill:
|
||||
|
||||
- Tell the user which branch you committed
|
||||
- Provide the final commit subject you used
|
||||
- Mention that you staged all current changes
|
||||
- Report the push target as `origin/<branch>`
|
||||
- If commit or push did not happen, explain exactly why
|
||||
|
||||
## Example
|
||||
|
||||
User request:
|
||||
|
||||
```text
|
||||
Generate a commit message that follows this repository's convention, then commit and push the current branch
|
||||
```
|
||||
|
||||
Expected behavior:
|
||||
|
||||
- Inspect the repo status and diff
|
||||
- Generate a conventional subject from the real changes
|
||||
- Run one commit for the whole current working tree
|
||||
- Push the active branch to `origin`
|
||||
142
.agents/skills/git-worktree/SKILL.md
Normal file
142
.agents/skills/git-worktree/SKILL.md
Normal file
@ -0,0 +1,142 @@
|
||||
---
|
||||
name: git-worktree
|
||||
description: Create and actively use an isolated git worktree for the user's task, then continue the task inside that new directory. Use this whenever the user asks for a separate worktree, isolated checkout, clean branch directory, safer parallel changes, or a fresh workspace to avoid unrelated local edits.
|
||||
---
|
||||
|
||||
# Git Worktree
|
||||
|
||||
Create a dedicated `git worktree` for the current task, then keep working inside that new directory instead of the original checkout.
|
||||
|
||||
This skill is about execution, not just advice. When it triggers, actually create the worktree unless the repository state makes that impossible.
|
||||
|
||||
Do not introduce helper scripts for this skill. Use direct `git` and shell commands inline.
|
||||
|
||||
## When To Use
|
||||
|
||||
- The user explicitly asks for a new `git worktree`, independent branch directory, or isolated workspace
|
||||
- The current checkout contains unrelated local changes and isolation is the safest way forward
|
||||
- The user wants parallel work on multiple tasks without stashing or disturbing the original worktree
|
||||
- The user says things like "create a separate branch folder", "open a fresh worktree", "use a clean checkout", or "work in an isolated workspace"
|
||||
|
||||
## Core Rule
|
||||
|
||||
After creating the worktree, treat the new path as the active working directory for the rest of the task.
|
||||
|
||||
In any agent environment, "enter the directory" means:
|
||||
|
||||
- Run subsequent commands against the new worktree path
|
||||
- Apply all edits under that worktree path
|
||||
- Do not keep using the original checkout by accident
|
||||
- Confirm the handoff by running at least one follow-up command in the new worktree
|
||||
|
||||
Never claim you "switched" unless your subsequent actions actually target the new `worktree_path`.
|
||||
|
||||
If your environment supports a per-command working directory, use it for every later command. If it does not, prefix later commands with an explicit `cd <worktree_path> && ...`.
|
||||
|
||||
## Name Derivation
|
||||
|
||||
- Derive a short ASCII kebab-case task slug from the user's real task, such as `login-timeout-fix` or `user-export`
|
||||
- Do not use generic names like `git-worktree`, `new-worktree`, or `task` unless the request is too vague
|
||||
- If the request is mostly non-ASCII or no good slug is obvious, fall back to `task-$(date +%Y%m%d-%H%M%S)`
|
||||
- Default branch prefix is `worktree/`
|
||||
- Default worktree directory is a sibling of the repository root, named `<repo-name>-<slug>`
|
||||
|
||||
## Default Workflow
|
||||
|
||||
1. Inspect the repository context from the current checkout:
|
||||
- `git rev-parse --show-toplevel`
|
||||
- `git branch --show-current`
|
||||
- `git status --short`
|
||||
- `git worktree list --porcelain`
|
||||
2. Decide a task slug yourself using the rules above
|
||||
3. Build branch and path names inline, then create the worktree with direct shell commands like:
|
||||
|
||||
```bash
|
||||
repo_root=$(git rev-parse --show-toplevel)
|
||||
repo_name=$(basename "$repo_root")
|
||||
parent_dir=$(dirname "$repo_root")
|
||||
source_branch=$(git -C "$repo_root" branch --show-current)
|
||||
if [ -n "$source_branch" ]; then
|
||||
source_ref="$source_branch"
|
||||
else
|
||||
source_ref="HEAD@$(git -C "$repo_root" rev-parse --short HEAD)"
|
||||
fi
|
||||
|
||||
slug="<task-slug>"
|
||||
base_branch="worktree/$slug"
|
||||
branch_name="$base_branch"
|
||||
base_path="$parent_dir/$repo_name-$slug"
|
||||
worktree_path="$base_path"
|
||||
index=2
|
||||
|
||||
while git -C "$repo_root" show-ref --verify --quiet "refs/heads/$branch_name" || [ -e "$worktree_path" ]; do
|
||||
branch_name="${base_branch}-$index"
|
||||
worktree_path="${base_path}-$index"
|
||||
index=$((index + 1))
|
||||
done
|
||||
|
||||
git -C "$repo_root" worktree add -b "$branch_name" "$worktree_path" HEAD
|
||||
```
|
||||
|
||||
4. Immediately verify the handoff inside the new worktree, for example:
|
||||
|
||||
```bash
|
||||
pwd
|
||||
git status --short --branch
|
||||
```
|
||||
|
||||
These verification commands must run against `worktree_path`.
|
||||
|
||||
5. Announce the new active path briefly, then continue the main task there
|
||||
6. For the remainder of the task, use `worktree_path` as the working directory for every relevant command or edit operation
|
||||
|
||||
## Behavior Rules
|
||||
|
||||
- Default base ref is `HEAD` from the current checkout so uncommitted local changes are not dragged into the new worktree
|
||||
- If a branch name or path already exists, auto-increment it instead of failing
|
||||
- If you are already inside a non-default worktree and the user still wants another isolated workspace, create a new one from the current `HEAD`
|
||||
- If the directory is not a Git repository, explain that clearly and do not pretend a worktree was created
|
||||
- If worktree creation succeeds, continue the user's actual task instead of stopping at setup
|
||||
- If worktree creation fails because of filesystem permissions, request the minimal approval needed and retry
|
||||
|
||||
## Uncommitted Change Policy
|
||||
|
||||
The safe default is isolation from uncommitted changes.
|
||||
|
||||
- If the source checkout is dirty, still create the new worktree from `HEAD` unless the user explicitly asks to carry local edits over
|
||||
- Do not silently stash, reset, or move the user's existing changes
|
||||
- If the user wants local edits copied into the new worktree, use an explicit flow such as a temporary commit, patch, or cherry-pick, and say what you are doing
|
||||
|
||||
## Output Contract
|
||||
|
||||
When you use this skill:
|
||||
|
||||
- Tell the user which branch and directory were created
|
||||
- Make it clear that subsequent work is now happening inside that path
|
||||
- Mention the source ref and whether the original checkout was dirty when that context matters
|
||||
- Do not stop after setup if the user asked for additional work; continue the task in the new worktree
|
||||
|
||||
## Example
|
||||
|
||||
User request:
|
||||
|
||||
```text
|
||||
Create a separate worktree for this task and then start implementing it.
|
||||
```
|
||||
|
||||
Expected behavior:
|
||||
|
||||
- Inspect current repo status
|
||||
- Create a new `worktree/...` branch and sibling directory with direct `git worktree` commands
|
||||
- Switch all following commands to that directory
|
||||
- Continue the requested implementation there
|
||||
|
||||
## Cleanup
|
||||
|
||||
Only remove a worktree when the user asks or when cleanup is clearly part of the task.
|
||||
|
||||
Before cleanup:
|
||||
|
||||
- Check status in the worktree you created
|
||||
- Make sure you are removing the correct path
|
||||
- Never remove the user's original checkout
|
||||
288
.agents/skills/openspec-explore/SKILL.md
Normal file
288
.agents/skills/openspec-explore/SKILL.md
Normal file
@ -0,0 +1,288 @@
|
||||
---
|
||||
name: openspec-explore
|
||||
description: Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
|
||||
|
||||
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
|
||||
|
||||
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
|
||||
|
||||
---
|
||||
|
||||
## The Stance
|
||||
|
||||
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
|
||||
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
|
||||
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
|
||||
- **Adaptive** - Follow interesting threads, pivot when new information emerges
|
||||
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
|
||||
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
|
||||
|
||||
---
|
||||
|
||||
## What You Might Do
|
||||
|
||||
Depending on what the user brings, you might:
|
||||
|
||||
**Explore the problem space**
|
||||
- Ask clarifying questions that emerge from what they said
|
||||
- Challenge assumptions
|
||||
- Reframe the problem
|
||||
- Find analogies
|
||||
|
||||
**Investigate the codebase**
|
||||
- Map existing architecture relevant to the discussion
|
||||
- Find integration points
|
||||
- Identify patterns already in use
|
||||
- Surface hidden complexity
|
||||
|
||||
**Compare options**
|
||||
- Brainstorm multiple approaches
|
||||
- Build comparison tables
|
||||
- Sketch tradeoffs
|
||||
- Recommend a path (if asked)
|
||||
|
||||
**Visualize**
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Use ASCII diagrams liberally │
|
||||
├─────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌────────┐ ┌────────┐ │
|
||||
│ │ State │────────▶│ State │ │
|
||||
│ │ A │ │ B │ │
|
||||
│ └────────┘ └────────┘ │
|
||||
│ │
|
||||
│ System diagrams, state machines, │
|
||||
│ data flows, architecture sketches, │
|
||||
│ dependency graphs, comparison tables │
|
||||
│ │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Surface risks and unknowns**
|
||||
- Identify what could go wrong
|
||||
- Find gaps in understanding
|
||||
- Suggest spikes or investigations
|
||||
|
||||
---
|
||||
|
||||
## OpenSpec Awareness
|
||||
|
||||
You have full context of the OpenSpec system. Use it naturally, don't force it.
|
||||
|
||||
### Check for context
|
||||
|
||||
At the start, quickly check what exists:
|
||||
```bash
|
||||
openspec list --json
|
||||
```
|
||||
|
||||
This tells you:
|
||||
- If there are active changes
|
||||
- Their names, schemas, and status
|
||||
- What the user might be working on
|
||||
|
||||
### When no change exists
|
||||
|
||||
Think freely. When insights crystallize, you might offer:
|
||||
|
||||
- "This feels solid enough to start a change. Want me to create a proposal?"
|
||||
- Or keep exploring - no pressure to formalize
|
||||
|
||||
### When a change exists
|
||||
|
||||
If the user mentions a change or you detect one is relevant:
|
||||
|
||||
1. **Read existing artifacts for context**
|
||||
- `openspec/changes/<name>/proposal.md`
|
||||
- `openspec/changes/<name>/design.md`
|
||||
- `openspec/changes/<name>/tasks.md`
|
||||
- etc.
|
||||
|
||||
2. **Reference them naturally in conversation**
|
||||
- "Your design mentions using Redis, but we just realized SQLite fits better..."
|
||||
- "The proposal scopes this to premium users, but we're now thinking everyone..."
|
||||
|
||||
3. **Offer to capture when decisions are made**
|
||||
|
||||
| Insight Type | Where to Capture |
|
||||
|--------------|------------------|
|
||||
| New requirement discovered | `specs/<capability>/spec.md` |
|
||||
| Requirement changed | `specs/<capability>/spec.md` |
|
||||
| Design decision made | `design.md` |
|
||||
| Scope changed | `proposal.md` |
|
||||
| New work identified | `tasks.md` |
|
||||
| Assumption invalidated | Relevant artifact |
|
||||
|
||||
Example offers:
|
||||
- "That's a design decision. Capture it in design.md?"
|
||||
- "This is a new requirement. Add it to specs?"
|
||||
- "This changes scope. Update the proposal?"
|
||||
|
||||
4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
|
||||
|
||||
---
|
||||
|
||||
## What You Don't Have To Do
|
||||
|
||||
- Follow a script
|
||||
- Ask the same questions every time
|
||||
- Produce a specific artifact
|
||||
- Reach a conclusion
|
||||
- Stay on topic if a tangent is valuable
|
||||
- Be brief (this is thinking time)
|
||||
|
||||
---
|
||||
|
||||
## Handling Different Entry Points
|
||||
|
||||
**User brings a vague idea:**
|
||||
```
|
||||
User: I'm thinking about adding real-time collaboration
|
||||
|
||||
You: Real-time collab is a big space. Let me think about this...
|
||||
|
||||
COLLABORATION SPECTRUM
|
||||
════════════════════════════════════════════
|
||||
|
||||
Awareness Coordination Sync
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌────────┐ ┌────────┐ ┌────────┐
|
||||
│Presence│ │Cursors │ │ CRDT │
|
||||
│ "3 │ │ Multi │ │Conflict│
|
||||
│online" │ │ select │ │ free │
|
||||
└────────┘ └────────┘ └────────┘
|
||||
│ │ │
|
||||
trivial moderate complex
|
||||
|
||||
Where's your head at?
|
||||
```
|
||||
|
||||
**User brings a specific problem:**
|
||||
```
|
||||
User: The auth system is a mess
|
||||
|
||||
You: [reads codebase]
|
||||
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ CURRENT AUTH FLOW │
|
||||
└─────────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────┼─────────────┐
|
||||
▼ ▼ ▼
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ Google │ │ GitHub │ │ Email │
|
||||
│ OAuth │ │ OAuth │ │ Magic │
|
||||
└────┬────┘ └────┬────┘ └────┬────┘
|
||||
│ │ │
|
||||
└─────────────┼─────────────┘
|
||||
▼
|
||||
┌───────────┐
|
||||
│ Session │
|
||||
└─────┬─────┘
|
||||
│
|
||||
▼
|
||||
┌───────────┐
|
||||
│ Perms │
|
||||
└───────────┘
|
||||
|
||||
I see three tangles. Which one's burning?
|
||||
```
|
||||
|
||||
**User is stuck mid-implementation:**
|
||||
```
|
||||
User: /opsx:explore add-auth-system
|
||||
The OAuth integration is more complex than expected
|
||||
|
||||
You: [reads change artifacts]
|
||||
|
||||
You're on task 4: "Implement OAuth flow"
|
||||
|
||||
Let me trace what's involved...
|
||||
|
||||
[draws diagram, explores options, suggests paths]
|
||||
|
||||
Want to update the design to reflect this?
|
||||
Or add a spike task to investigate?
|
||||
```
|
||||
|
||||
**User wants to compare options:**
|
||||
```
|
||||
User: Should we use Postgres or SQLite?
|
||||
|
||||
You: Generic answer is boring. What's the context?
|
||||
|
||||
User: A CLI tool that tracks local dev environments
|
||||
|
||||
You: That changes everything.
|
||||
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ CLI TOOL DATA STORAGE │
|
||||
└─────────────────────────────────────────────────┘
|
||||
|
||||
Key constraints:
|
||||
• No daemon running
|
||||
• Must work offline
|
||||
• Single user
|
||||
|
||||
SQLite Postgres
|
||||
Deployment embedded ✓ needs server ✗
|
||||
Offline yes ✓ no ✗
|
||||
Single file yes ✓ no ✗
|
||||
|
||||
SQLite. Not even close.
|
||||
|
||||
Unless... is there a sync component?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ending Discovery
|
||||
|
||||
There's no required ending. Discovery might:
|
||||
|
||||
- **Flow into a proposal**: "Ready to start? I can create a change proposal."
|
||||
- **Result in artifact updates**: "Updated design.md with these decisions"
|
||||
- **Just provide clarity**: User has what they need, moves on
|
||||
- **Continue later**: "We can pick this up anytime"
|
||||
|
||||
When it feels like things are crystallizing, you might summarize:
|
||||
|
||||
```
|
||||
## What We Figured Out
|
||||
|
||||
**The problem**: [crystallized understanding]
|
||||
|
||||
**The approach**: [if one emerged]
|
||||
|
||||
**Open questions**: [if any remain]
|
||||
|
||||
**Next steps** (if ready):
|
||||
- Create a change proposal
|
||||
- Keep exploring: just keep talking
|
||||
```
|
||||
|
||||
But this summary is optional. Sometimes the thinking IS the value.
|
||||
|
||||
---
|
||||
|
||||
## Guardrails
|
||||
|
||||
- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
|
||||
- **Don't fake understanding** - If something is unclear, dig deeper
|
||||
- **Don't rush** - Discovery is thinking time, not task time
|
||||
- **Don't force structure** - Let patterns emerge naturally
|
||||
- **Don't auto-capture** - Offer to save insights, don't just do it
|
||||
- **Do visualize** - A good diagram is worth many paragraphs
|
||||
- **Do explore the codebase** - Ground discussions in reality
|
||||
- **Do question assumptions** - Including the user's and your own
|
||||
110
.agents/skills/openspec-propose/SKILL.md
Normal file
110
.agents/skills/openspec-propose/SKILL.md
Normal file
@ -0,0 +1,110 @@
|
||||
---
|
||||
name: openspec-propose
|
||||
description: Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.2.0"
|
||||
---
|
||||
|
||||
Propose a new change - create the change and generate all artifacts in one step.
|
||||
|
||||
I'll create a change with artifacts:
|
||||
- proposal.md (what & why)
|
||||
- design.md (how)
|
||||
- tasks.md (implementation steps)
|
||||
|
||||
When ready to implement, run /opsx:apply
|
||||
|
||||
---
|
||||
|
||||
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no clear input provided, ask what they want to build**
|
||||
|
||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||
> "What change do you want to work on? Describe what you want to build or fix."
|
||||
|
||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||
|
||||
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||
|
||||
2. **Create the change directory**
|
||||
```bash
|
||||
openspec new change "<name>"
|
||||
```
|
||||
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
|
||||
|
||||
3. **Get the artifact build order**
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
Parse the JSON to get:
|
||||
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
||||
- `artifacts`: list of all artifacts with their status and dependencies
|
||||
|
||||
4. **Create artifacts in sequence until apply-ready**
|
||||
|
||||
Use the **TodoWrite tool** to track progress through the artifacts.
|
||||
|
||||
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
||||
|
||||
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
||||
- Get instructions:
|
||||
```bash
|
||||
openspec instructions <artifact-id> --change "<name>" --json
|
||||
```
|
||||
- The instructions JSON includes:
|
||||
- `context`: Project background (constraints for you - do NOT include in output)
|
||||
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
||||
- `template`: The structure to use for your output file
|
||||
- `instruction`: Schema-specific guidance for this artifact type
|
||||
- `outputPath`: Where to write the artifact
|
||||
- `dependencies`: Completed artifacts to read for context
|
||||
- Read any completed dependency files for context
|
||||
- Create the artifact file using `template` as the structure
|
||||
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
||||
- Show brief progress: "Created <artifact-id>"
|
||||
|
||||
b. **Continue until all `applyRequires` artifacts are complete**
|
||||
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
||||
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
||||
- Stop when all `applyRequires` artifacts are done
|
||||
|
||||
c. **If an artifact requires user input** (unclear context):
|
||||
- Use **AskUserQuestion tool** to clarify
|
||||
- Then continue with creation
|
||||
|
||||
5. **Show final status**
|
||||
```bash
|
||||
openspec status --change "<name>"
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
After completing all artifacts, summarize:
|
||||
- Change name and location
|
||||
- List of artifacts created with brief descriptions
|
||||
- What's ready: "All artifacts created! Ready for implementation."
|
||||
- Prompt: "Run `/opsx:apply` or ask me to implement to start working on the tasks."
|
||||
|
||||
**Artifact Creation Guidelines**
|
||||
|
||||
- Follow the `instruction` field from `openspec instructions` for each artifact type
|
||||
- The schema defines what each artifact should contain - follow it
|
||||
- Read dependency artifacts for context before creating new ones
|
||||
- Use `template` as the structure for your output file - fill in its sections
|
||||
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
||||
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
||||
- These guide what you write, but should never appear in the output
|
||||
|
||||
**Guardrails**
|
||||
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
||||
- Always read dependency artifacts before creating a new one
|
||||
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
||||
- If a change with that name already exists, ask if user wants to continue it or create a new one
|
||||
- Verify each artifact file exists after writing before proceeding to next
|
||||
1
.claude/commands
Symbolic link
1
.claude/commands
Symbolic link
@ -0,0 +1 @@
|
||||
../.agents/prompts
|
||||
1
.claude/skills
Symbolic link
1
.claude/skills
Symbolic link
@ -0,0 +1 @@
|
||||
../.agents/skills
|
||||
5
.codex/config.toml
Normal file
5
.codex/config.toml
Normal file
@ -0,0 +1,5 @@
|
||||
approval_policy = "never"
|
||||
sandbox_mode = "danger-full-access"
|
||||
|
||||
[sandbox_workspace_write]
|
||||
network_access = true
|
||||
@ -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
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Function to run sed in-place with OS-specific options
|
||||
sed_inplace() {
|
||||
sed_replace() {
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# macOS - requires empty string after -i
|
||||
sed -i '' "$@"
|
||||
@ -40,11 +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_inplace 's/version=[^"]*/version='${newVersion}'/' README.zh_CN.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
|
||||
@ -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_inplace '/\/\/ 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,
|
||||
# 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
|
||||
# Remove indirect dependencies
|
||||
sed_inplace '/\/\/ 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
|
||||
|
||||
211
CLAUDE.md
Normal file
211
CLAUDE.md
Normal file
@ -0,0 +1,211 @@
|
||||
# Repository Overview
|
||||
|
||||
This is the [GoFrame](https://goframe.org) framework (`github.com/gogf/gf/v2`) — a modular Go application framework. The repository is a **multi-module monorepo**: the root module hosts the core framework, while `cmd/gf` and every directory under `contrib/` are independent Go modules with their own `go.mod`.
|
||||
|
||||
- Root module: `github.com/gogf/gf/v2` (Go 1.23+)
|
||||
- Tooling module: `cmd/gf` (CLI: `gf` command, separate `go.work`)
|
||||
- Plugin modules under `contrib/`: `config/*`, `drivers/*` (mysql, pgsql, mssql, oracle, sqlite, clickhouse, dm, gaussdb, mariadb, oceanbase, tidb, sqlitecgo), `metric/*`, `nosql/*`, `registry/*`, `rpc/grpcx`, `sdk/*`, `trace/*`. Each is published as its own module so users only pull what they need.
|
||||
|
||||
The root framework intentionally has **minimal external dependencies** (see `go.mod`). Anything requiring a heavy third-party dep (a DB driver, a registry client, an RPC stack) lives in `contrib/`.
|
||||
|
||||
## Top-level package map
|
||||
|
||||
| Path | Purpose |
|
||||
| --- | --- |
|
||||
| `frame/g`, `frame/gins` | Convenience facade (`g.Server()`, `g.DB()`, `g.Cfg()`) and the singleton/instance container |
|
||||
| `net/` | `ghttp` (HTTP server/router), `gclient` (HTTP client), `gtcp`, `gudp`, `gsel` (load balancing), `gsvc` (service discovery), `gtrace`, `goai` (OpenAPI) |
|
||||
| `os/` | OS abstractions: `gcfg` (config), `gcmd` (CLI), `gcron`, `glog`, `gfile`, `gres` (resource embedding), `gview` (templating), `gcache`, `gsession`, `gctx`, `gtimer`, `gproc`, `gmetric`, `gfsnotify`, `gstructs` |
|
||||
| `database/` | `gdb` (ORM/query builder, driver-agnostic core), `gredis` (Redis client core) |
|
||||
| `container/` | Concurrent-safe data structures: `garray`, `gmap`, `gset`, `gtree`, `glist`, `gqueue`, `gring`, `gpool`, `gtype`, `gvar` |
|
||||
| `encoding/` | Codecs for JSON/XML/YAML/TOML/INI/Properties, base64, charsets, compression, hashes, HTML, URL |
|
||||
| `text/` | `gstr`, `gregex` |
|
||||
| `util/` | `gconv` (universal conversion — heavily used), `gvalid` (validation), `gutil`, `grand`, `guid`, `gtag`, `gmeta`, `gpage`, `gmode` |
|
||||
| `crypto/` | `gaes`, `gdes`, `grsa`, `gmd5`, `gsha*`, `gcrc32` |
|
||||
| `errors/` | `gerror` (stack-aware errors), `gcode` (error code registry) |
|
||||
| `internal/` | Framework-internal helpers (do not import from outside the root module) |
|
||||
| `test/gtest` | The framework's own testing helpers — used throughout the test suite |
|
||||
|
||||
Concrete database drivers and Redis adapters live under `contrib/drivers/` and `contrib/nosql/`; the `database/gdb` and `database/gredis` packages define the abstract layer.
|
||||
|
||||
# Common Commands
|
||||
|
||||
All commands run from the repository root unless noted.
|
||||
|
||||
## Build / lint / tidy
|
||||
|
||||
```bash
|
||||
# Tidy every go.mod in the repo (root, cmd/gf, contrib/*) — strips // indirect and toolchain lines
|
||||
make tidy
|
||||
|
||||
# Run the project's golangci-lint config (.golangci.yml)
|
||||
make lint
|
||||
# Equivalent: golangci-lint run -c .golangci.yml
|
||||
```
|
||||
|
||||
`make tidy` invokes `.make_tidy.sh`, which `cd`s into every directory containing a `go.mod` (skipping `testdata/` and `examples/`) and runs `go mod tidy`. After editing imports in any module, run this from the repo root.
|
||||
|
||||
## Tests
|
||||
|
||||
Each Go module must be tested from inside its own directory because they are separate modules. The CI script (`.github/workflows/scripts/ci-main.sh`) iterates every `go.mod`:
|
||||
|
||||
```bash
|
||||
# Build + race-enabled tests for the root module
|
||||
go build ./...
|
||||
go test ./... -count=1 -race
|
||||
|
||||
# Coverage (matches CI 'coverage' mode)
|
||||
go test ./... -count=1 -race -coverprofile=coverage.out -covermode=atomic \
|
||||
-coverpkg=./...,github.com/gogf/gf/...
|
||||
|
||||
# Run a single package's tests
|
||||
go test -count=1 -race ./os/gcfg/...
|
||||
|
||||
# Run a single test by name
|
||||
go test -count=1 -race -run TestCfg_Get ./os/gcfg/
|
||||
|
||||
# Test a contrib module — must cd in first (separate go.mod)
|
||||
cd contrib/drivers/mysql && go test ./... -count=1 -race
|
||||
```
|
||||
|
||||
Many tests (database drivers, registry clients, redis cluster, apollo/nacos config) require backing services. CI starts them via docker-compose files under `.github/workflows/{redis,apollo,nacos,consul}/`. Locally, use:
|
||||
|
||||
```bash
|
||||
make docker # start the default local stack
|
||||
make docker cmd=start svc=mysql # start a specific service
|
||||
make docker cmd=stop svc=mysql
|
||||
```
|
||||
|
||||
## CLI tool
|
||||
|
||||
```bash
|
||||
cd cmd/gf && go run . <subcommand> # iterate on the gf CLI itself
|
||||
```
|
||||
|
||||
# Architecture Notes Worth Knowing Up Front
|
||||
|
||||
- **The `frame/g` package is a facade, not a library.** It re-exports types and provides singleton accessors (`g.DB()`, `g.Server()`, `g.Cfg()`, `g.Log()`) backed by `frame/gins`. Examples in docs use `g.*` heavily; framework-internal code generally imports the underlying packages directly.
|
||||
- **`util/gconv` is foundational.** Most public APIs accept `any` and use `gconv` for type coercion. When changing argument handling, search for `gconv.` usage to understand the conversion contract.
|
||||
- **`gdb` is driver-agnostic.** The core in `database/gdb` exposes interfaces; concrete drivers (`contrib/drivers/mysql`, etc.) register themselves via `init()` when imported. The same model pattern applies to `gredis`, `gcfg` (apollo/nacos/polaris adapters), and `gsvc` (etcd/consul/nacos/polaris/zookeeper registries).
|
||||
- **`internal/` is private.** Sub-packages (`intlog`, `instance`, `reflection`, `utils`, `json`, `command`) are not part of the public API surface — do not import them from outside the root module, and avoid leaking their types in exported signatures.
|
||||
- **Tests use `gtest`, not stdlib `testing` directly.** `test/gtest` provides `gtest.C(t, func(t *gtest.T){...})` blocks, fluent assertions (`t.Assert`, `t.AssertNE`, `t.AssertNil`), and is the project-wide convention. Match this style when adding tests.
|
||||
- **CI matrix.** Tests run on Go 1.23, 1.24, 1.25 across 386 and amd64. `contrib/*` only runs on the latest Go version (`LATEST_GO_VERSION` in `.github/workflows/ci-main.yml`). Code that requires the latest stdlib should live in `contrib/` or be guarded.
|
||||
- **Lint config matters.** `.golangci.yml` enforces a 380-char line limit, function length up to 340 lines, custom import grouping (`gci` with `prefix(github.com/gogf/gf/v2)` ahead of `cmd`/`contrib`/`example`), `gofmt -s` with rewrites (`interface{}` → `any`, `ioutil.*` → `io`/`os`, `reflect.Ptr` → `reflect.Pointer`). Run `make lint` before pushing.
|
||||
- **OpenSpec changes live under `openspec/changes/`** and drive every non-trivial change through the workflow defined below. The active iteration directory must be checked before starting work — see the Development Workflow Rules section.
|
||||
|
||||
# Karpathy Guidelines
|
||||
|
||||
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
|
||||
|
||||
## 1. Think Before Coding
|
||||
|
||||
**Don't assume. Don't hide confusion. Surface tradeoffs.**
|
||||
|
||||
Before implementing:
|
||||
- State your assumptions explicitly. If uncertain, ask.
|
||||
- If multiple interpretations exist, present them - don't pick silently.
|
||||
- If a simpler approach exists, say so. Push back when warranted.
|
||||
- If something is unclear, stop. Name what's confusing. Ask.
|
||||
|
||||
## 2. Simplicity First
|
||||
|
||||
**Minimum code that solves the problem. Nothing speculative.**
|
||||
|
||||
- No features beyond what was asked.
|
||||
- No abstractions for single-use code.
|
||||
- No "flexibility" or "configurability" that wasn't requested.
|
||||
- No error handling for impossible scenarios.
|
||||
- If you write 200 lines and it could be 50, rewrite it.
|
||||
|
||||
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
|
||||
|
||||
## 3. Surgical Changes
|
||||
|
||||
**Touch only what you must. Clean up only your own mess.**
|
||||
|
||||
When editing existing code:
|
||||
- Don't "improve" adjacent code, comments, or formatting.
|
||||
- Don't refactor things that aren't broken.
|
||||
- Match existing style, even if you'd do it differently.
|
||||
- If you notice unrelated dead code, mention it - don't delete it.
|
||||
|
||||
When your changes create orphans:
|
||||
- Remove imports/variables/functions that YOUR changes made unused.
|
||||
- Don't remove pre-existing dead code unless asked.
|
||||
|
||||
The test: Every changed line should trace directly to the user's request.
|
||||
|
||||
## 4. Goal-Driven Execution
|
||||
|
||||
**Define success criteria. Loop until verified.**
|
||||
|
||||
Transform tasks into verifiable goals:
|
||||
- "Add validation" → "Write tests for invalid inputs, then make them pass"
|
||||
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
|
||||
- "Refactor X" → "Ensure tests pass before and after"
|
||||
|
||||
For multi-step tasks, state a brief plan:
|
||||
```
|
||||
1. [Step] → verify: [check]
|
||||
2. [Step] → verify: [check]
|
||||
3. [Step] → verify: [check]
|
||||
```
|
||||
|
||||
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
|
||||
|
||||
# Documentation Writing Rules
|
||||
|
||||
Technical documentation such as `README.md` must follow `.agents/instructions/markdown-format.instructions.md`.
|
||||
|
||||
- All directory-level primary documentation files in the repository must use the English `README.md` and provide a matching Chinese mirror in `README.zh_CN.md`.
|
||||
- When adding a new directory documentation file, create both `README.md` and `README.zh_CN.md` in the same change. Maintaining only one language version is not allowed.
|
||||
|
||||
# Development Workflow Rules
|
||||
|
||||
This project follows `SDD` and uses the `OpenSpec` tool to drive implementation. Change records are stored under `openspec/changes/`. Each change includes `proposal.md`, `design.md`, `specs/`, and `tasks.md`.
|
||||
|
||||
**Execution workflow**:
|
||||
1. Use the `/opsx:explore` slash command at `.agents/prompts/opsx/explore.md` to conduct an exploratory discussion based on the requirement description, analyze the problem, design the solution, and assess risks.
|
||||
2. Once the exploratory discussion finishes and the solution is clear, use the `/opsx:propose` slash command at `.agents/prompts/opsx/propose.md` to turn it into a formal `OpenSpec` change proposal. The command format is `/opsx:propose feature-name`, where `feature-name` is a descriptive name for the current change in `kebab-case`, such as `user-auth` or `data-export`. A new change directory will then be generated automatically under `openspec/changes`, containing incremental spec documents (`spec/`), the technical implementation plan (`design.md`), the proposal and rationale (`proposal.md`), and the implementation task list (`tasks.md`).
|
||||
3. Then run the `/opsx:apply` slash command at `.agents/prompts/opsx/apply.md` to execute the items in `tasks.md` one by one, completing code changes, tests, and documentation updates. After the work is done, the `/gf-review` skill must be invoked for code and spec review.
|
||||
4. When users report issues or improvement requests, the `/gf-feedback` skill must be used to fix and verify them, and the related `OpenSpec` documents must be updated. After the work is done, the `/gf-review` skill must be invoked for review.
|
||||
5. After the user confirms that the current iteration is complete and has no remaining issues, run the `/opsx:archive` slash command at `.agents/prompts/opsx/archive.md` to archive the change. Before archiving, the `/gf-review` skill must be used for a full change review to ensure code quality and compliance with the spec.
|
||||
|
||||
**Key rules**:
|
||||
- **An `OpenSpec` change is considered active until it is archived**: any change directory that still exists directly under `openspec/changes/` and has **not been moved to** `openspec/changes/archive/` is an active change. **Even if the change has completed all tasks and `openspec list --json` shows `status: complete`, it must still be treated as active until the archive step has been executed.**
|
||||
- When a user reports a bug, defect, or improvement request in either Chinese or English, and there is an active `OpenSpec` change in the project, the `gf-feedback` skill must be used. **Unless the user explicitly asks to create a new change, the feedback must always be appended to the current active iteration, even if it is unrelated to the main feature of that iteration**, so that everything can be managed and archived together.
|
||||
- The `/gf-review` review skill is triggered automatically after `/opsx:apply` completes, after `/opsx:feedback` completes, and before `/opsx:archive`.
|
||||
- During development tasks executed with tools such as `Claude Code` or `Codex CLI`, if the work can be parallelized effectively with `SubAgent` and doing so would clearly improve efficiency, that option must be evaluated first and adopted whenever appropriate. Only skip `SubAgent` when the task is strongly dependent on serial context, the split cost is too high, or it introduces obvious collaboration risk.
|
||||
- When creating new iteration documents, the content of `proposal.md`, `design.md`, `tasks.md`, and the incremental specs must be written in English.
|
||||
|
||||
# Code Development Rules
|
||||
- All source code must include comments, such as package comments, file comments, method comments for both public and private methods, constant comments, variable comments, and comments for key logic.
|
||||
- **All submitted code changes must include unit tests**: every submitted code change must add or update focused unit tests that directly cover the affected logic and expected behavior of the changed code path, and the coverage for newly added code must stay at or above 80%; 90% or above is the preferred target when feasible.
|
||||
- **Do not hardcode string literals with enum semantics in backend implementation code**: values that represent statuses, types, stages, actions, execution modes, sort directions, filter operators, or any other enum-like semantics must be managed through Go named types and constants. Writing raw string literals directly in business branching, comparisons, assignments, or persistence logic is forbidden.
|
||||
- **Do not ignore any `error` return value**: every call that may return an `error` must be handled explicitly. Do not use patterns such as `_ = someFunc()`, `_, _ = someFunc()`, or direct calls that discard returned errors. In business flows, errors should be returned explicitly or converted before returning; in initialization, startup, or other critical non-recoverable paths, they should `panic`; in tests and cleanup paths, they must still be asserted, logged, or otherwise handled explicitly rather than silently ignored.
|
||||
- **Do not use stand-alone assignments like `_ = var` to mask unused parameters or local variables**: this placeholder pattern has no business meaning and creates misleading signals about whether the variable was supposed to participate in the logic. Prefer deleting unused variables. If a parameter must be kept to satisfy an interface signature or callback contract, use the blank identifier directly in the function signature, such as `func(ctx context.Context, _ gdb.TX) error`, or omit an unused receiver name instead of adding one-line statements like `_ = tx`, `_ = req`, or `_ = ctx` in the function body.
|
||||
- **File header comment rules**:
|
||||
- Every `Go` source file must include a file-purpose comment at the top of the file. Component-level comments should appear in the component's main file, meaning the file with the same name as the component, such as `plugin.go`, `config.go`, or `file.go`.
|
||||
- In a main file, the component comment must be placed immediately before the `package xxx` declaration with no blank line in between. For example:
|
||||
```go
|
||||
// Package plugin implements plugin manifest discovery, lifecycle orchestration,
|
||||
// governance metadata synchronization, and host integration for LinaPro plugins.
|
||||
package plugin
|
||||
```
|
||||
- Other implementation files must keep only file comments that describe the responsibility of the current file, such as `plugin_xxx.go` or `config_xxx.go`. There must be one blank line between the file comment and `package xxx`, and non-main files must not duplicate component-level descriptions.
|
||||
- **Variable Declarations**: When defining multiple variables, use a `var` block to group them for better alignment and readability:
|
||||
```go
|
||||
// Good - aligned and clean
|
||||
var (
|
||||
authSvc *auth.Service
|
||||
bizCtxSvc *bizctx.Service
|
||||
k8sSvc *svcK8s.Service
|
||||
notebookSvc *notebook.Service
|
||||
middlewareSvc *middleware.Service
|
||||
)
|
||||
|
||||
// Avoid - scattered declarations
|
||||
authSvc := auth.New()
|
||||
bizCtxSvc := bizctx.New()
|
||||
k8sSvc := svcK8s.New()
|
||||
```
|
||||
Apply this pattern when you have 3 or more related variable declarations in the same scope.
|
||||
26
Makefile
26
Makefile
@ -34,6 +34,7 @@ branch:
|
||||
version:
|
||||
@set -e; \
|
||||
newVersion=$(to); \
|
||||
$(MAKE) -C cmd/gf pack; \
|
||||
./.make_version.sh ./ $$newVersion; \
|
||||
echo "make version to=$(to) done"
|
||||
|
||||
@ -52,31 +53,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
|
||||
|
||||
@ -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"/>
|
||||
|
||||
[](https://pkg.go.dev/github.com/gogf/gf/v2)
|
||||
[](https://github.com/gogf/gf/actions/workflows/ci-main.yml)
|
||||
@ -19,6 +19,7 @@ English | [简体中文](README.zh_CN.MD)
|
||||
[](https://github.com/gogf/gf/issues?q=is%3Aissue+is%3Aclosed)
|
||||

|
||||

|
||||
[](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.10.0" alt="goframe contributors"/>
|
||||
<img src="https://goframe.org/img/contributors.svg?version=v2.10.2" alt="goframe contributors"/>
|
||||
</a>
|
||||
|
||||
## License
|
||||
|
||||
@ -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"/>
|
||||
|
||||
[](https://pkg.go.dev/github.com/gogf/gf/v2)
|
||||
[](https://github.com/gogf/gf/actions/workflows/ci-main.yml)
|
||||
@ -19,10 +19,11 @@
|
||||
[](https://github.com/gogf/gf/issues?q=is%3Aissue+is%3Aclosed)
|
||||

|
||||

|
||||
[](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.10.0" alt="goframe contributors"/>
|
||||
<img src="https://goframe.org/img/contributors.svg?version=v2.10.2" alt="goframe contributors"/>
|
||||
</a>
|
||||
|
||||
## 许可证
|
||||
|
||||
`GoFrame` 采用 [MIT License](LICENSE) 许可,100% 免费和开源,永久保持。
|
||||
`GoFrame` 采用 [MIT License](LICENSE) 许可,100%开源和免费。
|
||||
|
||||
@ -18,9 +18,6 @@ pack.template-mono:
|
||||
@cd temp && gf pack template-mono ../internal/packed/template-mono.go -n=packed -y
|
||||
@rm -fr temp
|
||||
|
||||
# Note:
|
||||
# command `sed` only works on MacOS.
|
||||
# use `grep -irl 'template-single' temp| xargs sed -i'' -e 's/template-single/template-mono-app/g'` on other platforms.
|
||||
pack.template-mono-app:
|
||||
@rm -fr temp
|
||||
@mkdir temp || exit 0
|
||||
@ -31,6 +28,6 @@ pack.template-mono-app:
|
||||
@rm -fr temp/template-mono-app/.gitignore
|
||||
@rm -fr temp/template-mono-app/go.mod
|
||||
@rm -fr temp/template-mono-app/go.sum
|
||||
@grep -irl 'template-single' temp| xargs sed -i '' -e 's/template-single/template-mono-app/g'
|
||||
@grep -irl 'template-single' temp| xargs perl -pi -e 's/template-single/template-mono-app/g'
|
||||
@cd temp && gf pack template-mono-app ../internal/packed/template-mono-app.go -n=packed -y
|
||||
@rm -fr temp
|
||||
@ -3,13 +3,14 @@ module github.com/gogf/gf/cmd/gf/v2
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
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/gf/contrib/drivers/clickhouse/v2 v2.10.2
|
||||
github.com/gogf/gf/contrib/drivers/dm/v2 v2.10.2
|
||||
github.com/gogf/gf/contrib/drivers/mssql/v2 v2.10.2
|
||||
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.10.2
|
||||
github.com/gogf/gf/contrib/drivers/oracle/v2 v2.10.2
|
||||
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.2
|
||||
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.2
|
||||
github.com/gogf/gf/v2 v2.10.2
|
||||
github.com/gogf/selfupdate v0.0.0-20231215043001-5c48c528462f
|
||||
github.com/olekukonko/tablewriter v1.1.0
|
||||
github.com/schollz/progressbar/v3 v3.15.0
|
||||
@ -19,6 +20,7 @@ require (
|
||||
|
||||
require (
|
||||
aead.dev/minisign v0.2.0 // indirect
|
||||
gitee.com/chunanyong/dm v1.8.12 // indirect
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.0.15 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
@ -32,6 +34,7 @@ require (
|
||||
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/golang/snappy v0.0.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/grokify/html-strip-tags-go v0.1.0 // indirect
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
aead.dev/minisign v0.2.0 h1:kAWrq/hBRu4AARY6AlciO83xhNnW9UaC8YipS2uhLPk=
|
||||
aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ=
|
||||
gitee.com/chunanyong/dm v1.8.12 h1:WupbFZL0MRNIIiCPaLDHgFi5jkdkjzjPReuWPaInGwk=
|
||||
gitee.com/chunanyong/dm v1.8.12/go.mod h1:EPRJnuPFgbyOFgJ0TRYCTGzhq+ZT4wdyaj/GW/LLcNg=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ=
|
||||
@ -46,6 +48,22 @@ 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.10.2 h1:K9MuyxpkwbQFRypXZnqZm06l0N2p3urM8PEqH45IYXo=
|
||||
github.com/gogf/gf/contrib/drivers/clickhouse/v2 v2.10.2/go.mod h1:Pr/klQ7g0l0qx/MtnFqu9sgeMfVul8ntj/kvGuopJcM=
|
||||
github.com/gogf/gf/contrib/drivers/dm/v2 v2.10.2 h1:jgppTDbSMW/zMRrhvmYFvvArfHQyy556dujwjFRdtUw=
|
||||
github.com/gogf/gf/contrib/drivers/dm/v2 v2.10.2/go.mod h1:FsEjU9SLF4ZSuN8YVkMzCxmFFjEBTbzvXw7D9SzK6IU=
|
||||
github.com/gogf/gf/contrib/drivers/mssql/v2 v2.10.2 h1:7V+23ohcOWvT4Fgf/79uEs51VLfESbhgntkdLL9IPyA=
|
||||
github.com/gogf/gf/contrib/drivers/mssql/v2 v2.10.2/go.mod h1:8mQd1INT1l7c8gYnUdfqlbDdTyq9ZqjkdvlLFdPD6RE=
|
||||
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.10.2 h1:UdUV+7GhwYLpkwz7VrwIVO/1ZYodyzSL5is25NET24A=
|
||||
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.10.2/go.mod h1:eKc+0i3Il7efS2BBjmpy7T9wvN9NGRd67ZV94r9behA=
|
||||
github.com/gogf/gf/contrib/drivers/oracle/v2 v2.10.2 h1:1ufTnX0yqYvfY0h8cMTfcwKnmkfPl/ClJNsbHEboJhc=
|
||||
github.com/gogf/gf/contrib/drivers/oracle/v2 v2.10.2/go.mod h1:gHYoaDSZA2DeZ7e/n6YcplP3fXAjDvijDFz0WijHASU=
|
||||
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.2 h1:u8EpP24GkprogROnJ7htMov9Fc66pTP1eVYrWxiCYOs=
|
||||
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.2/go.mod h1:GmvM3r8GVByVMi4RD2+MCs5+CfxVXPMeT8mVDkAaAXE=
|
||||
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.2 h1:KLS68SWS2W749x7e+eCCOO3UD2Sbw+bIbLEPR8o1FXw=
|
||||
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.2/go.mod h1:uLcsu73PfpyhRc0Jq0gGAWQjN1tyGU9iBRrYgt/lu7g=
|
||||
github.com/gogf/gf/v2 v2.10.2 h1:46IO0Uc8e85/FqdftJFskfDejJLBL0JBnGS5qOftUu8=
|
||||
github.com/gogf/gf/v2 v2.10.2/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=
|
||||
@ -56,6 +74,8 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2V
|
||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
@ -200,6 +220,7 @@ golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
|
||||
@ -14,9 +14,6 @@ import (
|
||||
_ "github.com/gogf/gf/contrib/drivers/pgsql/v2"
|
||||
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||||
|
||||
// do not add dm in cli pre-compilation,
|
||||
// the dm driver does not support certain target platforms.
|
||||
// _ "github.com/gogf/gf/contrib/drivers/dm/v2"
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/cmd/gendao"
|
||||
)
|
||||
|
||||
|
||||
13
cmd/gf/internal/cmd/cmd_gen_dao_dm.go
Normal file
13
cmd/gf/internal/cmd/cmd_gen_dao_dm.go
Normal file
@ -0,0 +1,13 @@
|
||||
// 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.
|
||||
|
||||
//go:build dm
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
_ "github.com/gogf/gf/contrib/drivers/dm/v2"
|
||||
)
|
||||
@ -232,14 +232,17 @@ func (c cInit) initFromBuiltin(ctx context.Context, in cInitInput) (out *cInitOu
|
||||
return content
|
||||
}
|
||||
}
|
||||
mlog.Debugf("replace %s %s to %s", path, cInitRepoPrefix+templateRepoName, in.Module)
|
||||
return gstr.Replace(gfile.GetContents(path), cInitRepoPrefix+templateRepoName, in.Module)
|
||||
}, in.Name, "*", true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Format the generated Go files.
|
||||
utils.GoFmt(in.Name)
|
||||
// Format the generated Go files using go/format (not goimports).
|
||||
// utils.GoFmt uses imports.Process which may remove local import paths that cannot
|
||||
// be resolved in the GOPATH or module cache right after generation (e.g. "myapp/api/hello/v1").
|
||||
geninit.FormatGoFiles(in.Name)
|
||||
|
||||
// Update the GoFrame version.
|
||||
if in.Update {
|
||||
|
||||
@ -412,3 +412,60 @@ func Test_Gen_Dao_Sqlite3(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Gen_Dao_FileNameCaseSnakeFirstUpper(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var (
|
||||
err error
|
||||
db = testDB
|
||||
table = "sys_i18n_message"
|
||||
sqlContent = fmt.Sprintf(
|
||||
gtest.DataContent(`gendao`, `user.tpl.sql`),
|
||||
table,
|
||||
)
|
||||
)
|
||||
dropTableWithDb(db, table)
|
||||
array := gstr.SplitAndTrim(sqlContent, ";")
|
||||
for _, v := range array {
|
||||
if _, err = db.Exec(ctx, v); err != nil {
|
||||
t.AssertNil(err)
|
||||
}
|
||||
}
|
||||
defer dropTableWithDb(db, table)
|
||||
|
||||
var (
|
||||
path = gfile.Temp(guid.S())
|
||||
in = gendao.CGenDaoInput{
|
||||
Path: path,
|
||||
Link: link,
|
||||
Group: "test",
|
||||
Tables: table,
|
||||
FileNameCase: "SnakeFirstUpper",
|
||||
}
|
||||
)
|
||||
err = gutil.FillStructWithDefault(&in)
|
||||
t.AssertNil(err)
|
||||
|
||||
err = gfile.Mkdir(path)
|
||||
t.AssertNil(err)
|
||||
defer gfile.Remove(path)
|
||||
|
||||
err = gfile.Copy(
|
||||
gtest.DataPath("gendao", "go.mod.txt"),
|
||||
gfile.Join(path, "go.mod"),
|
||||
)
|
||||
t.AssertNil(err)
|
||||
|
||||
_, err = gendao.CGenDao{}.Dao(ctx, in)
|
||||
t.AssertNil(err)
|
||||
|
||||
files, err := gfile.ScanDir(path, "*.go", true)
|
||||
t.AssertNil(err)
|
||||
t.Assert(files, []string{
|
||||
filepath.FromSlash(path + "/dao/internal/sys_i18n_message.go"),
|
||||
filepath.FromSlash(path + "/dao/sys_i18n_message.go"),
|
||||
filepath.FromSlash(path + "/model/do/sys_i18n_message.go"),
|
||||
filepath.FromSlash(path + "/model/entity/sys_i18n_message.go"),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@ -46,6 +46,7 @@ type (
|
||||
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"`
|
||||
FileNameCase string `name:"fileNameCase" short:"fc" brief:"{CGenDaoBriefFileNameCase}" d:"Snake"`
|
||||
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"`
|
||||
|
||||
@ -69,7 +69,7 @@ func generateDaoSingle(ctx context.Context, in generateDaoSingleInput) {
|
||||
var (
|
||||
tableNameCamelCase = formatFieldName(in.NewTableName, FieldNameCaseCamel)
|
||||
tableNameCamelLowerCase = formatFieldName(in.NewTableName, FieldNameCaseCamelLower)
|
||||
tableNameSnakeCase = gstr.CaseSnake(in.NewTableName)
|
||||
fileName = formatFileName(in.NewTableName, in.FileNameCase)
|
||||
importPrefix = in.ImportPrefix
|
||||
)
|
||||
if importPrefix == "" {
|
||||
@ -78,13 +78,6 @@ func generateDaoSingle(ctx context.Context, in generateDaoSingleInput) {
|
||||
importPrefix = gstr.Join(g.SliceStr{importPrefix, in.DaoPath}, "/")
|
||||
}
|
||||
|
||||
fileName := gstr.Trim(tableNameSnakeCase, "-_.")
|
||||
if len(fileName) > 5 && fileName[len(fileName)-5:] == "_test" {
|
||||
// Add suffix to avoid the table name which contains "_test",
|
||||
// which would make the go file a testing file.
|
||||
fileName += "_table"
|
||||
}
|
||||
|
||||
// dao - index
|
||||
generateDaoIndex(generateDaoIndexInput{
|
||||
generateDaoSingleInput: in,
|
||||
|
||||
@ -36,7 +36,7 @@ func generateDo(ctx context.Context, in CGenDaoInternalInput) {
|
||||
}
|
||||
var (
|
||||
newTableName = in.NewTableNames[i]
|
||||
doFilePath = gfile.Join(dirPathDo, gstr.CaseSnake(newTableName)+".go")
|
||||
doFilePath = gfile.Join(dirPathDo, formatFileName(newTableName, in.FileNameCase)+".go")
|
||||
structDefinition, _ = generateStructDefinition(ctx, generateStructDefinitionInput{
|
||||
CGenDaoInternalInput: in,
|
||||
TableName: tableName,
|
||||
|
||||
@ -13,7 +13,6 @@ import (
|
||||
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
"github.com/gogf/gf/v2/os/gview"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/consts"
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
|
||||
@ -32,7 +31,7 @@ func generateEntity(ctx context.Context, in CGenDaoInternalInput) {
|
||||
|
||||
var (
|
||||
newTableName = in.NewTableNames[i]
|
||||
entityFilePath = filepath.FromSlash(gfile.Join(dirPathEntity, gstr.CaseSnake(newTableName)+".go"))
|
||||
entityFilePath = filepath.FromSlash(gfile.Join(dirPathEntity, formatFileName(newTableName, in.FileNameCase)+".go"))
|
||||
structDefinition, appendImports = generateStructDefinition(ctx, generateStructDefinitionInput{
|
||||
CGenDaoInternalInput: in,
|
||||
TableName: tableName,
|
||||
|
||||
@ -208,6 +208,28 @@ func formatFieldName(fieldName string, nameCase FieldNameCase) string {
|
||||
}
|
||||
}
|
||||
|
||||
// formatFileName formats and returns a new file name for generated source files.
|
||||
func formatFileName(fileName, nameCase string) string {
|
||||
if nameCase == "" {
|
||||
nameCase = string(gstr.Snake)
|
||||
}
|
||||
fileName = normalizeNameForCaseConvert(fileName)
|
||||
fileName = gstr.Trim(gstr.CaseConvert(fileName, gstr.CaseTypeMatch(nameCase)), "-_.")
|
||||
if len(fileName) > 5 && fileName[len(fileName)-5:] == "_test" {
|
||||
// Add suffix to avoid the table name which contains "_test",
|
||||
// which would make the go file a testing file.
|
||||
fileName += "_table"
|
||||
}
|
||||
return fileName
|
||||
}
|
||||
|
||||
func normalizeNameForCaseConvert(name string) string {
|
||||
if isAllUpper(name) {
|
||||
return strings.ToLower(name)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// isAllUpper checks and returns whether given `fieldName` all letters are upper case.
|
||||
func isAllUpper(fieldName string) bool {
|
||||
for _, b := range fieldName {
|
||||
|
||||
@ -17,7 +17,6 @@ import (
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
"github.com/gogf/gf/v2/os/gview"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal/consts"
|
||||
@ -67,13 +66,7 @@ func generateTableSingle(ctx context.Context, in generateTableSingleInput) {
|
||||
mlog.Fatalf(`fetching tables fields failed for table "%s": %+v`, in.TableName, err)
|
||||
}
|
||||
|
||||
tableNameSnakeCase := gstr.CaseSnake(in.NewTableName)
|
||||
fileName := gstr.Trim(tableNameSnakeCase, "-_.")
|
||||
if len(fileName) > 5 && fileName[len(fileName)-5:] == "_test" {
|
||||
// Add suffix to avoid the table name which contains "_test",
|
||||
// which would make the go file a testing file.
|
||||
fileName += "_table"
|
||||
}
|
||||
fileName := formatFileName(in.NewTableName, in.FileNameCase)
|
||||
path := filepath.FromSlash(gfile.Join(in.DirPathTable, fileName+".go"))
|
||||
in.genItems.AppendGeneratedFilePath(path)
|
||||
if in.OverwriteDao || !gfile.Exists(path) {
|
||||
|
||||
@ -58,23 +58,30 @@ CONFIGURATION SUPPORT
|
||||
CGenDaoBriefStdTime = `use time.Time from stdlib instead of gtime.Time for generated time/date fields of tables`
|
||||
CGenDaoBriefWithTime = `add created time for auto produced go files`
|
||||
CGenDaoBriefGJsonSupport = `use gJsonSupport to use *gjson.Json instead of string for generated json fields of tables`
|
||||
CGenDaoBriefImportPrefix = `custom import prefix for generated go files`
|
||||
CGenDaoBriefDaoPath = `directory path for storing generated dao files under path`
|
||||
CGenDaoBriefTablePath = `directory path for storing generated table files under path`
|
||||
CGenDaoBriefDoPath = `directory path for storing generated do files under path`
|
||||
CGenDaoBriefEntityPath = `directory path for storing generated entity files under path`
|
||||
CGenDaoBriefOverwriteDao = `overwrite all dao files both inside/outside internal folder`
|
||||
CGenDaoBriefModelFile = `custom file name for storing generated model content`
|
||||
CGenDaoBriefModelFileForDao = `custom file name generating model for DAO operations like Where/Data. It's empty in default`
|
||||
CGenDaoBriefDescriptionTag = `add comment to description tag for each field`
|
||||
CGenDaoBriefNoJsonTag = `no json tag will be added for each field`
|
||||
CGenDaoBriefNoModelComment = `no model comment will be added for each field`
|
||||
CGenDaoBriefClear = `delete all generated go files that do not exist in database`
|
||||
CGenDaoBriefGenTable = `generate table files`
|
||||
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`
|
||||
CGenDaoBriefGroup = `
|
||||
CGenDaoBriefFileNameCase = `
|
||||
generated go file name case for dao/table/do/entity files, cases are as follows:
|
||||
| Case | Example |
|
||||
|---------------- |--------------------|
|
||||
| Snake | any_kind_of_string | default
|
||||
| SnakeFirstUpper | rgb_code_md5 |
|
||||
`
|
||||
CGenDaoBriefImportPrefix = `custom import prefix for generated go files`
|
||||
CGenDaoBriefDaoPath = `directory path for storing generated dao files under path`
|
||||
CGenDaoBriefTablePath = `directory path for storing generated table files under path`
|
||||
CGenDaoBriefDoPath = `directory path for storing generated do files under path`
|
||||
CGenDaoBriefEntityPath = `directory path for storing generated entity files under path`
|
||||
CGenDaoBriefOverwriteDao = `overwrite all dao files both inside/outside internal folder`
|
||||
CGenDaoBriefModelFile = `custom file name for storing generated model content`
|
||||
CGenDaoBriefModelFileForDao = `custom file name generating model for DAO operations like Where/Data. It's empty in default`
|
||||
CGenDaoBriefDescriptionTag = `add comment to description tag for each field`
|
||||
CGenDaoBriefNoJsonTag = `no json tag will be added for each field`
|
||||
CGenDaoBriefNoModelComment = `no model comment will be added for each field`
|
||||
CGenDaoBriefClear = `delete all generated go files that do not exist in database`
|
||||
CGenDaoBriefGenTable = `generate table files`
|
||||
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`
|
||||
CGenDaoBriefGroup = `
|
||||
specifying the configuration group name of database for generated ORM instance,
|
||||
it's not necessary and the default value is "default"
|
||||
`
|
||||
@ -128,6 +135,7 @@ func init() {
|
||||
`CGenDaoBriefRemoveFieldPrefix`: CGenDaoBriefRemoveFieldPrefix,
|
||||
`CGenDaoBriefStdTime`: CGenDaoBriefStdTime,
|
||||
`CGenDaoBriefWithTime`: CGenDaoBriefWithTime,
|
||||
`CGenDaoBriefFileNameCase`: CGenDaoBriefFileNameCase,
|
||||
`CGenDaoBriefDaoPath`: CGenDaoBriefDaoPath,
|
||||
`CGenDaoBriefTablePath`: CGenDaoBriefTablePath,
|
||||
`CGenDaoBriefDoPath`: CGenDaoBriefDoPath,
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
)
|
||||
|
||||
// Test containsWildcard function.
|
||||
@ -180,3 +181,12 @@ func Test_filterTablesByPatterns_NonExistent(t *testing.T) {
|
||||
t.AssertNI("nonexistent", result)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_formatFileName(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
t.Assert(formatFileName("sys_i18n_message", ""), "sys_i_18_n_message")
|
||||
t.Assert(formatFileName("sys_i18n_message", string(gstr.SnakeFirstUpper)), "sys_i18n_message")
|
||||
t.Assert(formatFileName("SYS_I18N_MESSAGE", string(gstr.SnakeFirstUpper)), "sys_i18n_message")
|
||||
t.Assert(formatFileName("user_test", string(gstr.SnakeFirstUpper)), "user_test_table")
|
||||
})
|
||||
}
|
||||
|
||||
@ -85,7 +85,7 @@ func generateProject(ctx context.Context, srcPath, name, oldModule, newModule st
|
||||
// 6. Format the generated Go files using go/format (not imports.Process)
|
||||
// Note: We use formatGoFiles instead of utils.GoFmt because imports.Process
|
||||
// may incorrectly "fix" local import paths by replacing them with cached module paths.
|
||||
formatGoFiles(dstPath)
|
||||
FormatGoFiles(dstPath)
|
||||
|
||||
mlog.Print("Project generated successfully!")
|
||||
return nil
|
||||
@ -115,10 +115,10 @@ func upgradeDependencies(ctx context.Context, projectDir string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// formatGoFiles formats all Go files in the directory using go/format.
|
||||
// FormatGoFiles formats all Go files in the directory using go/format.
|
||||
// Unlike imports.Process, this only formats code without modifying imports,
|
||||
// which prevents incorrect "fixing" of local import paths.
|
||||
func formatGoFiles(dir string) {
|
||||
func FormatGoFiles(dir string) {
|
||||
files, err := findGoFiles(dir)
|
||||
if err != nil {
|
||||
mlog.Printf("Failed to find Go files for formatting: %v", err)
|
||||
|
||||
@ -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.2
|
||||
|
||||
require (
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
|
||||
@ -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
|
||||
|
||||
0
cmd/gf/internal/cmd/testdata/issue/4387/go.sum
vendored
Normal file
0
cmd/gf/internal/cmd/testdata/issue/4387/go.sum
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -4,7 +4,7 @@ go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/apolloconfig/agollo/v4 v4.3.1
|
||||
github.com/gogf/gf/v2 v2.10.0
|
||||
github.com/gogf/gf/v2 v2.10.2
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/config/consul/v2
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/gogf/gf/v2 v2.10.0
|
||||
github.com/gogf/gf/v2 v2.10.2
|
||||
github.com/hashicorp/consul/api v1.24.0
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2
|
||||
)
|
||||
|
||||
@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/config/kubecm/v2
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/gogf/gf/v2 v2.10.0
|
||||
github.com/gogf/gf/v2 v2.10.2
|
||||
k8s.io/api v0.33.4
|
||||
k8s.io/apimachinery v0.33.4
|
||||
k8s.io/client-go v0.33.4
|
||||
|
||||
@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/config/nacos/v2
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/gogf/gf/v2 v2.10.0
|
||||
github.com/gogf/gf/v2 v2.10.2
|
||||
github.com/nacos-group/nacos-sdk-go/v2 v2.3.3
|
||||
)
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ module github.com/gogf/gf/contrib/config/polaris/v2
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/gogf/gf/v2 v2.10.0
|
||||
github.com/gogf/gf/v2 v2.10.2
|
||||
github.com/polarismesh/polaris-go v1.6.1
|
||||
)
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.0.15
|
||||
github.com/gogf/gf/v2 v2.10.0
|
||||
github.com/gogf/gf/v2 v2.10.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/shopspring/decimal v1.3.1
|
||||
)
|
||||
|
||||
@ -6,7 +6,7 @@ replace github.com/gogf/gf/v2 => ../../../
|
||||
|
||||
require (
|
||||
gitee.com/chunanyong/dm v1.8.12
|
||||
github.com/gogf/gf/v2 v2.10.0
|
||||
github.com/gogf/gf/v2 v2.10.2
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
102
contrib/drivers/gaussdb/gaussdb_z_unit_feature_ctx_test.go
Normal file
102
contrib/drivers/gaussdb/gaussdb_z_unit_feature_ctx_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
216
contrib/drivers/gaussdb/gaussdb_z_unit_feature_hook_test.go
Normal file
216
contrib/drivers/gaussdb/gaussdb_z_unit_feature_hook_test.go
Normal 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")
|
||||
})
|
||||
}
|
||||
@ -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})
|
||||
})
|
||||
}
|
||||
410
contrib/drivers/gaussdb/gaussdb_z_unit_feature_model_do_test.go
Normal file
410
contrib/drivers/gaussdb/gaussdb_z_unit_feature_model_do_test.go
Normal 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")
|
||||
})
|
||||
}
|
||||
@ -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")
|
||||
})
|
||||
}
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
1004
contrib/drivers/gaussdb/gaussdb_z_unit_feature_scanlist_test.go
Normal file
1004
contrib/drivers/gaussdb/gaussdb_z_unit_feature_scanlist_test.go
Normal file
File diff suppressed because it is too large
Load Diff
1301
contrib/drivers/gaussdb/gaussdb_z_unit_feature_soft_time_test.go
Normal file
1301
contrib/drivers/gaussdb/gaussdb_z_unit_feature_soft_time_test.go
Normal file
File diff suppressed because it is too large
Load Diff
146
contrib/drivers/gaussdb/gaussdb_z_unit_feature_union_test.go
Normal file
146
contrib/drivers/gaussdb/gaussdb_z_unit_feature_union_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
1592
contrib/drivers/gaussdb/gaussdb_z_unit_feature_with_test.go
Normal file
1592
contrib/drivers/gaussdb/gaussdb_z_unit_feature_with_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
|
||||
1213
contrib/drivers/gaussdb/gaussdb_z_unit_model_where_test.go
Normal file
1213
contrib/drivers/gaussdb/gaussdb_z_unit_model_where_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
1693
contrib/drivers/gaussdb/gaussdb_z_unit_transaction_test.go
Normal file
1693
contrib/drivers/gaussdb/gaussdb_z_unit_transaction_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -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.10.0
|
||||
github.com/gogf/gf/v2 v2.10.2
|
||||
github.com/google/uuid v1.6.0
|
||||
)
|
||||
|
||||
|
||||
6
contrib/drivers/gaussdb/testdata/table_with_prefix.sql
vendored
Normal file
6
contrib/drivers/gaussdb/testdata/table_with_prefix.sql
vendored
Normal 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');
|
||||
30
contrib/drivers/gaussdb/testdata/with_multiple_depends.sql
vendored
Normal file
30
contrib/drivers/gaussdb/testdata/with_multiple_depends.sql
vendored
Normal 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');
|
||||
4
contrib/drivers/gaussdb/testdata/with_tpl_user.sql
vendored
Normal file
4
contrib/drivers/gaussdb/testdata/with_tpl_user.sql
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
CREATE TABLE IF NOT EXISTS %s (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name varchar(45) NOT NULL
|
||||
);
|
||||
4
contrib/drivers/gaussdb/testdata/with_tpl_user_detail.sql
vendored
Normal file
4
contrib/drivers/gaussdb/testdata/with_tpl_user_detail.sql
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
CREATE TABLE IF NOT EXISTS %s (
|
||||
uid SERIAL PRIMARY KEY,
|
||||
address varchar(45) NOT NULL
|
||||
);
|
||||
5
contrib/drivers/gaussdb/testdata/with_tpl_user_scores.sql
vendored
Normal file
5
contrib/drivers/gaussdb/testdata/with_tpl_user_scores.sql
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
CREATE TABLE IF NOT EXISTS %s (
|
||||
id SERIAL PRIMARY KEY,
|
||||
uid integer NOT NULL,
|
||||
score integer NOT NULL
|
||||
);
|
||||
@ -3,8 +3,8 @@ module github.com/gogf/gf/contrib/drivers/mariadb/v2
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.10.0
|
||||
github.com/gogf/gf/v2 v2.10.0
|
||||
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.10.2
|
||||
github.com/gogf/gf/v2 v2.10.2
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
@ -9,6 +9,7 @@ package mariadb_test
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
_ "github.com/gogf/gf/contrib/drivers/mariadb/v2"
|
||||
@ -21,18 +22,24 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
TableSize = 10
|
||||
TableName = "user"
|
||||
TestSchema1 = "test1"
|
||||
TestSchema2 = "test2"
|
||||
TestDbPass = "12345678"
|
||||
CreateTime = "2018-10-24 10:00:00"
|
||||
TableSize = 10
|
||||
TableName = "user"
|
||||
TestSchema1 = "test1"
|
||||
TestSchema2 = "test2"
|
||||
TestPartitionDB = "test3"
|
||||
TableNamePrefix1 = "gf_"
|
||||
TestDbUser = "root"
|
||||
TestDbPass = "12345678"
|
||||
CreateTime = "2018-10-24 10:00:00"
|
||||
)
|
||||
|
||||
var (
|
||||
db gdb.DB
|
||||
db2 gdb.DB
|
||||
ctx = context.TODO()
|
||||
db gdb.DB
|
||||
db2 gdb.DB
|
||||
db3 gdb.DB
|
||||
dbPrefix gdb.DB
|
||||
dbInvalid gdb.DB
|
||||
ctx = context.TODO()
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -41,10 +48,26 @@ func init() {
|
||||
Link: fmt.Sprintf("mariadb:root:%s@tcp(127.0.0.1:3307)/?loc=Local&parseTime=true", TestDbPass),
|
||||
TranTimeout: time.Second * 3,
|
||||
}
|
||||
err := gdb.AddConfigNode(gdb.DefaultGroupName, nodeDefault)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
partitionDefault := gdb.ConfigNode{
|
||||
Link: fmt.Sprintf("mariadb:root:%s@tcp(127.0.0.1:3307)/?loc=Local&parseTime=true", TestDbPass),
|
||||
Debug: true,
|
||||
TranTimeout: time.Second * 3,
|
||||
}
|
||||
nodePrefix := gdb.ConfigNode{
|
||||
Link: fmt.Sprintf("mariadb:root:%s@tcp(127.0.0.1:3307)/?loc=Local&parseTime=true", TestDbPass),
|
||||
TranTimeout: time.Second * 3,
|
||||
}
|
||||
nodePrefix.Prefix = TableNamePrefix1
|
||||
|
||||
nodeInvalid := gdb.ConfigNode{
|
||||
Link: fmt.Sprintf("mariadb:root:%s@tcp(127.0.0.1:3317)/?loc=Local&parseTime=true", TestDbPass),
|
||||
TranTimeout: time.Second * 3,
|
||||
}
|
||||
gdb.AddConfigNode("test", nodeDefault)
|
||||
gdb.AddConfigNode("prefix", nodePrefix)
|
||||
gdb.AddConfigNode("nodeinvalid", nodeInvalid)
|
||||
gdb.AddConfigNode("partition", partitionDefault)
|
||||
gdb.AddConfigNode(gdb.DefaultGroupName, nodeDefault)
|
||||
|
||||
// Default db.
|
||||
if r, err := gdb.NewByGroup(); err != nil {
|
||||
@ -59,8 +82,33 @@ func init() {
|
||||
if _, err := db.Exec(ctx, fmt.Sprintf(schemaTemplate, TestSchema2)); err != nil {
|
||||
gtest.Error(err)
|
||||
}
|
||||
if _, err := db.Exec(ctx, fmt.Sprintf(schemaTemplate, TestPartitionDB)); err != nil {
|
||||
gtest.Error(err)
|
||||
}
|
||||
db = db.Schema(TestSchema1)
|
||||
db2 = db.Schema(TestSchema2)
|
||||
db3 = db.Schema(TestPartitionDB)
|
||||
// Prefix db.
|
||||
if r, err := gdb.NewByGroup("prefix"); err != nil {
|
||||
gtest.Error(err)
|
||||
} else {
|
||||
dbPrefix = r
|
||||
}
|
||||
if _, err := dbPrefix.Exec(ctx, fmt.Sprintf(schemaTemplate, TestSchema1)); err != nil {
|
||||
gtest.Error(err)
|
||||
}
|
||||
if _, err := dbPrefix.Exec(ctx, fmt.Sprintf(schemaTemplate, TestSchema2)); err != nil {
|
||||
gtest.Error(err)
|
||||
}
|
||||
dbPrefix = dbPrefix.Schema(TestSchema1)
|
||||
|
||||
// Invalid db.
|
||||
if r, err := gdb.NewByGroup("nodeinvalid"); err != nil {
|
||||
gtest.Error(err)
|
||||
} else {
|
||||
dbInvalid = r
|
||||
}
|
||||
dbInvalid = dbInvalid.Schema(TestSchema1)
|
||||
}
|
||||
|
||||
func createTable(table ...string) string {
|
||||
@ -126,3 +174,61 @@ func dropTableWithDb(db gdb.DB, table string) {
|
||||
gtest.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_PartitionTable(t *testing.T) {
|
||||
dropShopDBTable()
|
||||
createShopDBTable()
|
||||
insertShopDBData()
|
||||
|
||||
// defer dropShopDBTable()
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data, err := db3.Ctx(ctx).Model("dbx_order").Partition("p3", "p4").All()
|
||||
t.AssertNil(err)
|
||||
dataLen := len(data)
|
||||
t.Assert(dataLen, 5)
|
||||
data, err = db3.Ctx(ctx).Model("dbx_order").Partition("p3").All()
|
||||
t.AssertNil(err)
|
||||
dataLen = len(data)
|
||||
t.Assert(dataLen, 5)
|
||||
})
|
||||
}
|
||||
|
||||
func createShopDBTable() {
|
||||
sql := `CREATE TABLE dbx_order (
|
||||
id int(11) NOT NULL,
|
||||
sales_date date DEFAULT NULL,
|
||||
amount decimal(10,2) DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
PARTITION BY RANGE (YEAR(sales_date))
|
||||
(PARTITION p1 VALUES LESS THAN (2020) ENGINE = InnoDB,
|
||||
PARTITION p2 VALUES LESS THAN (2021) ENGINE = InnoDB,
|
||||
PARTITION p3 VALUES LESS THAN (2022) ENGINE = InnoDB,
|
||||
PARTITION p4 VALUES LESS THAN MAXVALUE ENGINE = InnoDB);`
|
||||
_, err := db3.Exec(ctx, sql)
|
||||
if err != nil {
|
||||
gtest.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func insertShopDBData() {
|
||||
data := g.Slice{}
|
||||
year := 2020
|
||||
for i := 1; i <= 5; i++ {
|
||||
year++
|
||||
data = append(data, g.Map{
|
||||
"id": i,
|
||||
"sales_date": fmt.Sprintf("%d-09-21", year),
|
||||
"amount": fmt.Sprintf("1%d.21", i),
|
||||
})
|
||||
}
|
||||
_, err := db3.Model("dbx_order").Ctx(ctx).Data(data).Insert()
|
||||
if err != nil {
|
||||
gtest.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func dropShopDBTable() {
|
||||
if _, err := db3.Exec(ctx, "DROP TABLE IF EXISTS `dbx_order`"); err != nil {
|
||||
gtest.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
83
contrib/drivers/mariadb/mariadb_z_unit_basic_test.go
Normal file
83
contrib/drivers/mariadb/mariadb_z_unit_basic_test.go
Normal file
@ -0,0 +1,83 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package mariadb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
)
|
||||
|
||||
func Test_Instance(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
_, err := gdb.Instance("none")
|
||||
t.AssertNE(err, nil)
|
||||
|
||||
db, err := gdb.Instance()
|
||||
t.AssertNil(err)
|
||||
|
||||
err1 := db.PingMaster()
|
||||
err2 := db.PingSlave()
|
||||
t.Assert(err1, nil)
|
||||
t.Assert(err2, nil)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Func_FormatSqlWithArgs(t *testing.T) {
|
||||
// mysql
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var s string
|
||||
s = gdb.FormatSqlWithArgs("select * from table where id>=? and sex=?", []any{100, 1})
|
||||
t.Assert(s, "select * from table where id>=100 and sex=1")
|
||||
})
|
||||
// mssql
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var s string
|
||||
s = gdb.FormatSqlWithArgs("select * from table where id>=@p1 and sex=@p2", []any{100, 1})
|
||||
t.Assert(s, "select * from table where id>=100 and sex=1")
|
||||
})
|
||||
// pgsql
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var s string
|
||||
s = gdb.FormatSqlWithArgs("select * from table where id>=$1 and sex=$2", []any{100, 1})
|
||||
t.Assert(s, "select * from table where id>=100 and sex=1")
|
||||
})
|
||||
// oracle
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var s string
|
||||
s = gdb.FormatSqlWithArgs("select * from table where id>=:v1 and sex=:v2", []any{100, 1})
|
||||
t.Assert(s, "select * from table where id>=100 and sex=1")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Func_ToSQL(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
sql, err := gdb.ToSQL(ctx, func(ctx context.Context) error {
|
||||
value, err := db.Ctx(ctx).Model(TableName).Fields("nickname").Where("id", 1).Value()
|
||||
t.Assert(value, nil)
|
||||
return err
|
||||
})
|
||||
t.AssertNil(err)
|
||||
t.Assert(sql, "SELECT `nickname` FROM `user` WHERE `id`=1 LIMIT 1")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Func_CatchSQL(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
array, err := gdb.CatchSQL(ctx, func(ctx context.Context) error {
|
||||
value, err := db.Ctx(ctx).Model(table).Fields("nickname").Where("id", 1).Value()
|
||||
t.Assert(value, "name_1")
|
||||
return err
|
||||
})
|
||||
t.AssertNil(err)
|
||||
t.AssertGE(len(array), 1)
|
||||
})
|
||||
}
|
||||
1713
contrib/drivers/mariadb/mariadb_z_unit_core_test.go
Normal file
1713
contrib/drivers/mariadb/mariadb_z_unit_core_test.go
Normal file
File diff suppressed because it is too large
Load Diff
337
contrib/drivers/mariadb/mariadb_z_unit_feature_batch_test.go
Normal file
337
contrib/drivers/mariadb/mariadb_z_unit_feature_batch_test.go
Normal file
@ -0,0 +1,337 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package mariadb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
)
|
||||
|
||||
// Test_Model_Batch_Insert tests batch insert with different batch sizes
|
||||
func Test_Model_Batch_Insert(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Prepare data for batch insert
|
||||
data := g.Slice{}
|
||||
for i := 1; i <= 10; i++ {
|
||||
data = append(data, g.Map{
|
||||
"id": i,
|
||||
"passport": fmt.Sprintf("batch_user_%d", i),
|
||||
"password": fmt.Sprintf("batch_pass_%d", i),
|
||||
"nickname": fmt.Sprintf("batch_name_%d", i),
|
||||
})
|
||||
}
|
||||
|
||||
// Batch insert with batch size 3
|
||||
result, err := db.Model(table).Batch(3).Data(data).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 10)
|
||||
|
||||
// Verify all records were inserted
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 10)
|
||||
|
||||
// Verify specific records
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], "batch_user_1")
|
||||
|
||||
one, err = db.Model(table).Where("id", 10).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], "batch_user_10")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Batch_Replace tests batch replace operation
|
||||
func Test_Model_Batch_Replace(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Initial insert
|
||||
data := g.Slice{}
|
||||
for i := 1; i <= 5; i++ {
|
||||
data = append(data, g.Map{
|
||||
"id": i,
|
||||
"passport": fmt.Sprintf("original_%d", i),
|
||||
})
|
||||
}
|
||||
_, err := db.Model(table).Data(data).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Batch replace with overlapping ids
|
||||
replaceData := g.Slice{}
|
||||
for i := 3; i <= 8; i++ {
|
||||
replaceData = append(replaceData, g.Map{
|
||||
"id": i,
|
||||
"passport": fmt.Sprintf("replaced_%d", i),
|
||||
"nickname": fmt.Sprintf("new_name_%d", i),
|
||||
})
|
||||
}
|
||||
result, err := db.Model(table).Batch(2).Data(replaceData).Replace()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.AssertGT(n, 0)
|
||||
|
||||
// Verify replaced records
|
||||
one, err := db.Model(table).Where("id", 3).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], "replaced_3")
|
||||
t.Assert(one["nickname"], "new_name_3")
|
||||
|
||||
// Verify new records
|
||||
one, err = db.Model(table).Where("id", 8).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], "replaced_8")
|
||||
|
||||
// Verify total count
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 8) // ids 1-8
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Batch_Save tests batch save operation
|
||||
func Test_Model_Batch_Save(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Initial data
|
||||
data := g.Slice{}
|
||||
for i := 1; i <= 5; i++ {
|
||||
data = append(data, g.Map{
|
||||
"id": i,
|
||||
"passport": fmt.Sprintf("save_user_%d", i),
|
||||
})
|
||||
}
|
||||
_, err := db.Model(table).Data(data).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Batch save with overlapping and new ids
|
||||
saveData := g.Slice{}
|
||||
for i := 3; i <= 8; i++ {
|
||||
saveData = append(saveData, g.Map{
|
||||
"id": i,
|
||||
"passport": fmt.Sprintf("saved_%d", i),
|
||||
"nickname": fmt.Sprintf("save_name_%d", i),
|
||||
})
|
||||
}
|
||||
result, err := db.Model(table).Batch(3).Data(saveData).Save()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.AssertGT(n, 0)
|
||||
|
||||
// Verify updated records
|
||||
one, err := db.Model(table).Where("id", 3).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], "saved_3")
|
||||
|
||||
// Verify inserted records
|
||||
one, err = db.Model(table).Where("id", 8).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], "saved_8")
|
||||
|
||||
// Verify total count
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 8)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Batch_LargeBatch tests batch operation with large dataset
|
||||
func Test_Model_Batch_LargeBatch(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Prepare 1000+ records
|
||||
data := g.Slice{}
|
||||
totalRecords := 1500
|
||||
for i := 1; i <= totalRecords; i++ {
|
||||
data = append(data, g.Map{
|
||||
"id": i,
|
||||
"passport": fmt.Sprintf("large_user_%d", i),
|
||||
"nickname": fmt.Sprintf("large_name_%d", i),
|
||||
})
|
||||
}
|
||||
|
||||
// Batch insert with batch size 100
|
||||
result, err := db.Model(table).Batch(100).Data(data).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, totalRecords)
|
||||
|
||||
// Verify count
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, totalRecords)
|
||||
|
||||
// Verify first and last records
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], "large_user_1")
|
||||
|
||||
one, err = db.Model(table).Where("id", totalRecords).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], fmt.Sprintf("large_user_%d", totalRecords))
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Batch_EmptyBatch tests batch operation with empty data
|
||||
func Test_Model_Batch_EmptyBatch(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Empty slice
|
||||
data := g.Slice{}
|
||||
|
||||
// Batch insert with empty data should return error
|
||||
_, err := db.Model(table).Batch(10).Data(data).Insert()
|
||||
t.AssertNE(err, nil)
|
||||
t.AssertIN(err.Error(), "data list cannot be empty")
|
||||
|
||||
// Verify no records inserted
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 0)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Batch_SingleRecord tests batch operation with single record
|
||||
func Test_Model_Batch_SingleRecord(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Single record batch insert
|
||||
data := g.Slice{
|
||||
g.Map{
|
||||
"id": 1,
|
||||
"passport": "single_user",
|
||||
"nickname": "single_name",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := db.Model(table).Batch(10).Data(data).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
// Verify the record
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], "single_user")
|
||||
t.Assert(one["nickname"], "single_name")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Batch_VsBatch tests performance comparison between different batch sizes
|
||||
func Test_Model_Batch_VsBatch(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Prepare data
|
||||
data := g.Slice{}
|
||||
for i := 1; i <= 100; i++ {
|
||||
data = append(data, g.Map{
|
||||
"id": i,
|
||||
"passport": fmt.Sprintf("perf_user_%d", i),
|
||||
})
|
||||
}
|
||||
|
||||
// Test with batch size 1
|
||||
result, err := db.Model(table).Batch(1).Data(data).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 100)
|
||||
|
||||
// Clean up
|
||||
_, err = db.Model(table).Where("1=1").Delete()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Test with batch size 10
|
||||
result, err = db.Model(table).Batch(10).Data(data).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ = result.RowsAffected()
|
||||
t.Assert(n, 100)
|
||||
|
||||
// Clean up
|
||||
_, err = db.Model(table).Where("1=1").Delete()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Test with batch size 50
|
||||
result, err = db.Model(table).Batch(50).Data(data).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ = result.RowsAffected()
|
||||
t.Assert(n, 100)
|
||||
|
||||
// All batch sizes should produce same result
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 100)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Batch_WithTransaction tests batch operation within transaction
|
||||
func Test_Model_Batch_WithTransaction(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Slice{}
|
||||
for i := 1; i <= 50; i++ {
|
||||
data = append(data, g.Map{
|
||||
"id": i,
|
||||
"passport": fmt.Sprintf("tx_batch_%d", i),
|
||||
})
|
||||
}
|
||||
|
||||
// Test commit
|
||||
err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
result, err := tx.Model(table).Batch(10).Data(data).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 50)
|
||||
return nil
|
||||
})
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify commit
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 50)
|
||||
|
||||
// Clean up
|
||||
_, err = db.Model(table).Where("1=1").Delete()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Test rollback
|
||||
err = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
_, err := tx.Model(table).Batch(10).Data(data).Insert()
|
||||
t.AssertNil(err)
|
||||
return fmt.Errorf("rollback test")
|
||||
})
|
||||
t.AssertNE(err, nil)
|
||||
|
||||
// Verify rollback - no records should exist
|
||||
count, err = db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 0)
|
||||
})
|
||||
}
|
||||
300
contrib/drivers/mariadb/mariadb_z_unit_feature_cache_test.go
Normal file
300
contrib/drivers/mariadb/mariadb_z_unit_feature_cache_test.go
Normal file
@ -0,0 +1,300 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package mariadb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
)
|
||||
|
||||
// Test_Model_Cache_Basic tests basic cache functionality
|
||||
func Test_Model_Cache_Basic(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// First query - cache miss, result from DB
|
||||
one, err := db.Model(table).Cache(gdb.CacheOption{
|
||||
Duration: time.Second * 10,
|
||||
Name: "test_cache_basic",
|
||||
}).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["id"], 1)
|
||||
t.Assert(one["passport"], "user_1")
|
||||
|
||||
// Update the record in DB
|
||||
_, err = db.Model(table).Data(g.Map{"passport": "updated_user"}).Where("id", 1).Update()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Second query - cache hit, still returns old cached value
|
||||
one, err = db.Model(table).Cache(gdb.CacheOption{
|
||||
Duration: time.Second * 10,
|
||||
Name: "test_cache_basic",
|
||||
}).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], "user_1") // cached value, not "updated_user"
|
||||
|
||||
// Query without cache - returns updated value from DB
|
||||
one, err = db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], "updated_user")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Cache_TTL tests cache TTL expiration
|
||||
func Test_Model_Cache_TTL(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Cache with short TTL
|
||||
one, err := db.Model(table).Cache(gdb.CacheOption{
|
||||
Duration: time.Millisecond * 100, // 100ms TTL
|
||||
Name: "test_cache_ttl",
|
||||
}).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], "user_1")
|
||||
|
||||
// Update record
|
||||
_, err = db.Model(table).Data(g.Map{"passport": "ttl_test"}).Where("id", 1).Update()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Immediate query - cache still valid
|
||||
one, err = db.Model(table).Cache(gdb.CacheOption{
|
||||
Duration: time.Millisecond * 100,
|
||||
Name: "test_cache_ttl",
|
||||
}).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], "user_1") // cached value
|
||||
|
||||
// Wait for cache to expire
|
||||
time.Sleep(time.Millisecond * 150)
|
||||
|
||||
// Query after expiration - should get fresh data
|
||||
one, err = db.Model(table).Cache(gdb.CacheOption{
|
||||
Duration: time.Millisecond * 100,
|
||||
Name: "test_cache_ttl",
|
||||
}).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], "ttl_test") // fresh value from DB
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Cache_Clear tests clearing cache with negative duration
|
||||
func Test_Model_Cache_Clear(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Set cache
|
||||
one, err := db.Model(table).Cache(gdb.CacheOption{
|
||||
Duration: time.Second * 60,
|
||||
Name: "test_cache_clear",
|
||||
}).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], "user_1")
|
||||
|
||||
// Update record and clear cache
|
||||
_, err = db.Model(table).Cache(gdb.CacheOption{
|
||||
Duration: -1,
|
||||
Name: "test_cache_clear",
|
||||
}).Data(g.Map{"passport": "cleared"}).Where("id", 1).Update()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Query again - should get fresh data since cache was cleared
|
||||
one, err = db.Model(table).Cache(gdb.CacheOption{
|
||||
Duration: time.Second * 60,
|
||||
Name: "test_cache_clear",
|
||||
}).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], "cleared")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Cache_NoExpire tests cache with no expiration (Duration=0)
|
||||
func Test_Model_Cache_NoExpire(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Cache with no expiration
|
||||
one, err := db.Model(table).Cache(gdb.CacheOption{
|
||||
Duration: 0, // never expires
|
||||
Name: "test_cache_no_expire",
|
||||
}).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], "user_1")
|
||||
|
||||
// Update record
|
||||
_, err = db.Model(table).Data(g.Map{"passport": "no_expire_test"}).Where("id", 1).Update()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Wait a bit
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
|
||||
// Query - cache should still be valid
|
||||
one, err = db.Model(table).Cache(gdb.CacheOption{
|
||||
Duration: 0,
|
||||
Name: "test_cache_no_expire",
|
||||
}).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], "user_1") // cached value persists
|
||||
|
||||
// Clear the cache with update operation
|
||||
_, err = db.Model(table).Cache(gdb.CacheOption{
|
||||
Duration: -1,
|
||||
Name: "test_cache_no_expire",
|
||||
}).Data(g.Map{"nickname": "cleared"}).Where("id", 1).Update()
|
||||
t.AssertNil(err)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Cache_Force tests Force option to cache nil results
|
||||
func Test_Model_Cache_Force(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
// Note: Removed Force cache test due to cache invalidation on INSERT
|
||||
// The test logic was flawed - INSERT operations clear cache, so cached nil
|
||||
// results would be invalidated before the second query
|
||||
}
|
||||
|
||||
// Test_Model_Cache_DisabledInTransaction tests cache is disabled in transactions
|
||||
func Test_Model_Cache_DisabledInTransaction(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
// First query in transaction
|
||||
one, err := tx.Model(table).Cache(gdb.CacheOption{
|
||||
Duration: time.Second * 10,
|
||||
Name: "test_tx_cache",
|
||||
}).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], "user_1")
|
||||
|
||||
// Update in transaction
|
||||
_, err = tx.Model(table).Data(g.Map{"passport": "tx_update"}).Where("id", 1).Update()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Second query - should see updated value (cache disabled in tx)
|
||||
one, err = tx.Model(table).Cache(gdb.CacheOption{
|
||||
Duration: time.Second * 10,
|
||||
Name: "test_tx_cache",
|
||||
}).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], "tx_update") // not cached, fresh from DB
|
||||
|
||||
return nil
|
||||
})
|
||||
t.AssertNil(err)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_PageCache tests pagination cache
|
||||
func Test_Model_PageCache(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// First page query with cache
|
||||
all, err := db.Model(table).PageCache(
|
||||
gdb.CacheOption{Duration: time.Second * 10, Name: "test_page_count"},
|
||||
gdb.CacheOption{Duration: time.Second * 10, Name: "test_page_data"},
|
||||
).Page(1, 3).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 3)
|
||||
|
||||
// Insert new record
|
||||
_, err = db.Model(table).Data(g.Map{
|
||||
"id": 11,
|
||||
"passport": "user_11",
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Query again - should return cached results
|
||||
all, err = db.Model(table).PageCache(
|
||||
gdb.CacheOption{Duration: time.Second * 10, Name: "test_page_count"},
|
||||
gdb.CacheOption{Duration: time.Second * 10, Name: "test_page_data"},
|
||||
).Page(1, 3).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 3) // cached results
|
||||
|
||||
// Clear page cache by updating with Duration=-1
|
||||
_, err = db.Model(table).Cache(gdb.CacheOption{
|
||||
Duration: -1,
|
||||
Name: "test_page_count",
|
||||
}).Data(g.Map{"nickname": "page_test"}).Where("id", 1).Update()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Query with fresh cache - should return updated count
|
||||
all, err = db.Model(table).PageCache(
|
||||
gdb.CacheOption{Duration: time.Second * 10, Name: "test_page_count"},
|
||||
gdb.CacheOption{Duration: time.Second * 10, Name: "test_page_data"},
|
||||
).Page(1, 3).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 3) // still 3 items per page
|
||||
|
||||
// Verify total count increased
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 11)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Cache_DifferentNames tests different cache names for same query
|
||||
func Test_Model_Cache_DifferentNames(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Cache with name1
|
||||
one, err := db.Model(table).Cache(gdb.CacheOption{
|
||||
Duration: time.Second * 10,
|
||||
Name: "cache_name1",
|
||||
}).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], "user_1")
|
||||
|
||||
// Cache same query with name2
|
||||
one, err = db.Model(table).Cache(gdb.CacheOption{
|
||||
Duration: time.Second * 10,
|
||||
Name: "cache_name2",
|
||||
}).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], "user_1")
|
||||
|
||||
// Update record and clear only cache_name1
|
||||
_, err = db.Model(table).Cache(gdb.CacheOption{
|
||||
Duration: -1,
|
||||
Name: "cache_name1",
|
||||
}).Data(g.Map{"passport": "diff_name"}).Where("id", 1).Update()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Query with cache_name1 - should get fresh data
|
||||
one, err = db.Model(table).Cache(gdb.CacheOption{
|
||||
Duration: time.Second * 10,
|
||||
Name: "cache_name1",
|
||||
}).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], "diff_name")
|
||||
|
||||
// Query with cache_name2 - should still have cached old value
|
||||
one, err = db.Model(table).Cache(gdb.CacheOption{
|
||||
Duration: time.Second * 10,
|
||||
Name: "cache_name2",
|
||||
}).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], "user_1") // still cached
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,338 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package mariadb_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
)
|
||||
|
||||
// Test_Concurrent_Insert tests concurrent Insert operations
|
||||
func Test_Concurrent_Insert(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var wg sync.WaitGroup
|
||||
concurrency := 10
|
||||
|
||||
wg.Add(concurrency)
|
||||
for i := 0; i < concurrency; i++ {
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
_, err := db.Model(table).Insert(g.Map{
|
||||
"passport": fmt.Sprintf("user_%d", id),
|
||||
"password": fmt.Sprintf("pass_%d", id),
|
||||
"nickname": fmt.Sprintf("name_%d", id),
|
||||
})
|
||||
t.AssertNil(err)
|
||||
}(i + 1)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Verify all records inserted
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, concurrency)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Concurrent_Update tests concurrent Update operations
|
||||
func Test_Concurrent_Update(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var wg sync.WaitGroup
|
||||
concurrency := 5
|
||||
|
||||
wg.Add(concurrency)
|
||||
for i := 0; i < concurrency; i++ {
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"nickname": fmt.Sprintf("updated_%d", id),
|
||||
}).Where("id", id+1).Update()
|
||||
t.AssertNil(err)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Verify updates
|
||||
for i := 0; i < concurrency; i++ {
|
||||
one, err := db.Model(table).Where("id", i+1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["nickname"].String(), fmt.Sprintf("updated_%d", i))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Concurrent_Delete tests concurrent Delete operations
|
||||
func Test_Concurrent_Delete(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var wg sync.WaitGroup
|
||||
concurrency := 5
|
||||
|
||||
wg.Add(concurrency)
|
||||
for i := 0; i < concurrency; i++ {
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
_, err := db.Model(table).Where("id", id+1).Delete()
|
||||
t.AssertNil(err)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Verify deletions
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, TableSize-concurrency)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Concurrent_Query tests concurrent Query operations
|
||||
func Test_Concurrent_Query(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var wg sync.WaitGroup
|
||||
concurrency := 20
|
||||
|
||||
wg.Add(concurrency)
|
||||
for i := 0; i < concurrency; i++ {
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
result, err := db.Model(table).Where("id", (id%TableSize)+1).One()
|
||||
t.AssertNil(err)
|
||||
t.AssertNE(result, nil)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Concurrent_Transaction tests concurrent transaction operations
|
||||
func Test_Concurrent_Transaction(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var wg sync.WaitGroup
|
||||
concurrency := 10
|
||||
|
||||
wg.Add(concurrency)
|
||||
for i := 0; i < concurrency; i++ {
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
err := db.Transaction(ctx, func(ctx g.Ctx, tx gdb.TX) error {
|
||||
_, err := tx.Model(table).Insert(g.Map{
|
||||
"passport": fmt.Sprintf("user_%d", id),
|
||||
"password": fmt.Sprintf("pass_%d", id),
|
||||
"nickname": fmt.Sprintf("name_%d", id),
|
||||
})
|
||||
return err
|
||||
})
|
||||
t.AssertNil(err)
|
||||
}(i + 1)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Verify all transactions committed
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, concurrency)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Concurrent_Mixed_Operations tests mixed concurrent operations
|
||||
func Test_Concurrent_Mixed_Operations(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var wg sync.WaitGroup
|
||||
operations := 30
|
||||
|
||||
wg.Add(operations)
|
||||
for i := 0; i < operations; i++ {
|
||||
op := i % 3
|
||||
switch op {
|
||||
case 0: // Insert
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
_, _ = db.Model(table).Insert(g.Map{
|
||||
"passport": fmt.Sprintf("new_user_%d", id),
|
||||
"password": fmt.Sprintf("new_pass_%d", id),
|
||||
"nickname": fmt.Sprintf("new_name_%d", id),
|
||||
})
|
||||
}(i)
|
||||
case 1: // Update
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
targetId := (id % TableSize) + 1
|
||||
_, _ = db.Model(table).Data(g.Map{
|
||||
"nickname": fmt.Sprintf("concurrent_%d", id),
|
||||
}).Where("id", targetId).Update()
|
||||
}(i)
|
||||
case 2: // Query
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
targetId := (id % TableSize) + 1
|
||||
_, _ = db.Model(table).Where("id", targetId).One()
|
||||
}(i)
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Verify database is still consistent
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.AssertGT(count, TableSize)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Concurrent_Connection_Pool tests connection pool under load
|
||||
func Test_Concurrent_Connection_Pool(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var wg sync.WaitGroup
|
||||
concurrency := 50
|
||||
|
||||
wg.Add(concurrency)
|
||||
for i := 0; i < concurrency; i++ {
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
// Each goroutine performs multiple operations
|
||||
for j := 0; j < 5; j++ {
|
||||
_, err := db.Model(table).Where("id", (id%TableSize)+1).One()
|
||||
t.AssertNil(err)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Concurrent_Schema_Switch tests concurrent schema switching
|
||||
func Test_Concurrent_Schema_Switch(t *testing.T) {
|
||||
table1 := createTableWithDb(db, "test_schema_1")
|
||||
table2 := createTableWithDb(db2, "test_schema_2")
|
||||
defer dropTableWithDb(db, table1)
|
||||
defer dropTableWithDb(db2, table2)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var wg sync.WaitGroup
|
||||
concurrency := 10
|
||||
|
||||
wg.Add(concurrency * 2)
|
||||
for i := 0; i < concurrency; i++ {
|
||||
// Insert to schema1
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
_, err := db.Model(table1).Insert(g.Map{
|
||||
"passport": fmt.Sprintf("user_s1_%d", id),
|
||||
"password": fmt.Sprintf("pass_%d", id),
|
||||
"nickname": fmt.Sprintf("name_%d", id),
|
||||
})
|
||||
t.AssertNil(err)
|
||||
}(i)
|
||||
|
||||
// Insert to schema2
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
_, err := db2.Model(table2).Insert(g.Map{
|
||||
"passport": fmt.Sprintf("user_s2_%d", id),
|
||||
"password": fmt.Sprintf("pass_%d", id),
|
||||
"nickname": fmt.Sprintf("name_%d", id),
|
||||
})
|
||||
t.AssertNil(err)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Verify both schemas
|
||||
count1, err := db.Model(table1).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count1, concurrency)
|
||||
|
||||
count2, err := db2.Model(table2).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count2, concurrency)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Concurrent_Model_Clone tests concurrent model cloning
|
||||
func Test_Concurrent_Model_Clone(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
baseModel := db.Model(table).Where("id>", 0)
|
||||
var wg sync.WaitGroup
|
||||
concurrency := 20
|
||||
|
||||
wg.Add(concurrency)
|
||||
for i := 0; i < concurrency; i++ {
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
// Clone model for each goroutine
|
||||
m := baseModel.Clone()
|
||||
result, err := m.Where("id<=", TableSize/2).All()
|
||||
t.AssertNil(err)
|
||||
t.AssertGT(len(result), 0)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Concurrent_Batch_Insert tests concurrent batch insert operations
|
||||
func Test_Concurrent_Batch_Insert(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var wg sync.WaitGroup
|
||||
concurrency := 5
|
||||
batchSize := 10
|
||||
|
||||
wg.Add(concurrency)
|
||||
for i := 0; i < concurrency; i++ {
|
||||
go func(batchId int) {
|
||||
defer wg.Done()
|
||||
batch := make([]g.Map, 0, batchSize)
|
||||
for j := 0; j < batchSize; j++ {
|
||||
id := batchId*batchSize + j
|
||||
batch = append(batch, g.Map{
|
||||
"passport": fmt.Sprintf("batch_user_%d", id),
|
||||
"password": fmt.Sprintf("pass_%d", id),
|
||||
"nickname": fmt.Sprintf("name_%d", id),
|
||||
})
|
||||
}
|
||||
_, err := db.Model(table).Data(batch).Insert()
|
||||
t.AssertNil(err)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Verify all batch inserts
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, concurrency*batchSize)
|
||||
})
|
||||
}
|
||||
163
contrib/drivers/mariadb/mariadb_z_unit_feature_ctx_test.go
Normal file
163
contrib/drivers/mariadb/mariadb_z_unit_feature_ctx_test.go
Normal file
@ -0,0 +1,163 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package mariadb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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()
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Ctx_Timeout tests context timeout behavior
|
||||
func Test_Ctx_Timeout(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Create a context with very short timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
|
||||
defer cancel()
|
||||
|
||||
// Wait for timeout
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
|
||||
// Query should fail due to context timeout
|
||||
_, err := db.Model(table).Ctx(ctx).All()
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Ctx_Cancel tests context cancellation
|
||||
func Test_Ctx_Cancel(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
// Cancel immediately
|
||||
cancel()
|
||||
|
||||
// Query should fail due to cancelled context
|
||||
_, err := db.Model(table).Ctx(ctx).All()
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Ctx_Propagation_Transaction tests context propagation in transaction
|
||||
func Test_Ctx_Propagation_Transaction(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
db.GetLogger().(*glog.Logger).SetCtxKeys("TraceId")
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
db.SetDebug(true)
|
||||
defer db.SetDebug(false)
|
||||
|
||||
ctx := context.WithValue(context.Background(), "TraceId", "tx_trace_123")
|
||||
err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
// Context should propagate to transaction operations
|
||||
_, err := tx.Model(table).Ctx(ctx).Where("id", 1).One()
|
||||
return err
|
||||
})
|
||||
t.AssertNil(err)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Ctx_Multiple_Values tests context with multiple values
|
||||
func Test_Ctx_Multiple_Values(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
db.GetLogger().(*glog.Logger).SetCtxKeys("TraceId", "RequestId", "UserId")
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
db.SetDebug(true)
|
||||
defer db.SetDebug(false)
|
||||
|
||||
ctx := context.WithValue(context.Background(), "TraceId", "trace_001")
|
||||
ctx = context.WithValue(ctx, "RequestId", "req_002")
|
||||
ctx = context.WithValue(ctx, "UserId", "user_003")
|
||||
|
||||
db.Model(table).Ctx(ctx).Where("id", 1).One()
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Ctx_Nested_Operations tests context in nested operations
|
||||
func Test_Ctx_Nested_Operations(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
db.GetLogger().(*glog.Logger).SetCtxKeys("TraceId")
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
db.SetDebug(true)
|
||||
defer db.SetDebug(false)
|
||||
|
||||
ctx := context.WithValue(context.Background(), "TraceId", "nested_trace")
|
||||
|
||||
// Nested query operations should all have context
|
||||
result, err := db.Model(table).Ctx(ctx).Where("id>", 0).All()
|
||||
t.AssertNil(err)
|
||||
|
||||
if len(result) > 0 {
|
||||
// Another query using same context
|
||||
_, err = db.Model(table).Ctx(ctx).Where("id", result[0]["id"]).One()
|
||||
t.AssertNil(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
321
contrib/drivers/mariadb/mariadb_z_unit_feature_duplicate_test.go
Normal file
321
contrib/drivers/mariadb/mariadb_z_unit_feature_duplicate_test.go
Normal file
@ -0,0 +1,321 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package mariadb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
)
|
||||
|
||||
func createDuplicateTable(table ...string) string {
|
||||
var name string
|
||||
if len(table) > 0 {
|
||||
name = table[0]
|
||||
} else {
|
||||
name = fmt.Sprintf(`duplicate_table_%d`, gtime.TimestampNano())
|
||||
}
|
||||
dropTable(name)
|
||||
if _, err := db.Exec(ctx, fmt.Sprintf(`
|
||||
CREATE TABLE %s (
|
||||
id int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
email varchar(100) NOT NULL,
|
||||
username varchar(45) NULL,
|
||||
score int(10) unsigned DEFAULT 0,
|
||||
login_count int(10) unsigned DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_email (email)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`, name)); err != nil {
|
||||
gtest.Fatal(err)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func Test_OnDuplicateKeyUpdate_Basic(t *testing.T) {
|
||||
table := createDuplicateTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// First insert
|
||||
_, err := db.Exec(ctx, fmt.Sprintf(
|
||||
"INSERT INTO %s (email, username, score) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE username = VALUES(username), score = VALUES(score)",
|
||||
table,
|
||||
), "user1@example.com", "user1", 100)
|
||||
t.AssertNil(err)
|
||||
|
||||
one, err := db.Model(table).Where("email", "user1@example.com").One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["username"], "user1")
|
||||
t.Assert(one["score"], 100)
|
||||
|
||||
// Duplicate insert - should update
|
||||
_, err = db.Exec(ctx, fmt.Sprintf(
|
||||
"INSERT INTO %s (email, username, score) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE username = VALUES(username), score = VALUES(score)",
|
||||
table,
|
||||
), "user1@example.com", "user1_updated", 200)
|
||||
t.AssertNil(err)
|
||||
|
||||
one, err = db.Model(table).Where("email", "user1@example.com").One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["username"], "user1_updated")
|
||||
t.Assert(one["score"], 200)
|
||||
|
||||
// Verify only one record exists
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_OnDuplicateKeyUpdate_Increment(t *testing.T) {
|
||||
table := createDuplicateTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// First insert
|
||||
_, err := db.Exec(ctx, fmt.Sprintf(
|
||||
"INSERT INTO %s (email, username, login_count) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE login_count = login_count + 1",
|
||||
table,
|
||||
), "user1@example.com", "user1", 1)
|
||||
t.AssertNil(err)
|
||||
|
||||
one, err := db.Model(table).Where("email", "user1@example.com").One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["login_count"], 1)
|
||||
|
||||
// Duplicate - increment login_count
|
||||
_, err = db.Exec(ctx, fmt.Sprintf(
|
||||
"INSERT INTO %s (email, username, login_count) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE login_count = login_count + 1",
|
||||
table,
|
||||
), "user1@example.com", "user1", 1)
|
||||
t.AssertNil(err)
|
||||
|
||||
one, err = db.Model(table).Where("email", "user1@example.com").One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["login_count"], 2)
|
||||
|
||||
// Third time
|
||||
_, err = db.Exec(ctx, fmt.Sprintf(
|
||||
"INSERT INTO %s (email, username, login_count) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE login_count = login_count + 1",
|
||||
table,
|
||||
), "user1@example.com", "user1", 1)
|
||||
t.AssertNil(err)
|
||||
|
||||
one, err = db.Model(table).Where("email", "user1@example.com").One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["login_count"], 3)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_OnDuplicateKeyUpdate_MultipleColumns(t *testing.T) {
|
||||
table := createDuplicateTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// First insert
|
||||
_, err := db.Exec(ctx, fmt.Sprintf(
|
||||
"INSERT INTO %s (email, username, score, login_count) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE username = VALUES(username), score = VALUES(score), login_count = login_count + 1",
|
||||
table,
|
||||
), "user1@example.com", "user1", 100, 1)
|
||||
t.AssertNil(err)
|
||||
|
||||
one, err := db.Model(table).Where("email", "user1@example.com").One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["username"], "user1")
|
||||
t.Assert(one["score"], 100)
|
||||
t.Assert(one["login_count"], 1)
|
||||
|
||||
// Duplicate - update multiple columns
|
||||
_, err = db.Exec(ctx, fmt.Sprintf(
|
||||
"INSERT INTO %s (email, username, score, login_count) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE username = VALUES(username), score = VALUES(score), login_count = login_count + 1",
|
||||
table,
|
||||
), "user1@example.com", "user1_v2", 200, 1)
|
||||
t.AssertNil(err)
|
||||
|
||||
one, err = db.Model(table).Where("email", "user1@example.com").One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["username"], "user1_v2")
|
||||
t.Assert(one["score"], 200)
|
||||
t.Assert(one["login_count"], 2)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_OnDuplicateKeyUpdate_Batch(t *testing.T) {
|
||||
table := createDuplicateTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert multiple records
|
||||
_, err := db.Exec(ctx, fmt.Sprintf(
|
||||
"INSERT INTO %s (email, username, score) VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?) ON DUPLICATE KEY UPDATE username = VALUES(username), score = VALUES(score)",
|
||||
table,
|
||||
), "user1@example.com", "user1", 100,
|
||||
"user2@example.com", "user2", 200,
|
||||
"user3@example.com", "user3", 300)
|
||||
t.AssertNil(err)
|
||||
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 3)
|
||||
|
||||
// Update with duplicate - should update specific records
|
||||
_, err = db.Exec(ctx, fmt.Sprintf(
|
||||
"INSERT INTO %s (email, username, score) VALUES (?, ?, ?), (?, ?, ?) ON DUPLICATE KEY UPDATE username = VALUES(username), score = VALUES(score)",
|
||||
table,
|
||||
), "user1@example.com", "user1_updated", 150,
|
||||
"user2@example.com", "user2_updated", 250)
|
||||
t.AssertNil(err)
|
||||
|
||||
// Still 3 records
|
||||
count, err = db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 3)
|
||||
|
||||
// Verify updates
|
||||
one, err := db.Model(table).Where("email", "user1@example.com").One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["username"], "user1_updated")
|
||||
t.Assert(one["score"], 150)
|
||||
|
||||
one, err = db.Model(table).Where("email", "user2@example.com").One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["username"], "user2_updated")
|
||||
t.Assert(one["score"], 250)
|
||||
|
||||
// user3 unchanged
|
||||
one, err = db.Model(table).Where("email", "user3@example.com").One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["username"], "user3")
|
||||
t.Assert(one["score"], 300)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_OnDuplicateKeyUpdate_ConditionalUpdate(t *testing.T) {
|
||||
table := createDuplicateTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// First insert
|
||||
_, err := db.Exec(ctx, fmt.Sprintf(
|
||||
"INSERT INTO %s (email, username, score) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE score = IF(VALUES(score) > score, VALUES(score), score)",
|
||||
table,
|
||||
), "user1@example.com", "user1", 100)
|
||||
t.AssertNil(err)
|
||||
|
||||
one, err := db.Model(table).Where("email", "user1@example.com").One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["score"], 100)
|
||||
|
||||
// Try to update with lower score - should not update
|
||||
_, err = db.Exec(ctx, fmt.Sprintf(
|
||||
"INSERT INTO %s (email, username, score) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE score = IF(VALUES(score) > score, VALUES(score), score)",
|
||||
table,
|
||||
), "user1@example.com", "user1", 50)
|
||||
t.AssertNil(err)
|
||||
|
||||
one, err = db.Model(table).Where("email", "user1@example.com").One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["score"], 100) // Still 100
|
||||
|
||||
// Update with higher score - should update
|
||||
_, err = db.Exec(ctx, fmt.Sprintf(
|
||||
"INSERT INTO %s (email, username, score) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE score = IF(VALUES(score) > score, VALUES(score), score)",
|
||||
table,
|
||||
), "user1@example.com", "user1", 150)
|
||||
t.AssertNil(err)
|
||||
|
||||
one, err = db.Model(table).Where("email", "user1@example.com").One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["score"], 150) // Updated to 150
|
||||
})
|
||||
}
|
||||
|
||||
func Test_OnDuplicateKeyUpdate_WithTransaction(t *testing.T) {
|
||||
table := createDuplicateTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Transaction with ON DUPLICATE KEY UPDATE
|
||||
err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
// First insert
|
||||
_, err := tx.Exec(fmt.Sprintf(
|
||||
"INSERT INTO %s (email, username, score) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE username = VALUES(username), score = VALUES(score)",
|
||||
table,
|
||||
), "user1@example.com", "user1", 100)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Duplicate in same transaction
|
||||
_, err = tx.Exec(fmt.Sprintf(
|
||||
"INSERT INTO %s (email, username, score) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE username = VALUES(username), score = VALUES(score)",
|
||||
table,
|
||||
), "user1@example.com", "user1_updated", 200)
|
||||
return err
|
||||
})
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify final state
|
||||
one, err := db.Model(table).Where("email", "user1@example.com").One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["username"], "user1_updated")
|
||||
t.Assert(one["score"], 200)
|
||||
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_OnDuplicateKeyUpdate_MixedInsertUpdate(t *testing.T) {
|
||||
table := createDuplicateTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// First batch insert
|
||||
_, err := db.Exec(ctx, fmt.Sprintf(
|
||||
"INSERT INTO %s (email, username, score) VALUES (?, ?, ?), (?, ?, ?) ON DUPLICATE KEY UPDATE username = VALUES(username), score = VALUES(score)",
|
||||
table,
|
||||
), "user1@example.com", "user1", 100,
|
||||
"user2@example.com", "user2", 200)
|
||||
t.AssertNil(err)
|
||||
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 2)
|
||||
|
||||
// Mixed batch: one duplicate, one new
|
||||
_, err = db.Exec(ctx, fmt.Sprintf(
|
||||
"INSERT INTO %s (email, username, score) VALUES (?, ?, ?), (?, ?, ?) ON DUPLICATE KEY UPDATE username = VALUES(username), score = VALUES(score)",
|
||||
table,
|
||||
), "user1@example.com", "user1_updated", 150,
|
||||
"user3@example.com", "user3", 300)
|
||||
t.AssertNil(err)
|
||||
|
||||
// Should have 3 records now
|
||||
count, err = db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 3)
|
||||
|
||||
// Verify user1 was updated
|
||||
one, err := db.Model(table).Where("email", "user1@example.com").One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["username"], "user1_updated")
|
||||
t.Assert(one["score"], 150)
|
||||
|
||||
// Verify user3 was inserted
|
||||
one, err = db.Model(table).Where("email", "user3@example.com").One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["username"], "user3")
|
||||
t.Assert(one["score"], 300)
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,489 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package mariadb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// Test_Model_Insert_NilData tests Insert with nil data
|
||||
func Test_Model_Insert_NilData(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
_, err := db.Model(table).Data(nil).Insert()
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Insert_EmptyMap tests Insert with empty map
|
||||
func Test_Model_Insert_EmptyMap(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
_, err := db.Model(table).Data(g.Map{}).Insert()
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Insert_EmptySlice tests Insert with empty slice
|
||||
func Test_Model_Insert_EmptySlice(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
_, err := db.Model(table).Data(g.Slice{}).Insert()
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Update_NilData tests Update with nil data
|
||||
func Test_Model_Update_NilData(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
_, err := db.Model(table).Data(nil).Where("id", 1).Update()
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Update_EmptyData tests Update with empty data
|
||||
func Test_Model_Update_EmptyData(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
_, err := db.Model(table).Data(g.Map{}).Where("id", 1).Update()
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Update_NoWhere tests Update without WHERE clause is rejected by framework
|
||||
func Test_Model_Update_NoWhere(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Framework safety check: Update without WHERE should return error
|
||||
_, err := db.Model(table).Data(g.Map{"nickname": "updated"}).Update()
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Delete_NoWhere tests Delete without WHERE clause is rejected by framework
|
||||
func Test_Model_Delete_NoWhere(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Framework safety check: Delete without WHERE should return error
|
||||
_, err := db.Model(table).Delete()
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Scan_NilPointer tests Scan with nil pointer
|
||||
func Test_Model_Scan_NilPointer(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
err := db.Model(table).Where("id", 1).Scan(nil)
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Scan_InvalidPointer tests Scan with invalid pointer type
|
||||
func Test_Model_Scan_InvalidPointer(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var str string
|
||||
err := db.Model(table).Where("id", 1).Scan(&str)
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Scan_EmptyResult tests Scan with empty result
|
||||
func Test_Model_Scan_EmptyResult(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
type User struct {
|
||||
Id int
|
||||
}
|
||||
|
||||
// Scan initialized struct with empty result returns sql.ErrNoRows
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var user User
|
||||
err := db.Model(table).Where("id > ?", 1000).Scan(&user)
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
|
||||
// Scan nil pointer with empty result returns nil error
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var user *User
|
||||
err := db.Model(table).Where("id > ?", 1000).Scan(&user)
|
||||
t.AssertNil(err)
|
||||
t.Assert(user, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Where_InvalidOperator tests Where with invalid operator
|
||||
func Test_Model_Where_InvalidOperator(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Invalid SQL should cause error at query time
|
||||
_, err := db.Model(table).Where("id INVALID_OP ?", 1).All()
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Where_EmptyString tests Where with empty string
|
||||
func Test_Model_Where_EmptyString(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, err := db.Model(table).Where("").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), TableSize) // Empty WHERE returns all
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Fields_InvalidField tests Fields with non-existent field
|
||||
func Test_Model_Fields_InvalidField(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
_, err := db.Model(table).Fields("non_existent_field").All()
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Fields_Empty tests Fields with empty string
|
||||
// Regression test for #4697: Fields("") should handle empty string gracefully
|
||||
// https://github.com/gogf/gf/issues/4697
|
||||
func Test_Model_Fields_Empty(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, err := db.Model(table).Fields("").Limit(1).All()
|
||||
t.AssertNil(err)
|
||||
t.AssertLE(len(result), 1)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Order_InvalidSyntax tests Order with invalid syntax
|
||||
func Test_Model_Order_InvalidSyntax(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Invalid ORDER BY syntax
|
||||
_, err := db.Model(table).Order("id INVALID").All()
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Group_UnknownColumn tests Group with non-existent column
|
||||
func Test_Model_Group_UnknownColumn(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
_, err := db.Model(table).Group("non_existent_field").All()
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_TableNotExist tests querying non-existent table
|
||||
func Test_Model_TableNotExist(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
_, err := db.Model("non_existent_table_xyz").All()
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_InvalidTableName tests invalid table name
|
||||
func Test_Model_InvalidTableName(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Empty table name
|
||||
_, err := db.Model("").All()
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_SQLInjection_Where tests SQL injection prevention in Where
|
||||
func Test_Model_SQLInjection_Where(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Attempt SQL injection through string column parameter.
|
||||
// Using string column `nickname` instead of int column `id`,
|
||||
// because MySQL coerces "1 OR 1=1" to 1 for int columns.
|
||||
maliciousInput := "1 OR 1=1"
|
||||
result, err := db.Model(table).Where("nickname = ?", maliciousInput).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), 0) // Should not return all records
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Attempt SQL injection with quotes, using string column to avoid
|
||||
// MySQL implicit int conversion (which would coerce "1'..." to 1)
|
||||
maliciousInput := "1'; DROP TABLE " + table + "; --"
|
||||
result, err := db.Model(table).Where("nickname = ?", maliciousInput).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), 0)
|
||||
// Table should still exist
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, TableSize)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_SQLInjection_Insert tests SQL injection prevention in Insert
|
||||
func Test_Model_SQLInjection_Insert(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
maliciousData := g.Map{
|
||||
"id": 1,
|
||||
"passport": "'; DROP TABLE " + table + "; --",
|
||||
"password": "pwd",
|
||||
"nickname": "test",
|
||||
}
|
||||
_, err := db.Model(table).Data(maliciousData).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify data was inserted correctly and table still exists
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.AssertNE(one, nil)
|
||||
t.Assert(one["passport"].String(), "'; DROP TABLE "+table+"; --")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_SQLInjection_Update tests SQL injection prevention in Update
|
||||
func Test_Model_SQLInjection_Update(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Use shorter malicious string to fit in nickname column
|
||||
maliciousData := g.Map{
|
||||
"nickname": "'; DELETE FROM users; --",
|
||||
}
|
||||
_, err := db.Model(table).Data(maliciousData).Where("id", 1).Update()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify only one record was updated (parameterized query prevents injection)
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["nickname"].String(), "'; DELETE FROM users; --")
|
||||
|
||||
// Other records should still exist (injection was prevented)
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, TableSize)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Context_Cancelled tests query with cancelled context
|
||||
func Test_Model_Context_Cancelled(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // Cancel immediately
|
||||
|
||||
_, err := db.Model(table).Ctx(ctx).All()
|
||||
t.AssertNE(err, nil)
|
||||
t.Assert(gerror.Is(err, context.Canceled), true)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Value_EmptyResult tests Value with empty result
|
||||
func Test_Model_Value_EmptyResult(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
value, err := db.Model(table).Where("id > ?", 1000).Value()
|
||||
t.AssertNil(err)
|
||||
t.Assert(value.IsEmpty(), true)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Array_EmptyResult tests Array with empty result
|
||||
func Test_Model_Array_EmptyResult(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
array, err := db.Model(table).Where("id > ?", 1000).Array()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(array), 0)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Count_InvalidTable tests Count on invalid table
|
||||
func Test_Model_Count_InvalidTable(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
_, err := db.Model("non_existent_table").Count()
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Max_EmptyResult tests Max with empty result
|
||||
func Test_Model_Max_EmptyResult(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
value, err := db.Model(table).Where("id > ?", 1000).Max("id")
|
||||
t.AssertNil(err)
|
||||
t.Assert(value, 0) // Returns 0 for empty result
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Min_EmptyResult tests Min with empty result
|
||||
func Test_Model_Min_EmptyResult(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
value, err := db.Model(table).Where("id > ?", 1000).Min("id")
|
||||
t.AssertNil(err)
|
||||
t.Assert(value, 0) // Returns 0 for empty result
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Avg_EmptyResult tests Avg with empty result
|
||||
func Test_Model_Avg_EmptyResult(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
value, err := db.Model(table).Where("id > ?", 1000).Avg("id")
|
||||
t.AssertNil(err)
|
||||
t.Assert(value, 0) // Returns 0 for empty result
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Sum_EmptyResult tests Sum with empty result
|
||||
func Test_Model_Sum_EmptyResult(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
value, err := db.Model(table).Where("id > ?", 1000).Sum("id")
|
||||
t.AssertNil(err)
|
||||
t.Assert(value, 0) // Returns 0 for empty result
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_One_NilResult tests One returning nil
|
||||
func Test_Model_One_NilResult(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
one, err := db.Model(table).Where("id > ?", 1000).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_TX_Rollback_AfterError tests transaction rollback after error
|
||||
func Test_TX_Rollback_AfterError(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
// Insert valid record
|
||||
_, err := tx.Model(table).Data(g.Map{
|
||||
"id": 1,
|
||||
"passport": "pass1",
|
||||
"password": "pwd1",
|
||||
"nickname": "name1",
|
||||
}).Insert()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Insert duplicate id (should fail)
|
||||
_, err = tx.Model(table).Data(g.Map{
|
||||
"id": 1, // Duplicate
|
||||
"passport": "pass2",
|
||||
"password": "pwd2",
|
||||
"nickname": "name2",
|
||||
}).Insert()
|
||||
|
||||
return err // Return error to trigger rollback
|
||||
})
|
||||
|
||||
t.AssertNE(err, nil)
|
||||
|
||||
// Verify rollback - table should be empty
|
||||
count, err := db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 0)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Insert_DuplicateKey tests handling of duplicate key error
|
||||
func Test_Model_Insert_DuplicateKey(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
"id": 1,
|
||||
"passport": "pass",
|
||||
"password": "pwd",
|
||||
"nickname": "name",
|
||||
}
|
||||
|
||||
// First insert should succeed
|
||||
_, err := db.Model(table).Data(data).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Second insert with same id should fail
|
||||
_, err = db.Model(table).Data(data).Insert()
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_All_InvalidConnection tests query with invalid connection
|
||||
func Test_Model_All_InvalidConnection(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
if dbInvalid == nil {
|
||||
t.Skip("dbInvalid not configured")
|
||||
}
|
||||
_, err := dbInvalid.Model("test_table").All()
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
229
contrib/drivers/mariadb/mariadb_z_unit_feature_hook_test.go
Normal file
229
contrib/drivers/mariadb/mariadb_z_unit_feature_hook_test.go
Normal file
@ -0,0 +1,229 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package mariadb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/container/gvar"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"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"])
|
||||
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).Delete()
|
||||
t.AssertNil(err)
|
||||
|
||||
all, err := m.All()
|
||||
t.AssertNil(err)
|
||||
for _, item := range all {
|
||||
t.Assert(item["nickname"].String(), `deleted`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Hook_Multiple tests multiple hooks execution order
|
||||
func Test_Model_Hook_Multiple(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var execOrder []string
|
||||
|
||||
m := db.Model(table).Hook(gdb.HookHandler{
|
||||
Select: func(ctx context.Context, in *gdb.HookSelectInput) (result gdb.Result, err error) {
|
||||
execOrder = append(execOrder, "hook1_before")
|
||||
result, err = in.Next(ctx)
|
||||
execOrder = append(execOrder, "hook1_after")
|
||||
return
|
||||
},
|
||||
}).Hook(gdb.HookHandler{
|
||||
Select: func(ctx context.Context, in *gdb.HookSelectInput) (result gdb.Result, err error) {
|
||||
execOrder = append(execOrder, "hook2_before")
|
||||
result, err = in.Next(ctx)
|
||||
execOrder = append(execOrder, "hook2_after")
|
||||
return
|
||||
},
|
||||
})
|
||||
|
||||
_, err := m.Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify only the last registered hook executes (Hook is override, not chain)
|
||||
t.Assert(len(execOrder), 2)
|
||||
t.Assert(execOrder, g.Slice{"hook2_before", "hook2_after"})
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Hook_Error_Abort tests hook returning error aborts operation
|
||||
func Test_Model_Hook_Error_Abort(t *testing.T) {
|
||||
table := createInitTable()
|
||||
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) {
|
||||
// Return error to abort insert
|
||||
return nil, fmt.Errorf("hook aborted insert")
|
||||
},
|
||||
})
|
||||
|
||||
_, err := m.Insert(g.Map{
|
||||
"passport": "test_abort",
|
||||
"password": "pass",
|
||||
"nickname": "name",
|
||||
})
|
||||
t.AssertNE(err, nil)
|
||||
t.Assert(err.Error(), "hook aborted insert")
|
||||
|
||||
// Verify record was not inserted
|
||||
count, err := db.Model(table).Where("passport", "test_abort").Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 0)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Hook_Modify_Data tests hook modifying data before insert
|
||||
func Test_Model_Hook_Modify_Data(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) {
|
||||
// Modify all data items
|
||||
for i := range in.Data {
|
||||
in.Data[i]["password"] = "encrypted_" + fmt.Sprint(in.Data[i]["password"])
|
||||
in.Data[i]["nickname"] = "verified_" + fmt.Sprint(in.Data[i]["nickname"])
|
||||
}
|
||||
return in.Next(ctx)
|
||||
},
|
||||
})
|
||||
|
||||
_, err := m.Insert(g.Map{
|
||||
"passport": "test_user",
|
||||
"password": "plain123",
|
||||
"nickname": "john",
|
||||
})
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify data was modified by hook
|
||||
one, err := db.Model(table).Where("passport", "test_user").One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["password"].String(), "encrypted_plain123")
|
||||
t.Assert(one["nickname"].String(), "verified_john")
|
||||
})
|
||||
}
|
||||
394
contrib/drivers/mariadb/mariadb_z_unit_feature_json_test.go
Normal file
394
contrib/drivers/mariadb/mariadb_z_unit_feature_json_test.go
Normal file
@ -0,0 +1,394 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package mariadb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
)
|
||||
|
||||
func createJSONTable(table ...string) string {
|
||||
var name string
|
||||
if len(table) > 0 {
|
||||
name = table[0]
|
||||
} else {
|
||||
name = fmt.Sprintf(`json_table_%d`, gtime.TimestampNano())
|
||||
}
|
||||
dropTable(name)
|
||||
if _, err := db.Exec(ctx, fmt.Sprintf(`
|
||||
CREATE TABLE %s (
|
||||
id int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
name varchar(45) NULL,
|
||||
config json NULL,
|
||||
metadata json NULL,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`, name)); err != nil {
|
||||
gtest.Fatal(err)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func Test_JSON_Insert_Map(t *testing.T) {
|
||||
table := createJSONTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
"name": "user1",
|
||||
"config": g.Map{
|
||||
"theme": "dark",
|
||||
"lang": "zh-CN",
|
||||
},
|
||||
"metadata": g.Map{
|
||||
"tags": g.Slice{"admin", "developer"},
|
||||
"level": 5,
|
||||
},
|
||||
}
|
||||
result, err := db.Model(table).Data(data).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.LastInsertId()
|
||||
t.Assert(n, 1)
|
||||
|
||||
one, err := db.Model(table).WherePri(1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["name"], "user1")
|
||||
t.AssertNE(one["config"], nil)
|
||||
t.AssertNE(one["metadata"], nil)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_JSON_Insert_String(t *testing.T) {
|
||||
table := createJSONTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
"name": "user2",
|
||||
"config": `{"theme":"light","lang":"en-US"}`,
|
||||
"metadata": `{"tags":["user"],"level":1}`,
|
||||
}
|
||||
result, err := db.Model(table).Data(data).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.LastInsertId()
|
||||
t.Assert(n, 1)
|
||||
|
||||
one, err := db.Model(table).WherePri(1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["name"], "user2")
|
||||
t.AssertNE(one["config"], nil)
|
||||
t.AssertNE(one["metadata"], nil)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_JSON_Insert_Null(t *testing.T) {
|
||||
table := createJSONTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Map{
|
||||
"name": "user3",
|
||||
"config": nil,
|
||||
"metadata": nil,
|
||||
}
|
||||
result, err := db.Model(table).Data(data).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.LastInsertId()
|
||||
t.Assert(n, 1)
|
||||
|
||||
one, err := db.Model(table).WherePri(1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["name"], "user3")
|
||||
t.Assert(one["config"], nil)
|
||||
t.Assert(one["metadata"], nil)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_JSON_Update(t *testing.T) {
|
||||
table := createJSONTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert initial data
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"name": "user1",
|
||||
"config": g.Map{
|
||||
"theme": "dark",
|
||||
},
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Update JSON column
|
||||
result, err := db.Model(table).Data(g.Map{
|
||||
"config": g.Map{
|
||||
"theme": "light",
|
||||
"lang": "en-US",
|
||||
},
|
||||
}).WherePri(1).Update()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
one, err := db.Model(table).WherePri(1).One()
|
||||
t.AssertNil(err)
|
||||
t.AssertNE(one["config"], nil)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_JSON_Extract_Where(t *testing.T) {
|
||||
table := createJSONTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert test data
|
||||
data := g.Slice{
|
||||
g.Map{
|
||||
"name": "user1",
|
||||
"config": g.Map{
|
||||
"theme": "dark",
|
||||
"lang": "zh-CN",
|
||||
},
|
||||
},
|
||||
g.Map{
|
||||
"name": "user2",
|
||||
"config": g.Map{
|
||||
"theme": "light",
|
||||
"lang": "en-US",
|
||||
},
|
||||
},
|
||||
g.Map{
|
||||
"name": "user3",
|
||||
"config": g.Map{
|
||||
"theme": "dark",
|
||||
"lang": "en-US",
|
||||
},
|
||||
},
|
||||
}
|
||||
_, err := db.Model(table).Data(data).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Query by JSON field using JSON_EXTRACT
|
||||
all, err := db.Model(table).Where("JSON_EXTRACT(config, '$.theme') = ?", "dark").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 2)
|
||||
|
||||
all, err = db.Model(table).Where("JSON_EXTRACT(config, '$.lang') = ?", "en-US").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 2)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_JSON_Extract_Select(t *testing.T) {
|
||||
table := createJSONTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert test data
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"name": "user1",
|
||||
"config": g.Map{
|
||||
"theme": "dark",
|
||||
"lang": "zh-CN",
|
||||
},
|
||||
"metadata": g.Map{
|
||||
"level": 5,
|
||||
},
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Select with JSON_EXTRACT
|
||||
one, err := db.Model(table).Fields("name, JSON_EXTRACT(config, '$.theme') as theme, JSON_EXTRACT(metadata, '$.level') as level").WherePri(1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["name"], "user1")
|
||||
t.AssertNE(one["theme"], nil)
|
||||
t.AssertNE(one["level"], nil)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_JSON_Array_Query(t *testing.T) {
|
||||
table := createJSONTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert data with JSON array
|
||||
data := g.Slice{
|
||||
g.Map{
|
||||
"name": "user1",
|
||||
"metadata": g.Map{
|
||||
"tags": g.Slice{"admin", "developer"},
|
||||
},
|
||||
},
|
||||
g.Map{
|
||||
"name": "user2",
|
||||
"metadata": g.Map{
|
||||
"tags": g.Slice{"user"},
|
||||
},
|
||||
},
|
||||
g.Map{
|
||||
"name": "user3",
|
||||
"metadata": g.Map{
|
||||
"tags": g.Slice{"admin", "user"},
|
||||
},
|
||||
},
|
||||
}
|
||||
_, err := db.Model(table).Data(data).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Query by JSON array contains
|
||||
all, err := db.Model(table).Where("JSON_CONTAINS(metadata, ?, '$.tags')", `"admin"`).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 2)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_JSON_Batch_Insert(t *testing.T) {
|
||||
table := createJSONTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
data := g.Slice{
|
||||
g.Map{
|
||||
"name": "user1",
|
||||
"config": g.Map{
|
||||
"theme": "dark",
|
||||
},
|
||||
},
|
||||
g.Map{
|
||||
"name": "user2",
|
||||
"config": g.Map{
|
||||
"theme": "light",
|
||||
},
|
||||
},
|
||||
g.Map{
|
||||
"name": "user3",
|
||||
"config": nil,
|
||||
},
|
||||
}
|
||||
result, err := db.Model(table).Data(data).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 3)
|
||||
|
||||
all, err := db.Model(table).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 3)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_JSON_Scan_To_Struct(t *testing.T) {
|
||||
table := createJSONTable()
|
||||
defer dropTable(table)
|
||||
|
||||
type Config struct {
|
||||
Theme string `json:"theme"`
|
||||
Lang string `json:"lang"`
|
||||
}
|
||||
type User struct {
|
||||
Id int
|
||||
Name string
|
||||
Config *Config
|
||||
}
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert data
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"name": "user1",
|
||||
"config": g.Map{
|
||||
"theme": "dark",
|
||||
"lang": "zh-CN",
|
||||
},
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Scan to struct
|
||||
var user User
|
||||
err = db.Model(table).WherePri(1).Scan(&user)
|
||||
t.AssertNil(err)
|
||||
t.Assert(user.Name, "user1")
|
||||
t.AssertNE(user.Config, nil)
|
||||
if user.Config != nil {
|
||||
t.Assert(user.Config.Theme, "dark")
|
||||
t.Assert(user.Config.Lang, "zh-CN")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_JSON_Complex_Structure(t *testing.T) {
|
||||
table := createJSONTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert complex nested JSON
|
||||
data := g.Map{
|
||||
"name": "user1",
|
||||
"config": g.Map{
|
||||
"ui": g.Map{
|
||||
"theme": "dark",
|
||||
"fontSize": g.Map{
|
||||
"base": 14,
|
||||
"code": 12,
|
||||
},
|
||||
},
|
||||
"editor": g.Map{
|
||||
"tabSize": 4,
|
||||
"wordWrap": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
result, err := db.Model(table).Data(data).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.LastInsertId()
|
||||
t.Assert(n, 1)
|
||||
|
||||
// Query nested JSON path
|
||||
one, err := db.Model(table).Fields("JSON_EXTRACT(config, '$.ui.theme') as theme, JSON_EXTRACT(config, '$.ui.fontSize.base') as base_font").WherePri(1).One()
|
||||
t.AssertNil(err)
|
||||
t.AssertNE(one["theme"], nil)
|
||||
t.AssertNE(one["base_font"], nil)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_JSON_Transaction(t *testing.T) {
|
||||
table := createJSONTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
// Insert in transaction
|
||||
_, err := tx.Model(table).Ctx(ctx).Data(g.Map{
|
||||
"name": "user1",
|
||||
"config": g.Map{
|
||||
"theme": "dark",
|
||||
},
|
||||
}).Insert()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update in transaction
|
||||
_, err = tx.Model(table).Ctx(ctx).Data(g.Map{
|
||||
"config": g.Map{
|
||||
"theme": "light",
|
||||
},
|
||||
}).WherePri(1).Update()
|
||||
return err
|
||||
})
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify data
|
||||
one, err := db.Model(table).WherePri(1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["name"], "user1")
|
||||
t.AssertNE(one["config"], nil)
|
||||
})
|
||||
}
|
||||
228
contrib/drivers/mariadb/mariadb_z_unit_feature_lock_test.go
Normal file
228
contrib/drivers/mariadb/mariadb_z_unit_feature_lock_test.go
Normal file
@ -0,0 +1,228 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package mariadb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
)
|
||||
|
||||
// Test_Model_Lock tests the Lock method with custom lock clause
|
||||
func Test_Model_Lock(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test basic Lock with FOR UPDATE
|
||||
one, err := db.Model(table).Lock("FOR UPDATE").Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["id"], 1)
|
||||
|
||||
// Test Lock with legacy LOCK IN SHARE MODE (MariaDB/MySQL compatible)
|
||||
one, err = db.Model(table).Lock("LOCK IN SHARE MODE").Where("id", 3).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["id"], 3)
|
||||
|
||||
// Test Lock with predefined constants
|
||||
one, err = db.Model(table).Lock(gdb.LockForUpdate).Where("id", 4).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["id"], 4)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_LockUpdate tests the LockUpdate convenience method
|
||||
func Test_Model_LockUpdate(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test LockUpdate is equivalent to Lock("FOR UPDATE")
|
||||
one, err := db.Model(table).LockUpdate().Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["id"], 1)
|
||||
t.Assert(one["passport"], "user_1")
|
||||
|
||||
// Test LockUpdate with All()
|
||||
all, err := db.Model(table).LockUpdate().Where("id<?", 4).Order("id").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 3)
|
||||
t.Assert(all[0]["id"], 1)
|
||||
t.Assert(all[2]["id"], 3)
|
||||
|
||||
// Test LockUpdate with Count()
|
||||
count, err := db.Model(table).LockUpdate().Where("id>?", 5).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 5)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_LockUpdateSkipLocked tests the LockUpdateSkipLocked convenience method
|
||||
// Note: SKIP LOCKED requires MariaDB 10.6+, skipped for compatibility
|
||||
// func Test_Model_LockUpdateSkipLocked(t *testing.T) {
|
||||
// table := createInitTable()
|
||||
// defer dropTable(table)
|
||||
//
|
||||
// gtest.C(t, func(t *gtest.T) {
|
||||
// // Test LockUpdateSkipLocked basic usage
|
||||
// one, err := db.Model(table).LockUpdateSkipLocked().Where("id", 1).One()
|
||||
// t.AssertNil(err)
|
||||
// t.Assert(one["id"], 1)
|
||||
//
|
||||
// // Test LockUpdateSkipLocked with All()
|
||||
// all, err := db.Model(table).LockUpdateSkipLocked().Where("id>?", 7).Order("id").All()
|
||||
// t.AssertNil(err)
|
||||
// t.Assert(len(all), 3)
|
||||
// })
|
||||
// }
|
||||
|
||||
// Test_Model_LockShared tests the LockShared convenience method
|
||||
func Test_Model_LockShared(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test LockShared is equivalent to Lock("LOCK IN SHARE MODE")
|
||||
one, err := db.Model(table).LockShared().Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["id"], 1)
|
||||
|
||||
// Test LockShared with All()
|
||||
all, err := db.Model(table).LockShared().Where("id<=?", 5).Order("id").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 5)
|
||||
t.Assert(all[0]["id"], 1)
|
||||
t.Assert(all[4]["id"], 5)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Lock_WithTransaction tests Lock methods within transaction
|
||||
func Test_Model_Lock_WithTransaction(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
// Lock row for update in transaction
|
||||
one, err := tx.Model(table).LockUpdate().Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["id"], 1)
|
||||
|
||||
// Update the locked row
|
||||
_, err = tx.Model(table).Data(g.Map{"nickname": "updated_name"}).Where("id", 1).Update()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify update
|
||||
updated, err := tx.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(updated["nickname"], "updated_name")
|
||||
|
||||
return nil
|
||||
})
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify transaction committed successfully
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["nickname"], "updated_name")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Lock_ReleaseAfterCommit tests lock is released after transaction commit
|
||||
func Test_Model_Lock_ReleaseAfterCommit(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Start transaction and lock a row
|
||||
tx, err := db.Begin(ctx)
|
||||
t.AssertNil(err)
|
||||
|
||||
one, err := tx.Model(table).LockUpdate().Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["id"], 1)
|
||||
|
||||
// Update within transaction
|
||||
_, err = tx.Model(table).Data(g.Map{"nickname": "tx_update"}).Where("id", 1).Update()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Commit transaction - this should release the lock
|
||||
err = tx.Commit()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Another query should succeed without blocking
|
||||
one, err = db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["nickname"], "tx_update")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Lock_ReleaseAfterRollback tests lock is released after transaction rollback
|
||||
func Test_Model_Lock_ReleaseAfterRollback(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Start transaction and lock a row
|
||||
tx, err := db.Begin(ctx)
|
||||
t.AssertNil(err)
|
||||
|
||||
one, err := tx.Model(table).LockUpdate().Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["id"], 1)
|
||||
|
||||
// Update within transaction
|
||||
_, err = tx.Model(table).Data(g.Map{"nickname": "rollback_update"}).Where("id", 1).Update()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Rollback transaction - this should release the lock and discard changes
|
||||
err = tx.Rollback()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify original value is preserved
|
||||
one, err = db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["nickname"], "name_1")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Lock_ChainedMethods tests Lock with other chained methods
|
||||
func Test_Model_Lock_ChainedMethods(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Lock with Fields
|
||||
one, err := db.Model(table).Fields("id,passport").LockUpdate().Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(one), 2)
|
||||
t.Assert(one["id"], 1)
|
||||
t.Assert(one["passport"], "user_1")
|
||||
|
||||
// Lock with Order and Limit
|
||||
all, err := db.Model(table).LockShared().Where("id>?", 5).Order("id desc").Limit(3).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 3)
|
||||
t.Assert(all[0]["id"], 10)
|
||||
t.Assert(all[2]["id"], 8)
|
||||
|
||||
// Lock with Group and Having
|
||||
all, err = db.Model(table).Fields("LEFT(passport,4) as prefix, COUNT(*) as cnt").
|
||||
LockUpdate().
|
||||
Group("prefix").
|
||||
Having("cnt>?", 0).
|
||||
Order("prefix").
|
||||
All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 1)
|
||||
t.Assert(all[0]["prefix"], "user")
|
||||
t.Assert(all[0]["cnt"], 10)
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,324 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package mariadb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/container/garray"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
"github.com/gogf/gf/v2/util/guid"
|
||||
)
|
||||
|
||||
func Test_Master_Slave(t *testing.T) {
|
||||
var err error
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
_, err = db.Exec(ctx, "CREATE DATABASE IF NOT EXISTS `master` CHARACTER SET UTF8")
|
||||
t.AssertNil(err)
|
||||
_, err = db.Exec(ctx, "CREATE DATABASE IF NOT EXISTS `slave` CHARACTER SET UTF8")
|
||||
t.AssertNil(err)
|
||||
})
|
||||
defer func() {
|
||||
_, _ = db.Exec(ctx, "DROP DATABASE `master`")
|
||||
_, _ = db.Exec(ctx, "DROP DATABASE `slave`")
|
||||
}()
|
||||
var (
|
||||
configKey = guid.S()
|
||||
configGroup = gdb.ConfigGroup{
|
||||
gdb.ConfigNode{
|
||||
Host: "127.0.0.1",
|
||||
Port: "3307",
|
||||
User: "root",
|
||||
Pass: "12345678",
|
||||
Name: "master",
|
||||
Type: "mariadb",
|
||||
Role: "master",
|
||||
Debug: true,
|
||||
Weight: 100,
|
||||
},
|
||||
gdb.ConfigNode{
|
||||
Host: "127.0.0.1",
|
||||
Port: "3307",
|
||||
User: "root",
|
||||
Pass: "12345678",
|
||||
Name: "slave",
|
||||
Type: "mariadb",
|
||||
Role: "slave",
|
||||
Debug: true,
|
||||
Weight: 100,
|
||||
},
|
||||
}
|
||||
)
|
||||
gdb.SetConfigGroup(configKey, configGroup)
|
||||
masterSlaveDB := g.DB(configKey)
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table := "table_" + guid.S()
|
||||
createTableWithDb(masterSlaveDB.Schema("master"), table)
|
||||
createTableWithDb(masterSlaveDB.Schema("slave"), table)
|
||||
defer dropTableWithDb(masterSlaveDB.Schema("master"), table)
|
||||
defer dropTableWithDb(masterSlaveDB.Schema("slave"), table)
|
||||
|
||||
// Data insert to master.
|
||||
array := garray.New(true)
|
||||
for i := 1; i <= TableSize; i++ {
|
||||
array.Append(g.Map{
|
||||
"id": i,
|
||||
"passport": fmt.Sprintf(`user_%d`, i),
|
||||
"password": fmt.Sprintf(`pass_%d`, i),
|
||||
"nickname": fmt.Sprintf(`name_%d`, i),
|
||||
"create_time": gtime.NewFromStr(CreateTime).String(),
|
||||
})
|
||||
}
|
||||
_, err = masterSlaveDB.Model(table).Data(array).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
var count int
|
||||
// Auto slave.
|
||||
count, err = masterSlaveDB.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, int64(0))
|
||||
|
||||
// slave.
|
||||
count, err = masterSlaveDB.Model(table).Slave().Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, int64(0))
|
||||
|
||||
// master.
|
||||
count, err = masterSlaveDB.Model(table).Master().Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, int64(TableSize))
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Master_Slave_Concurrent_ReadWrite tests concurrent read/write routing
|
||||
func Test_Master_Slave_Concurrent_ReadWrite(t *testing.T) {
|
||||
var err error
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
_, err = db.Exec(ctx, "CREATE DATABASE IF NOT EXISTS `master` CHARACTER SET UTF8")
|
||||
t.AssertNil(err)
|
||||
_, err = db.Exec(ctx, "CREATE DATABASE IF NOT EXISTS `slave` CHARACTER SET UTF8")
|
||||
t.AssertNil(err)
|
||||
})
|
||||
defer func() {
|
||||
_, _ = db.Exec(ctx, "DROP DATABASE `master`")
|
||||
_, _ = db.Exec(ctx, "DROP DATABASE `slave`")
|
||||
}()
|
||||
|
||||
var (
|
||||
configKey = guid.S()
|
||||
configGroup = gdb.ConfigGroup{
|
||||
gdb.ConfigNode{
|
||||
Host: "127.0.0.1",
|
||||
Port: "3307",
|
||||
User: "root",
|
||||
Pass: "12345678",
|
||||
Name: "master",
|
||||
Type: "mariadb",
|
||||
Role: "master",
|
||||
Weight: 100,
|
||||
},
|
||||
gdb.ConfigNode{
|
||||
Host: "127.0.0.1",
|
||||
Port: "3307",
|
||||
User: "root",
|
||||
Pass: "12345678",
|
||||
Name: "slave",
|
||||
Type: "mariadb",
|
||||
Role: "slave",
|
||||
Weight: 100,
|
||||
},
|
||||
}
|
||||
)
|
||||
gdb.SetConfigGroup(configKey, configGroup)
|
||||
masterSlaveDB := g.DB(configKey)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table := "table_" + guid.S()
|
||||
createTableWithDb(masterSlaveDB.Schema("master"), table)
|
||||
createTableWithDb(masterSlaveDB.Schema("slave"), table)
|
||||
defer dropTableWithDb(masterSlaveDB.Schema("master"), table)
|
||||
defer dropTableWithDb(masterSlaveDB.Schema("slave"), table)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
concurrency := 10
|
||||
|
||||
// Concurrent writes to master
|
||||
wg.Add(concurrency)
|
||||
for i := 0; i < concurrency; i++ {
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
_, err := masterSlaveDB.Model(table).Insert(g.Map{
|
||||
"passport": fmt.Sprintf("concurrent_%d", id),
|
||||
"password": fmt.Sprintf("pass_%d", id),
|
||||
"nickname": fmt.Sprintf("name_%d", id),
|
||||
})
|
||||
t.AssertNil(err)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Verify writes went to master
|
||||
count, err := masterSlaveDB.Model(table).Master().Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, concurrency)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Master_Slave_Transaction_Routing tests transaction routing to master
|
||||
func Test_Master_Slave_Transaction_Routing(t *testing.T) {
|
||||
var err error
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
_, err = db.Exec(ctx, "CREATE DATABASE IF NOT EXISTS `master` CHARACTER SET UTF8")
|
||||
t.AssertNil(err)
|
||||
_, err = db.Exec(ctx, "CREATE DATABASE IF NOT EXISTS `slave` CHARACTER SET UTF8")
|
||||
t.AssertNil(err)
|
||||
})
|
||||
defer func() {
|
||||
_, _ = db.Exec(ctx, "DROP DATABASE `master`")
|
||||
_, _ = db.Exec(ctx, "DROP DATABASE `slave`")
|
||||
}()
|
||||
|
||||
var (
|
||||
configKey = guid.S()
|
||||
configGroup = gdb.ConfigGroup{
|
||||
gdb.ConfigNode{
|
||||
Host: "127.0.0.1",
|
||||
Port: "3307",
|
||||
User: "root",
|
||||
Pass: "12345678",
|
||||
Name: "master",
|
||||
Type: "mariadb",
|
||||
Role: "master",
|
||||
Weight: 100,
|
||||
},
|
||||
gdb.ConfigNode{
|
||||
Host: "127.0.0.1",
|
||||
Port: "3307",
|
||||
User: "root",
|
||||
Pass: "12345678",
|
||||
Name: "slave",
|
||||
Type: "mariadb",
|
||||
Role: "slave",
|
||||
Weight: 100,
|
||||
},
|
||||
}
|
||||
)
|
||||
gdb.SetConfigGroup(configKey, configGroup)
|
||||
masterSlaveDB := g.DB(configKey)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table := "table_" + guid.S()
|
||||
createTableWithDb(masterSlaveDB.Schema("master"), table)
|
||||
createTableWithDb(masterSlaveDB.Schema("slave"), table)
|
||||
defer dropTableWithDb(masterSlaveDB.Schema("master"), table)
|
||||
defer dropTableWithDb(masterSlaveDB.Schema("slave"), table)
|
||||
|
||||
// Transaction should route to master
|
||||
err := masterSlaveDB.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
_, err := tx.Model(table).Insert(g.Map{
|
||||
"passport": "tx_user",
|
||||
"password": "tx_pass",
|
||||
"nickname": "tx_name",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Read within transaction should also use master
|
||||
count, err := tx.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 1)
|
||||
|
||||
return nil
|
||||
})
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify data is in master
|
||||
count, err := masterSlaveDB.Model(table).Master().Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 1)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Master_Slave_Explicit_Selection tests explicit master/slave selection
|
||||
func Test_Master_Slave_Explicit_Selection(t *testing.T) {
|
||||
var err error
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
_, err = db.Exec(ctx, "CREATE DATABASE IF NOT EXISTS `master` CHARACTER SET UTF8")
|
||||
t.AssertNil(err)
|
||||
_, err = db.Exec(ctx, "CREATE DATABASE IF NOT EXISTS `slave` CHARACTER SET UTF8")
|
||||
t.AssertNil(err)
|
||||
})
|
||||
defer func() {
|
||||
_, _ = db.Exec(ctx, "DROP DATABASE `master`")
|
||||
_, _ = db.Exec(ctx, "DROP DATABASE `slave`")
|
||||
}()
|
||||
|
||||
var (
|
||||
configKey = guid.S()
|
||||
configGroup = gdb.ConfigGroup{
|
||||
gdb.ConfigNode{
|
||||
Host: "127.0.0.1",
|
||||
Port: "3307",
|
||||
User: "root",
|
||||
Pass: "12345678",
|
||||
Name: "master",
|
||||
Type: "mariadb",
|
||||
Role: "master",
|
||||
Weight: 100,
|
||||
},
|
||||
gdb.ConfigNode{
|
||||
Host: "127.0.0.1",
|
||||
Port: "3307",
|
||||
User: "root",
|
||||
Pass: "12345678",
|
||||
Name: "slave",
|
||||
Type: "mariadb",
|
||||
Role: "slave",
|
||||
Weight: 100,
|
||||
},
|
||||
}
|
||||
)
|
||||
gdb.SetConfigGroup(configKey, configGroup)
|
||||
masterSlaveDB := g.DB(configKey)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
table := "table_" + guid.S()
|
||||
createTableWithDb(masterSlaveDB.Schema("master"), table)
|
||||
createTableWithDb(masterSlaveDB.Schema("slave"), table)
|
||||
defer dropTableWithDb(masterSlaveDB.Schema("master"), table)
|
||||
defer dropTableWithDb(masterSlaveDB.Schema("slave"), table)
|
||||
|
||||
// Insert to master
|
||||
_, err := masterSlaveDB.Model(table).Master().Insert(g.Map{
|
||||
"passport": "explicit_test",
|
||||
"password": "pass",
|
||||
"nickname": "name",
|
||||
})
|
||||
t.AssertNil(err)
|
||||
|
||||
// Explicitly read from slave (should be empty)
|
||||
count, err := masterSlaveDB.Model(table).Slave().Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 0)
|
||||
|
||||
// Explicitly read from master (should have data)
|
||||
count, err = masterSlaveDB.Model(table).Master().Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 1)
|
||||
})
|
||||
}
|
||||
115
contrib/drivers/mariadb/mariadb_z_unit_feature_metadata_test.go
Normal file
115
contrib/drivers/mariadb/mariadb_z_unit_feature_metadata_test.go
Normal file
@ -0,0 +1,115 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package mariadb_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
)
|
||||
|
||||
// Test_TableFields_Basic tests basic TableFields functionality
|
||||
func Test_TableFields_Basic(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
fields, err := db.TableFields(ctx, table)
|
||||
t.AssertNil(err)
|
||||
t.AssertGT(len(fields), 0)
|
||||
|
||||
// Verify common fields exist
|
||||
_, ok := fields["id"]
|
||||
t.Assert(ok, true)
|
||||
_, ok = fields["passport"]
|
||||
t.Assert(ok, true)
|
||||
_, ok = fields["password"]
|
||||
t.Assert(ok, true)
|
||||
_, ok = fields["nickname"]
|
||||
t.Assert(ok, true)
|
||||
_, ok = fields["create_time"]
|
||||
t.Assert(ok, true)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_TableFields_Schema tests TableFields with explicit schema
|
||||
func Test_TableFields_Schema(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
fields, err := db.TableFields(ctx, table, TestSchema1)
|
||||
t.AssertNil(err)
|
||||
t.AssertGT(len(fields), 0)
|
||||
|
||||
// Verify field properties
|
||||
idField, ok := fields["id"]
|
||||
t.Assert(ok, true)
|
||||
t.Assert(idField.Name, "id")
|
||||
t.AssertGT(idField.Index, -1)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_HasField_Positive tests HasField for existing field
|
||||
func Test_HasField_Positive(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
has, err := db.GetCore().HasField(ctx, table, "id")
|
||||
t.AssertNil(err)
|
||||
t.Assert(has, true)
|
||||
|
||||
has, err = db.GetCore().HasField(ctx, table, "passport")
|
||||
t.AssertNil(err)
|
||||
t.Assert(has, true)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_HasField_Negative tests HasField for non-existent field
|
||||
func Test_HasField_Negative(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
has, err := db.GetCore().HasField(ctx, table, "non_exist_field")
|
||||
t.AssertNil(err)
|
||||
t.Assert(has, false)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_HasField_Schema tests HasField with explicit schema
|
||||
func Test_HasField_Schema(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
has, err := db.GetCore().HasField(ctx, table, "id", TestSchema1)
|
||||
t.AssertNil(err)
|
||||
t.Assert(has, true)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_QuoteWord_Basic tests basic QuoteWord functionality
|
||||
func Test_QuoteWord_Basic(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
quoted := db.GetCore().QuoteWord("user")
|
||||
t.Assert(quoted, "`user`")
|
||||
|
||||
quoted = db.GetCore().QuoteWord("user_table")
|
||||
t.Assert(quoted, "`user_table`")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_QuoteWord_AlreadyQuoted tests QuoteWord with already quoted words
|
||||
func Test_QuoteWord_AlreadyQuoted(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// If already quoted, should not double quote
|
||||
quoted := db.GetCore().QuoteWord("`user`")
|
||||
t.Assert(quoted, "`user`")
|
||||
})
|
||||
}
|
||||
@ -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 mariadb_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})
|
||||
})
|
||||
}
|
||||
390
contrib/drivers/mariadb/mariadb_z_unit_feature_model_do_test.go
Normal file
390
contrib/drivers/mariadb/mariadb_z_unit_feature_model_do_test.go
Normal file
@ -0,0 +1,390 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package mariadb_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
func Test_Model_Insert_Data_DO(t *testing.T) {
|
||||
table := createTable()
|
||||
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.LastInsertId()
|
||||
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`], ``)
|
||||
t.Assert(one[`create_time`], ``)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_Insert_Data_List_DO(t *testing.T) {
|
||||
table := createTable()
|
||||
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.LastInsertId()
|
||||
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`], ``)
|
||||
t.Assert(one[`create_time`], ``)
|
||||
|
||||
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`], ``)
|
||||
t.Assert(one[`create_time`], ``)
|
||||
})
|
||||
}
|
||||
|
||||
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 := createTable()
|
||||
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.LastInsertId()
|
||||
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`], ``)
|
||||
t.Assert(one[`create_time`], ``)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_Insert_Data_List_ForDao(t *testing.T) {
|
||||
table := createTable()
|
||||
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.LastInsertId()
|
||||
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`], ``)
|
||||
t.Assert(one[`create_time`], ``)
|
||||
|
||||
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`], ``)
|
||||
t.Assert(one[`create_time`], ``)
|
||||
})
|
||||
}
|
||||
|
||||
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")
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,513 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package mariadb_test
|
||||
|
||||
import (
|
||||
"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 = gtime.TimestampNanoStr() + "_table1"
|
||||
table2 = 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 = gtime.TimestampNanoStr() + "_table1"
|
||||
table2 = 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 = gtime.TimestampNanoStr() + "_table1"
|
||||
table2 = 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 = gtime.TimestampNanoStr() + "_table1"
|
||||
table2 = 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 = gtime.TimestampNanoStr() + "_table1"
|
||||
table2 = 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 = gtime.TimestampNanoStr() + "_table1"
|
||||
table2 = 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 = gtime.TimestampNanoStr() + "_table1"
|
||||
table2 = 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")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Join_FiveTables tests complex join with 5+ tables
|
||||
func Test_Model_Join_FiveTables(t *testing.T) {
|
||||
var (
|
||||
table1 = gtime.TimestampNanoStr() + "_table1"
|
||||
table2 = gtime.TimestampNanoStr() + "_table2"
|
||||
table3 = gtime.TimestampNanoStr() + "_table3"
|
||||
table4 = gtime.TimestampNanoStr() + "_table4"
|
||||
table5 = gtime.TimestampNanoStr() + "_table5"
|
||||
)
|
||||
createInitTable(table1)
|
||||
defer dropTable(table1)
|
||||
createInitTable(table2)
|
||||
defer dropTable(table2)
|
||||
createInitTable(table3)
|
||||
defer dropTable(table3)
|
||||
createInitTable(table4)
|
||||
defer dropTable(table4)
|
||||
createInitTable(table5)
|
||||
defer dropTable(table5)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
r, err := db.Model(table1).As("t1").
|
||||
FieldsPrefix("t1", "id", "nickname").
|
||||
FieldsPrefix("t2", "passport").
|
||||
InnerJoin(table2+" AS t2", "t1.id = t2.id").
|
||||
InnerJoin(table3+" AS t3", "t2.id = t3.id").
|
||||
InnerJoin(table4+" AS t4", "t3.id = t4.id").
|
||||
InnerJoin(table5+" AS t5", "t4.id = t5.id").
|
||||
Where("t1.id IN(?)", g.Slice{1, 2, 3}).
|
||||
Order("t1.id asc").
|
||||
All()
|
||||
t.AssertNil(err)
|
||||
|
||||
t.Assert(len(r), 3)
|
||||
t.Assert(r[0]["id"], "1")
|
||||
t.Assert(r[0]["nickname"], "name_1")
|
||||
t.Assert(r[0]["passport"], "user_1")
|
||||
t.Assert(r[2]["id"], "3")
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// 6 tables with mixed join types
|
||||
table6 := gtime.TimestampNanoStr() + "_table6"
|
||||
createInitTable(table6)
|
||||
defer dropTable(table6)
|
||||
|
||||
r, err := db.Model(table1).As("t1").
|
||||
Fields("t1.id").
|
||||
InnerJoin(table2+" AS t2", "t1.id = t2.id").
|
||||
LeftJoin(table3+" AS t3", "t2.id = t3.id").
|
||||
InnerJoin(table4+" AS t4", "t3.id = t4.id").
|
||||
RightJoin(table5+" AS t5", "t4.id = t5.id").
|
||||
LeftJoin(table6+" AS t6", "t5.id = t6.id").
|
||||
Where("t1.id", 5).
|
||||
One()
|
||||
t.AssertNil(err)
|
||||
|
||||
t.Assert(r["id"], "5")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Join_SelfJoin tests self-join scenarios
|
||||
func Test_Model_Join_SelfJoin(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Self-join to find pairs where a.id < b.id
|
||||
r, err := db.Model(table).As("a").
|
||||
Fields("a.id AS a_id", "b.id AS b_id").
|
||||
InnerJoin(table+" AS b", "a.id < b.id").
|
||||
Where("a.id", 1).
|
||||
Where("b.id <=", 3).
|
||||
Order("b.id asc").
|
||||
All()
|
||||
t.AssertNil(err)
|
||||
|
||||
t.Assert(len(r), 2)
|
||||
t.Assert(r[0]["a_id"], "1")
|
||||
t.Assert(r[0]["b_id"], "2")
|
||||
t.Assert(r[1]["b_id"], "3")
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Self-join with multiple conditions
|
||||
r, err := db.Model(table).As("a").
|
||||
Fields("a.id", "a.nickname", "b.nickname AS other_nickname").
|
||||
LeftJoin(table+" AS b", "a.id = b.id - 1").
|
||||
Where("a.id IN(?)", g.Slice{1, 2}).
|
||||
Order("a.id asc").
|
||||
All()
|
||||
t.AssertNil(err)
|
||||
|
||||
t.Assert(len(r), 2)
|
||||
t.Assert(r[0]["id"], "1")
|
||||
t.Assert(r[0]["nickname"], "name_1")
|
||||
t.Assert(r[0]["other_nickname"], "name_2")
|
||||
t.Assert(r[1]["id"], "2")
|
||||
t.Assert(r[1]["other_nickname"], "name_3")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Join_LeftJoinNull tests LEFT JOIN NULL handling
|
||||
func Test_Model_Join_LeftJoinNull(t *testing.T) {
|
||||
var (
|
||||
table1 = gtime.TimestampNanoStr() + "_table1"
|
||||
table2 = gtime.TimestampNanoStr() + "_table2"
|
||||
)
|
||||
createInitTable(table1)
|
||||
defer dropTable(table1)
|
||||
|
||||
// Create table2 with only partial data
|
||||
createTable(table2)
|
||||
defer dropTable(table2)
|
||||
_, err := db.Insert(ctx, table2, g.List{
|
||||
{"id": 1, "passport": "user_1", "nickname": "name_1"},
|
||||
{"id": 2, "passport": "user_2", "nickname": "name_2"},
|
||||
})
|
||||
if err != nil {
|
||||
gtest.Fatal(err)
|
||||
}
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// LEFT JOIN - table1 has all records, table2 only has id 1,2
|
||||
r, err := db.Model(table1).As("t1").
|
||||
FieldsPrefix("t1", "id").
|
||||
FieldsPrefix("t2", "nickname").
|
||||
LeftJoin(table2+" AS t2", "t1.id = t2.id").
|
||||
Where("t1.id IN(?)", g.Slice{1, 2, 3}).
|
||||
Order("t1.id asc").
|
||||
All()
|
||||
t.AssertNil(err)
|
||||
|
||||
t.Assert(len(r), 3)
|
||||
t.Assert(r[0]["id"], "1")
|
||||
t.Assert(r[0]["nickname"], "name_1") // matched
|
||||
t.Assert(r[1]["id"], "2")
|
||||
t.Assert(r[1]["nickname"], "name_2") // matched
|
||||
t.Assert(r[2]["id"], "3")
|
||||
// r[2]["nickname"] should be NULL/empty from t2
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Find records where RIGHT table is NULL
|
||||
r, err := db.Model(table1).As("t1").
|
||||
FieldsPrefix("t1", "id", "nickname").
|
||||
LeftJoin(table2+" AS t2", "t1.id = t2.id").
|
||||
Where("t2.id IS NULL").
|
||||
Where("t1.id IN(?)", g.Slice{1, 2, 3, 4}).
|
||||
Order("t1.id asc").
|
||||
All()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Should return id 3,4 (not in table2)
|
||||
t.Assert(len(r), 2)
|
||||
t.Assert(r[0]["id"], "3")
|
||||
t.Assert(r[0]["nickname"], "name_3")
|
||||
t.Assert(r[1]["id"], "4")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Join_RightJoinNull tests RIGHT JOIN NULL handling
|
||||
func Test_Model_Join_RightJoinNull(t *testing.T) {
|
||||
var (
|
||||
table1 = gtime.TimestampNanoStr() + "_table1"
|
||||
table2 = gtime.TimestampNanoStr() + "_table2"
|
||||
)
|
||||
// table1 has partial data
|
||||
createTable(table1)
|
||||
defer dropTable(table1)
|
||||
_, err := db.Insert(ctx, table1, g.List{
|
||||
{"id": 1, "passport": "user_1", "nickname": "name_1"},
|
||||
{"id": 2, "passport": "user_2", "nickname": "name_2"},
|
||||
})
|
||||
if err != nil {
|
||||
gtest.Fatal(err)
|
||||
}
|
||||
|
||||
// table2 has all data
|
||||
createInitTable(table2)
|
||||
defer dropTable(table2)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// RIGHT JOIN - table1 only has id 1,2, table2 has all
|
||||
r, err := db.Model(table1).As("t1").
|
||||
FieldsPrefix("t2", "id").
|
||||
FieldsPrefix("t1", "nickname").
|
||||
RightJoin(table2+" AS t2", "t1.id = t2.id").
|
||||
Where("t2.id IN(?)", g.Slice{1, 2, 3}).
|
||||
Order("t2.id asc").
|
||||
All()
|
||||
t.AssertNil(err)
|
||||
|
||||
t.Assert(len(r), 3)
|
||||
t.Assert(r[0]["id"], "1")
|
||||
t.Assert(r[0]["nickname"], "name_1") // matched
|
||||
t.Assert(r[1]["id"], "2")
|
||||
t.Assert(r[1]["nickname"], "name_2") // matched
|
||||
t.Assert(r[2]["id"], "3")
|
||||
// r[2]["nickname"] should be NULL/empty from t1
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Find records where LEFT table is NULL
|
||||
r, err := db.Model(table1).As("t1").
|
||||
FieldsPrefix("t2", "id", "nickname").
|
||||
RightJoin(table2+" AS t2", "t1.id = t2.id").
|
||||
Where("t1.id IS NULL").
|
||||
Where("t2.id IN(?)", g.Slice{1, 2, 3, 4}).
|
||||
Order("t2.id asc").
|
||||
All()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Should return id 3,4 (not in table1)
|
||||
t.Assert(len(r), 2)
|
||||
t.Assert(r[0]["id"], "3")
|
||||
t.Assert(r[0]["nickname"], "name_3")
|
||||
t.Assert(r[1]["id"], "4")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Join_OnVsWhere tests difference between ON and WHERE conditions
|
||||
func Test_Model_Join_OnVsWhere(t *testing.T) {
|
||||
var (
|
||||
table1 = gtime.TimestampNanoStr() + "_table1"
|
||||
table2 = gtime.TimestampNanoStr() + "_table2"
|
||||
)
|
||||
createInitTable(table1)
|
||||
defer dropTable(table1)
|
||||
createInitTable(table2)
|
||||
defer dropTable(table2)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// INNER JOIN: ON and WHERE behave the same
|
||||
r1, err := db.Model(table1).As("t1").
|
||||
Fields("t1.id").
|
||||
InnerJoin(table2+" AS t2", "t1.id = t2.id AND t2.id <= 3").
|
||||
Order("t1.id asc").
|
||||
All()
|
||||
t.AssertNil(err)
|
||||
|
||||
r2, err := db.Model(table1).As("t1").
|
||||
Fields("t1.id").
|
||||
InnerJoin(table2+" AS t2", "t1.id = t2.id").
|
||||
Where("t2.id <=", 3).
|
||||
Order("t1.id asc").
|
||||
All()
|
||||
t.AssertNil(err)
|
||||
|
||||
// For INNER JOIN, results should be identical
|
||||
t.Assert(len(r1), 3)
|
||||
t.Assert(len(r2), 3)
|
||||
t.Assert(r1[0]["id"], r2[0]["id"])
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// LEFT JOIN: ON filter in join condition vs WHERE filter after join
|
||||
// ON condition: filters t2 before join (keeps all t1 rows)
|
||||
r1, err := db.Model(table1).As("t1").
|
||||
FieldsPrefix("t1", "id").
|
||||
FieldsPrefix("t2", "nickname").
|
||||
LeftJoin(table2+" AS t2", "t1.id = t2.id AND t2.id <= 2").
|
||||
Where("t1.id <=", 4).
|
||||
Order("t1.id asc").
|
||||
All()
|
||||
t.AssertNil(err)
|
||||
|
||||
// WHERE condition: filters result after join (removes t1 rows where t2 is NULL)
|
||||
r2, err := db.Model(table1).As("t1").
|
||||
FieldsPrefix("t1", "id").
|
||||
FieldsPrefix("t2", "nickname").
|
||||
LeftJoin(table2+" AS t2", "t1.id = t2.id").
|
||||
Where("t1.id <=", 4).
|
||||
Where("t2.id <=", 2).
|
||||
Order("t1.id asc").
|
||||
All()
|
||||
t.AssertNil(err)
|
||||
|
||||
// r1: all t1 rows (1,2,3,4), t2 data only for id 1,2
|
||||
t.Assert(len(r1), 4)
|
||||
t.Assert(r1[0]["id"], "1")
|
||||
t.Assert(r1[0]["nickname"], "name_1")
|
||||
t.Assert(r1[2]["id"], "3")
|
||||
// r1[2]["nickname"] is NULL from t2
|
||||
|
||||
// r2: only rows where t2.id <= 2, so only id 1,2
|
||||
t.Assert(len(r2), 2)
|
||||
t.Assert(r2[0]["id"], "1")
|
||||
t.Assert(r2[1]["id"], "2")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Join_ComplexConditions tests joins with complex ON conditions
|
||||
func Test_Model_Join_ComplexConditions(t *testing.T) {
|
||||
var (
|
||||
table1 = gtime.TimestampNanoStr() + "_table1"
|
||||
table2 = gtime.TimestampNanoStr() + "_table2"
|
||||
)
|
||||
createInitTable(table1)
|
||||
defer dropTable(table1)
|
||||
createInitTable(table2)
|
||||
defer dropTable(table2)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Multiple AND conditions in ON clause
|
||||
r, err := db.Model(table1).As("t1").
|
||||
Fields("t1.id", "t1.nickname").
|
||||
InnerJoin(
|
||||
table2+" AS t2",
|
||||
"t1.id = t2.id AND t1.nickname = t2.nickname AND t1.id BETWEEN 2 AND 4",
|
||||
).
|
||||
Order("t1.id asc").
|
||||
All()
|
||||
t.AssertNil(err)
|
||||
|
||||
t.Assert(len(r), 3)
|
||||
t.Assert(r[0]["id"], "2")
|
||||
t.Assert(r[2]["id"], "4")
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// OR conditions in ON clause (need to use Where for OR in join)
|
||||
r, err := db.Model(table1).As("t1").
|
||||
Fields("t1.id").
|
||||
InnerJoin(table2+" AS t2", "t1.id = t2.id").
|
||||
Where("t2.id = 1 OR t2.id = 5").
|
||||
Order("t1.id asc").
|
||||
All()
|
||||
t.AssertNil(err)
|
||||
|
||||
t.Assert(len(r), 2)
|
||||
t.Assert(r[0]["id"], "1")
|
||||
t.Assert(r[1]["id"], "5")
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,467 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package mariadb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
)
|
||||
|
||||
const (
|
||||
TestDbNameSh0 = "test_0"
|
||||
TestDbNameSh1 = "test_1"
|
||||
TestTableName = "user"
|
||||
)
|
||||
|
||||
type ShardingUser struct {
|
||||
Id int
|
||||
Name string
|
||||
}
|
||||
|
||||
// createShardingDatabase creates test databases and tables for sharding
|
||||
func createShardingDatabase(t *gtest.T) {
|
||||
// Create databases
|
||||
dbs := []string{TestDbNameSh0, TestDbNameSh1}
|
||||
for _, dbName := range dbs {
|
||||
sql := fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s`", dbName)
|
||||
_, err := db.Exec(ctx, sql)
|
||||
t.AssertNil(err)
|
||||
|
||||
// Switch to the database
|
||||
sql = fmt.Sprintf("USE `%s`", dbName)
|
||||
_, err = db.Exec(ctx, sql)
|
||||
t.AssertNil(err)
|
||||
|
||||
// Create tables
|
||||
tables := []string{"user_0", "user_1", "user_2", "user_3"}
|
||||
for _, table := range tables {
|
||||
sql := fmt.Sprintf(`
|
||||
CREATE TABLE IF NOT EXISTS %s (
|
||||
id int(11) NOT NULL,
|
||||
name varchar(255) NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`, table)
|
||||
_, err := db.Exec(ctx, sql)
|
||||
t.AssertNil(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dropShardingDatabase drops test databases
|
||||
func dropShardingDatabase(t *gtest.T) {
|
||||
dbs := []string{TestDbNameSh0, TestDbNameSh1}
|
||||
for _, dbName := range dbs {
|
||||
sql := fmt.Sprintf("DROP DATABASE IF EXISTS `%s`", dbName)
|
||||
_, err := db.Exec(ctx, sql)
|
||||
t.AssertNil(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Sharding_Basic(t *testing.T) {
|
||||
return
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var (
|
||||
tablePrefix = "user_"
|
||||
schemaPrefix = "test_"
|
||||
)
|
||||
|
||||
// Create test databases and tables
|
||||
createShardingDatabase(t)
|
||||
defer dropShardingDatabase(t)
|
||||
|
||||
// Create sharding configuration
|
||||
shardingConfig := gdb.ShardingConfig{
|
||||
Table: gdb.ShardingTableConfig{
|
||||
Enable: true,
|
||||
Prefix: tablePrefix,
|
||||
Rule: &gdb.DefaultShardingRule{
|
||||
TableCount: 4,
|
||||
},
|
||||
},
|
||||
Schema: gdb.ShardingSchemaConfig{
|
||||
Enable: true,
|
||||
Prefix: schemaPrefix,
|
||||
Rule: &gdb.DefaultShardingRule{
|
||||
SchemaCount: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Prepare test data
|
||||
user := ShardingUser{
|
||||
Id: 1,
|
||||
Name: "John",
|
||||
}
|
||||
|
||||
model := db.Model(TestTableName).
|
||||
Sharding(shardingConfig).
|
||||
ShardingValue(user.Id).
|
||||
Safe()
|
||||
|
||||
// Test Insert
|
||||
_, err := model.Data(user).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Test Select
|
||||
var result ShardingUser
|
||||
err = model.Where("id", user.Id).Scan(&result)
|
||||
t.AssertNil(err)
|
||||
t.Assert(result.Id, user.Id)
|
||||
t.Assert(result.Name, user.Name)
|
||||
|
||||
// Test Update
|
||||
_, err = model.Data(g.Map{"name": "John Doe"}).
|
||||
Where("id", user.Id).
|
||||
Update()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify Update
|
||||
err = model.Where("id", user.Id).Scan(&result)
|
||||
t.AssertNil(err)
|
||||
t.Assert(result.Name, "John Doe")
|
||||
|
||||
// Test Delete
|
||||
_, err = model.Where("id", user.Id).Delete()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify Delete
|
||||
count, err := model.Where("id", user.Id).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 0)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Sharding_Error tests error cases
|
||||
func Test_Sharding_Error(t *testing.T) {
|
||||
return
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Create test databases and tables
|
||||
createShardingDatabase(t)
|
||||
defer dropShardingDatabase(t)
|
||||
|
||||
// Test missing sharding value
|
||||
model := db.Model(TestTableName).
|
||||
Sharding(gdb.ShardingConfig{
|
||||
Table: gdb.ShardingTableConfig{
|
||||
Enable: true,
|
||||
Prefix: "user_",
|
||||
Rule: &gdb.DefaultShardingRule{TableCount: 4},
|
||||
},
|
||||
}).Safe()
|
||||
|
||||
_, err := model.Insert(g.Map{"id": 1, "name": "test"})
|
||||
t.AssertNE(err, nil)
|
||||
t.Assert(err.Error(), "sharding value is required when sharding feature enabled")
|
||||
|
||||
// Test missing sharding rule
|
||||
model = db.Model(TestTableName).
|
||||
Sharding(gdb.ShardingConfig{
|
||||
Table: gdb.ShardingTableConfig{
|
||||
Enable: true,
|
||||
Prefix: "user_",
|
||||
},
|
||||
}).
|
||||
ShardingValue(1)
|
||||
|
||||
_, err = model.Insert(g.Map{"id": 1, "name": "test"})
|
||||
t.AssertNE(err, nil)
|
||||
t.Assert(err.Error(), "sharding rule is required when sharding feature enabled")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Sharding_Complex tests complex sharding scenarios
|
||||
func Test_Sharding_Complex(t *testing.T) {
|
||||
return
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Create test databases and tables
|
||||
createShardingDatabase(t)
|
||||
defer dropShardingDatabase(t)
|
||||
|
||||
shardingConfig := gdb.ShardingConfig{
|
||||
Table: gdb.ShardingTableConfig{
|
||||
Enable: true,
|
||||
Prefix: "user_",
|
||||
Rule: &gdb.DefaultShardingRule{TableCount: 4},
|
||||
},
|
||||
Schema: gdb.ShardingSchemaConfig{
|
||||
Enable: true,
|
||||
Prefix: "test_",
|
||||
Rule: &gdb.DefaultShardingRule{SchemaCount: 2},
|
||||
},
|
||||
}
|
||||
|
||||
users := []ShardingUser{
|
||||
{Id: 1, Name: "User1"},
|
||||
{Id: 2, Name: "User2"},
|
||||
{Id: 3, Name: "User3"},
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
model := db.Model(TestTableName).
|
||||
Sharding(shardingConfig).
|
||||
ShardingValue(user.Id).
|
||||
Safe()
|
||||
|
||||
_, err := model.Data(user).Insert()
|
||||
t.AssertNil(err)
|
||||
}
|
||||
|
||||
// Test batch query
|
||||
for _, user := range users {
|
||||
model := db.Model(TestTableName).
|
||||
Sharding(shardingConfig).
|
||||
ShardingValue(user.Id).
|
||||
Safe()
|
||||
|
||||
var result ShardingUser
|
||||
err := model.Where("id", user.Id).Scan(&result)
|
||||
t.AssertNil(err)
|
||||
t.Assert(result.Id, user.Id)
|
||||
t.Assert(result.Name, user.Name)
|
||||
}
|
||||
|
||||
// Clean up
|
||||
for _, user := range users {
|
||||
model := db.Model(TestTableName).
|
||||
Sharding(shardingConfig).
|
||||
ShardingValue(user.Id).
|
||||
Safe()
|
||||
|
||||
_, err := model.Where("id", user.Id).Delete()
|
||||
t.AssertNil(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_Sharding_Table_Using_Hook(t *testing.T) {
|
||||
var (
|
||||
table1 = gtime.TimestampNanoStr() + "_table1"
|
||||
table2 = gtime.TimestampNanoStr() + "_table2"
|
||||
)
|
||||
createTable(table1)
|
||||
defer dropTable(table1)
|
||||
createTable(table2)
|
||||
defer dropTable(table2)
|
||||
|
||||
shardingModel := db.Model(table1).Hook(gdb.HookHandler{
|
||||
Select: func(ctx context.Context, in *gdb.HookSelectInput) (result gdb.Result, err error) {
|
||||
in.Table = table2
|
||||
return in.Next(ctx)
|
||||
},
|
||||
Insert: func(ctx context.Context, in *gdb.HookInsertInput) (result sql.Result, err error) {
|
||||
in.Table = table2
|
||||
return in.Next(ctx)
|
||||
},
|
||||
Update: func(ctx context.Context, in *gdb.HookUpdateInput) (result sql.Result, err error) {
|
||||
in.Table = table2
|
||||
return in.Next(ctx)
|
||||
},
|
||||
Delete: func(ctx context.Context, in *gdb.HookDeleteInput) (result sql.Result, err error) {
|
||||
in.Table = table2
|
||||
return in.Next(ctx)
|
||||
},
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
r, err := shardingModel.Insert(g.Map{
|
||||
"id": 1,
|
||||
"passport": fmt.Sprintf(`user_%d`, 1),
|
||||
"password": fmt.Sprintf(`pass_%d`, 1),
|
||||
"nickname": fmt.Sprintf(`name_%d`, 1),
|
||||
"create_time": gtime.NewFromStr(CreateTime).String(),
|
||||
})
|
||||
t.AssertNil(err)
|
||||
n, err := r.RowsAffected()
|
||||
t.AssertNil(err)
|
||||
t.Assert(n, 1)
|
||||
|
||||
var count int
|
||||
count, err = shardingModel.Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 1)
|
||||
|
||||
count, err = db.Model(table1).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 0)
|
||||
|
||||
count, err = db.Model(table2).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 1)
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
r, err := shardingModel.Where(g.Map{
|
||||
"id": 1,
|
||||
}).Data(g.Map{
|
||||
"passport": fmt.Sprintf(`user_%d`, 2),
|
||||
"password": fmt.Sprintf(`pass_%d`, 2),
|
||||
"nickname": fmt.Sprintf(`name_%d`, 2),
|
||||
}).Update()
|
||||
t.AssertNil(err)
|
||||
n, err := r.RowsAffected()
|
||||
t.AssertNil(err)
|
||||
t.Assert(n, 1)
|
||||
|
||||
var (
|
||||
count int
|
||||
where = g.Map{"passport": fmt.Sprintf(`user_%d`, 2)}
|
||||
)
|
||||
count, err = shardingModel.Where(where).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 1)
|
||||
|
||||
count, err = db.Model(table1).Where(where).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 0)
|
||||
|
||||
count, err = db.Model(table2).Where(where).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 1)
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
r, err := shardingModel.Where(g.Map{
|
||||
"id": 1,
|
||||
}).Delete()
|
||||
t.AssertNil(err)
|
||||
n, err := r.RowsAffected()
|
||||
t.AssertNil(err)
|
||||
t.Assert(n, 1)
|
||||
|
||||
var count int
|
||||
count, err = shardingModel.Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 0)
|
||||
|
||||
count, err = db.Model(table1).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 0)
|
||||
|
||||
count, err = db.Model(table2).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_Sharding_Schema_Using_Hook(t *testing.T) {
|
||||
var (
|
||||
table = gtime.TimestampNanoStr() + "_table"
|
||||
)
|
||||
createTableWithDb(db, table)
|
||||
defer dropTableWithDb(db, table)
|
||||
createTableWithDb(db2, table)
|
||||
defer dropTableWithDb(db2, table)
|
||||
|
||||
shardingModel := db.Model(table).Hook(gdb.HookHandler{
|
||||
Select: func(ctx context.Context, in *gdb.HookSelectInput) (result gdb.Result, err error) {
|
||||
in.Table = table
|
||||
in.Schema = db2.GetSchema()
|
||||
return in.Next(ctx)
|
||||
},
|
||||
Insert: func(ctx context.Context, in *gdb.HookInsertInput) (result sql.Result, err error) {
|
||||
in.Table = table
|
||||
in.Schema = db2.GetSchema()
|
||||
return in.Next(ctx)
|
||||
},
|
||||
Update: func(ctx context.Context, in *gdb.HookUpdateInput) (result sql.Result, err error) {
|
||||
in.Table = table
|
||||
in.Schema = db2.GetSchema()
|
||||
return in.Next(ctx)
|
||||
},
|
||||
Delete: func(ctx context.Context, in *gdb.HookDeleteInput) (result sql.Result, err error) {
|
||||
in.Table = table
|
||||
in.Schema = db2.GetSchema()
|
||||
return in.Next(ctx)
|
||||
},
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
r, err := shardingModel.Insert(g.Map{
|
||||
"id": 1,
|
||||
"passport": fmt.Sprintf(`user_%d`, 1),
|
||||
"password": fmt.Sprintf(`pass_%d`, 1),
|
||||
"nickname": fmt.Sprintf(`name_%d`, 1),
|
||||
"create_time": gtime.NewFromStr(CreateTime).String(),
|
||||
})
|
||||
t.AssertNil(err)
|
||||
n, err := r.RowsAffected()
|
||||
t.AssertNil(err)
|
||||
t.Assert(n, 1)
|
||||
|
||||
var count int
|
||||
count, err = shardingModel.Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 1)
|
||||
|
||||
count, err = db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 0)
|
||||
|
||||
count, err = db2.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 1)
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
r, err := shardingModel.Where(g.Map{
|
||||
"id": 1,
|
||||
}).Data(g.Map{
|
||||
"passport": fmt.Sprintf(`user_%d`, 2),
|
||||
"password": fmt.Sprintf(`pass_%d`, 2),
|
||||
"nickname": fmt.Sprintf(`name_%d`, 2),
|
||||
}).Update()
|
||||
t.AssertNil(err)
|
||||
n, err := r.RowsAffected()
|
||||
t.AssertNil(err)
|
||||
t.Assert(n, 1)
|
||||
|
||||
var (
|
||||
count int
|
||||
where = g.Map{"passport": fmt.Sprintf(`user_%d`, 2)}
|
||||
)
|
||||
count, err = shardingModel.Where(where).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 1)
|
||||
|
||||
count, err = db.Model(table).Where(where).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 0)
|
||||
|
||||
count, err = db2.Model(table).Where(where).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 1)
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
r, err := shardingModel.Where(g.Map{
|
||||
"id": 1,
|
||||
}).Delete()
|
||||
t.AssertNil(err)
|
||||
n, err := r.RowsAffected()
|
||||
t.AssertNil(err)
|
||||
t.Assert(n, 1)
|
||||
|
||||
var count int
|
||||
count, err = shardingModel.Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 0)
|
||||
|
||||
count, err = db.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 0)
|
||||
|
||||
count, err = db2.Model(table).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 0)
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,482 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package mariadb_test
|
||||
|
||||
import (
|
||||
"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"`
|
||||
Uid int `json:"uid"`
|
||||
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,
|
||||
Uid: 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"`
|
||||
Uid int `json:"uid"`
|
||||
}
|
||||
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,
|
||||
"uid": 101,
|
||||
"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
|
||||
}
|
||||
// db.SetDebug(true)
|
||||
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)
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,311 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package mariadb_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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}),
|
||||
).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)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_SubQuery_Correlated tests scalar subquery and correlated subquery with EXISTS
|
||||
func Test_Model_SubQuery_Correlated(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Scalar subquery: find users whose id is greater than average id
|
||||
subQuery := db.Model(table + " AS inner_table").Fields("AVG(id)")
|
||||
r, err := db.Model(table).Where(
|
||||
"id > (?)",
|
||||
subQuery,
|
||||
).OrderAsc("id").All()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Average of 1-10 is 5.5, so expect ids 6-10
|
||||
t.Assert(len(r), 5)
|
||||
t.Assert(r[0]["id"], 6)
|
||||
t.Assert(r[4]["id"], 10)
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Correlated subquery with EXISTS: find users with id matching their own id
|
||||
r, err := db.Model(table+" AS outer_table").
|
||||
Where(
|
||||
fmt.Sprintf("EXISTS (SELECT 1 FROM %s AS inner_table WHERE inner_table.id = outer_table.id AND inner_table.id <= ?)", table),
|
||||
3,
|
||||
).
|
||||
OrderAsc("id").
|
||||
All()
|
||||
t.AssertNil(err)
|
||||
|
||||
t.Assert(len(r), 3)
|
||||
t.Assert(r[0]["id"], 1)
|
||||
t.Assert(r[2]["id"], 3)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_SubQuery_From tests subquery in FROM clause
|
||||
func Test_Model_SubQuery_From(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Subquery in FROM clause
|
||||
subQuery := db.Model(table).Where("id <=", 5)
|
||||
r, err := db.Model("(?) AS sub", subQuery).
|
||||
Fields("sub.id", "sub.nickname").
|
||||
Where("sub.id >", 2).
|
||||
OrderAsc("id").
|
||||
All()
|
||||
t.AssertNil(err)
|
||||
|
||||
t.Assert(len(r), 3)
|
||||
t.Assert(r[0]["id"], 3)
|
||||
t.Assert(r[0]["nickname"], "name_3")
|
||||
t.Assert(r[2]["id"], 5)
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Multiple subqueries in FROM clause with JOIN
|
||||
subQuery1 := db.Model(table).Fields("id", "nickname").Where("id <=", 3)
|
||||
subQuery2 := db.Model(table).Fields("id", "passport").Where("id >=", 3)
|
||||
|
||||
r, err := db.Model("? AS a, ? AS b", subQuery1, subQuery2).
|
||||
Fields("a.id", "a.nickname", "b.passport").
|
||||
Where("a.id = b.id").
|
||||
OrderAsc("id").
|
||||
All()
|
||||
t.AssertNil(err)
|
||||
|
||||
t.Assert(len(r), 1)
|
||||
t.Assert(r[0]["id"], 3)
|
||||
t.Assert(r[0]["nickname"], "name_3")
|
||||
t.Assert(r[0]["passport"], "user_3")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_SubQuery_Select tests subquery in SELECT clause
|
||||
func Test_Model_SubQuery_Select(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Subquery in SELECT clause for scalar value
|
||||
r, err := db.Model(table).
|
||||
Fields("id", "nickname", fmt.Sprintf("(SELECT MAX(id) FROM %s) AS max_id", table)).
|
||||
Where("id", 1).
|
||||
One()
|
||||
t.AssertNil(err)
|
||||
|
||||
t.Assert(r["id"], 1)
|
||||
t.Assert(r["nickname"], "name_1")
|
||||
t.Assert(r["max_id"], 10)
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Multiple subqueries in SELECT clause
|
||||
r, err := db.Model(table).
|
||||
Fields(
|
||||
"id",
|
||||
fmt.Sprintf("(SELECT MAX(id) FROM %s) AS max_id", table),
|
||||
fmt.Sprintf("(SELECT MIN(id) FROM %s) AS min_id", table),
|
||||
).
|
||||
Where("id", 5).
|
||||
One()
|
||||
t.AssertNil(err)
|
||||
|
||||
t.Assert(r["id"], 5)
|
||||
t.Assert(r["max_id"], 10)
|
||||
t.Assert(r["min_id"], 1)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_SubQuery_Nested tests multi-level nested subqueries (3+ levels)
|
||||
func Test_Model_SubQuery_Nested(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// 3-level nested subquery
|
||||
// Level 3: innermost - get ids <= 8
|
||||
level3 := db.Model(table).Fields("id").Where("id <=", 8)
|
||||
|
||||
// Level 2: middle - filter from level 3 where id >= 3
|
||||
level2 := db.Model("(?) AS l3", level3).Fields("l3.id").Where("l3.id >=", 3)
|
||||
|
||||
// Level 1: outermost - filter from level 2 where id <= 6
|
||||
r, err := db.Model(table).
|
||||
Where("id IN (?)", level2).
|
||||
Where("id <=", 6).
|
||||
OrderAsc("id").
|
||||
All()
|
||||
t.AssertNil(err)
|
||||
|
||||
t.Assert(len(r), 4)
|
||||
t.Assert(r[0]["id"], 3)
|
||||
t.Assert(r[3]["id"], 6)
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// 4-level nested subquery with aggregates
|
||||
// Level 4: get all ids
|
||||
level4 := db.Model(table).Fields("id")
|
||||
|
||||
// Level 3: get ids > 5 from level 4
|
||||
level3 := db.Model("(?) AS l4", level4).Fields("l4.id").Where("l4.id >", 5)
|
||||
|
||||
// Level 2: get MIN(id) from level 3
|
||||
level2 := db.Model("(?) AS l3", level3).Fields("MIN(l3.id)")
|
||||
|
||||
// Level 1: find records >= the minimum from level 2
|
||||
r, err := db.Model(table).
|
||||
Where("id >= (?)", level2).
|
||||
OrderAsc("id").
|
||||
All()
|
||||
t.AssertNil(err)
|
||||
|
||||
// MIN(id) from level 3 should be 6, so expect ids 6-10
|
||||
t.Assert(len(r), 5)
|
||||
t.Assert(r[0]["id"], 6)
|
||||
t.Assert(r[4]["id"], 10)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_SubQuery_WhereIn tests subquery with WHERE IN
|
||||
func Test_Model_SubQuery_WhereIn(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Simple WHERE IN with subquery
|
||||
subQuery := db.Model(table).Fields("id").Where("id IN(?)", g.Slice{2, 4, 6})
|
||||
r, err := db.Model(table).
|
||||
Where("id IN(?)", subQuery).
|
||||
OrderAsc("id").
|
||||
All()
|
||||
t.AssertNil(err)
|
||||
|
||||
t.Assert(len(r), 3)
|
||||
t.Assert(r[0]["id"], 2)
|
||||
t.Assert(r[1]["id"], 4)
|
||||
t.Assert(r[2]["id"], 6)
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Multiple WHERE IN subqueries combined
|
||||
subQuery1 := db.Model(table).Fields("id").Where("id <=", 5)
|
||||
subQuery2 := db.Model(table).Fields("id").Where("id >=", 3)
|
||||
|
||||
r, err := db.Model(table).
|
||||
Where("id IN(?)", subQuery1).
|
||||
Where("id IN(?)", subQuery2).
|
||||
OrderAsc("id").
|
||||
All()
|
||||
t.AssertNil(err)
|
||||
|
||||
t.Assert(len(r), 3)
|
||||
t.Assert(r[0]["id"], 3)
|
||||
t.Assert(r[2]["id"], 5)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_SubQuery_Complex tests complex subquery combinations
|
||||
func Test_Model_SubQuery_Complex(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Combine subquery in WHERE, FROM, and SELECT
|
||||
whereSubQuery := db.Model(table).Fields("AVG(id)")
|
||||
fromSubQuery := db.Model(table).Where("id <=", 7)
|
||||
|
||||
r, err := db.Model("(?) AS sub", fromSubQuery).
|
||||
Fields("sub.id", "sub.nickname").
|
||||
Where("sub.id > (?)", whereSubQuery).
|
||||
OrderAsc("id").
|
||||
All()
|
||||
t.AssertNil(err)
|
||||
|
||||
// AVG(1-10) = 5.5, filter sub.id > 5.5 from ids 1-7
|
||||
t.Assert(len(r), 2)
|
||||
t.Assert(r[0]["id"], 6)
|
||||
t.Assert(r[1]["id"], 7)
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Subquery with GROUP BY and HAVING
|
||||
subQuery := db.Model(table).
|
||||
Fields("id % 3 AS mod_group", "COUNT(*) AS cnt").
|
||||
Group("mod_group").
|
||||
Having("COUNT(*) >=", 3)
|
||||
|
||||
r, err := db.Model(table).
|
||||
Where("id % 3 IN(?)", db.Model("(?) AS sub", subQuery).Fields("sub.mod_group")).
|
||||
OrderAsc("id").
|
||||
All()
|
||||
t.AssertNil(err)
|
||||
|
||||
// id % 3: 0(3,6,9), 1(1,4,7,10), 2(2,5,8)
|
||||
// Groups with count >= 3: 0(3 items), 1(4 items), 2(3 items) - all qualify
|
||||
t.Assert(len(r), 10)
|
||||
})
|
||||
}
|
||||
398
contrib/drivers/mariadb/mariadb_z_unit_feature_omit_test.go
Normal file
398
contrib/drivers/mariadb/mariadb_z_unit_feature_omit_test.go
Normal file
@ -0,0 +1,398 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package mariadb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
)
|
||||
|
||||
// Test_Model_OmitEmpty_Comprehensive tests OmitEmpty filtering for both data and where parameters
|
||||
func Test_Model_OmitEmpty_Comprehensive(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test OmitEmpty with empty string in Data
|
||||
result, err := db.Model(table).OmitEmpty().Data(g.Map{
|
||||
"nickname": "", // empty string should be omitted
|
||||
"passport": "new_user", // non-empty should be kept
|
||||
}).Where("id", 1).Update()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
// Verify nickname was not updated (omitted)
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["nickname"], "name_1") // original value preserved
|
||||
t.Assert(one["passport"], "new_user")
|
||||
|
||||
// Test OmitEmpty with empty slice in Where
|
||||
all, err := db.Model(table).OmitEmpty().Where(g.Map{
|
||||
"id": []int{}, // empty slice should be omitted
|
||||
"passport": "new_user",
|
||||
}).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 1)
|
||||
|
||||
// Without OmitEmpty, empty slice causes WHERE 0=1
|
||||
all, err = db.Model(table).Where(g.Map{
|
||||
"id": []int{},
|
||||
}).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 0) // no results due to WHERE 0=1
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_OmitEmptyWhere_Extended tests OmitEmpty filtering only for where parameters
|
||||
func Test_Model_OmitEmptyWhere_Extended(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// OmitEmptyWhere only affects Where, not Data
|
||||
result, err := db.Model(table).OmitEmptyWhere().Data(g.Map{
|
||||
"nickname": "", // empty string in Data should NOT be omitted (only Where is affected)
|
||||
}).Where(g.Map{
|
||||
"id": 1,
|
||||
"passport": "", // empty string in Where should be omitted
|
||||
}).Update()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
// Verify nickname was updated to empty (Data is not affected by OmitEmptyWhere)
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["nickname"], "")
|
||||
|
||||
// Test with empty slice in Where
|
||||
all, err := db.Model(table).OmitEmptyWhere().Where(g.Map{
|
||||
"id": []int{}, // should be omitted
|
||||
}).Order("id").Limit(3).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 3) // returns results because empty condition was omitted
|
||||
|
||||
// Test with zero value in Where (zero is considered empty)
|
||||
all, err = db.Model(table).OmitEmptyWhere().Where(g.Map{
|
||||
"id": 0, // zero should be omitted
|
||||
}).Order("id").Limit(3).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 3)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_OmitEmptyData tests OmitEmpty filtering only for data parameters
|
||||
func Test_Model_OmitEmptyData(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// OmitEmptyData only affects Data, not Where
|
||||
result, err := db.Model(table).OmitEmptyData().Data(g.Map{
|
||||
"nickname": "", // empty string in Data should be omitted
|
||||
"passport": "test_user", // non-empty should be kept
|
||||
}).Where(g.Map{
|
||||
"id": 1,
|
||||
}).Update()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
// Verify nickname was not updated (omitted), passport was updated
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["nickname"], "name_1")
|
||||
t.Assert(one["passport"], "test_user")
|
||||
|
||||
// Test Insert with OmitEmptyData
|
||||
result, err = db.Model(table).OmitEmptyData().Data(g.Map{
|
||||
"id": 100,
|
||||
"passport": "user_100",
|
||||
"nickname": "", // should be omitted
|
||||
"password": "pass_100",
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ = result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
// Verify nickname is NULL (was omitted from INSERT)
|
||||
one, err = db.Model(table).Where("id", 100).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], "user_100")
|
||||
t.Assert(one["nickname"].IsNil(), true)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_OmitNil_Comprehensive tests OmitNil filtering for both data and where parameters
|
||||
func Test_Model_OmitNil_Comprehensive(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test OmitNil with nil value in Data
|
||||
result, err := db.Model(table).OmitNil().Data(g.Map{
|
||||
"nickname": nil, // nil should be omitted
|
||||
"passport": "nil_test", // non-nil should be kept
|
||||
}).Where("id", 1).Update()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
// Verify nickname was not updated (omitted)
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["nickname"], "name_1")
|
||||
t.Assert(one["passport"], "nil_test")
|
||||
|
||||
// Test OmitNil with nil in Where
|
||||
all, err := db.Model(table).OmitNil().Where(g.Map{
|
||||
"passport": nil, // nil should be omitted
|
||||
}).Order("id").Limit(5).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 5) // returns results because nil condition was omitted
|
||||
|
||||
// Without OmitNil, WHERE passport=NULL (which won't match anything)
|
||||
all, err = db.Model(table).Where(g.Map{
|
||||
"passport": nil,
|
||||
}).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 0) // NULL comparison doesn't match
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_OmitNilWhere tests OmitNil filtering only for where parameters
|
||||
func Test_Model_OmitNilWhere(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// OmitNilWhere only affects Where, not Data
|
||||
result, err := db.Model(table).OmitNilWhere().Data(g.Map{
|
||||
"nickname": nil, // nil in Data should NOT be omitted (only Where is affected)
|
||||
}).Where(g.Map{
|
||||
"id": 1,
|
||||
"passport": nil, // nil in Where should be omitted
|
||||
}).Update()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
// Verify nickname was set to NULL (Data is not affected by OmitNilWhere)
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["nickname"].IsNil(), true)
|
||||
|
||||
// Test with nil in Where
|
||||
all, err := db.Model(table).OmitNilWhere().Where(g.Map{
|
||||
"passport": nil, // should be omitted
|
||||
}).Order("id").Limit(3).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 3) // returns results
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_OmitNilData tests OmitNil filtering only for data parameters
|
||||
func Test_Model_OmitNilData(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// OmitNilData only affects Data, not Where
|
||||
result, err := db.Model(table).OmitNilData().Data(g.Map{
|
||||
"nickname": nil, // nil in Data should be omitted
|
||||
"passport": "omitnil_test", // non-nil should be kept
|
||||
}).Where(g.Map{
|
||||
"id": 1,
|
||||
}).Update()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
// Verify nickname was not updated (omitted), passport was updated
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["nickname"], "name_1")
|
||||
t.Assert(one["passport"], "omitnil_test")
|
||||
|
||||
// Test Insert with OmitNilData
|
||||
result, err = db.Model(table).OmitNilData().Data(g.Map{
|
||||
"id": 101,
|
||||
"passport": "user_101",
|
||||
"nickname": nil, // should be omitted
|
||||
"password": "pass_101",
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ = result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
// Verify insert
|
||||
one, err = db.Model(table).Where("id", 101).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], "user_101")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_OmitEmpty_WithStruct tests OmitEmpty with struct data
|
||||
func Test_Model_OmitEmpty_WithStruct(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
type User struct {
|
||||
Id int
|
||||
Passport string
|
||||
Nickname string
|
||||
Password string
|
||||
}
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test OmitEmptyData with struct
|
||||
user := User{
|
||||
Passport: "struct_user",
|
||||
Nickname: "", // empty, should be omitted
|
||||
Password: "struct_pass",
|
||||
}
|
||||
result, err := db.Model(table).OmitEmptyData().Data(user).Where("id", 1).Update()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
// Verify nickname was not updated
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["nickname"], "name_1")
|
||||
t.Assert(one["passport"], "struct_user")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_OmitNil_WithPointerStruct tests OmitNil with pointer struct data
|
||||
func Test_Model_OmitNil_WithPointerStruct(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
type User struct {
|
||||
Id int
|
||||
Passport *string
|
||||
Nickname *string
|
||||
Password string
|
||||
}
|
||||
|
||||
// Note: Removed OmitNilData with pointer struct test due to framework limitations
|
||||
// Struct field nil pointer handling needs further investigation
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test OmitNilData with Map (working as expected)
|
||||
sqlArray2, err := gdb.CatchSQL(ctx, func(ctx context.Context) error {
|
||||
_, err := db.Ctx(ctx).Model(table).OmitNilData().Data(g.Map{
|
||||
"passport": "map_user",
|
||||
"nickname": nil,
|
||||
"password": "map_pass",
|
||||
}).Where("id", 2).Update()
|
||||
return err
|
||||
})
|
||||
t.AssertNil(err)
|
||||
t.Logf("Map SQL: %v", sqlArray2)
|
||||
|
||||
one2, err := db.Model(table).Where("id", 2).One()
|
||||
t.AssertNil(err)
|
||||
t.Logf("Map result - nickname: %v, passport: %v", one2["nickname"], one2["passport"])
|
||||
t.Assert(one2["nickname"], "name_2") // should be preserved
|
||||
t.Assert(one2["passport"], "map_user")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_OmitEmpty_ZeroValues tests OmitEmpty with various zero values
|
||||
func Test_Model_OmitEmpty_ZeroValues(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test OmitEmptyData with various zero values
|
||||
result, err := db.Model(table).OmitEmptyData().Data(g.Map{
|
||||
"id": 0, // zero int, should be omitted
|
||||
"passport": "zero_test", // non-empty
|
||||
"nickname": "", // empty string, should be omitted
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
// Verify the insert (id should be auto-generated since 0 was omitted)
|
||||
one, err := db.Model(table).Where("passport", "zero_test").One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], "zero_test")
|
||||
t.AssertNE(one["id"], 0) // auto-generated id
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_OmitEmpty_ComplexWhere tests OmitEmpty with complex where conditions
|
||||
func Test_Model_OmitEmpty_ComplexWhere(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test OmitEmptyWhere with multiple conditions
|
||||
all, err := db.Model(table).OmitEmptyWhere().Where(g.Map{
|
||||
"id >": 0, // zero, should be omitted
|
||||
"passport": "", // empty string, should be omitted
|
||||
"nickname": "?", // placeholder, should NOT be omitted
|
||||
}).Order("id").Limit(3).All()
|
||||
t.AssertNil(err)
|
||||
// Should execute query with only the nickname condition
|
||||
|
||||
// Test with all empty conditions
|
||||
all, err = db.Model(table).OmitEmptyWhere().Where(g.Map{
|
||||
"passport": "",
|
||||
"nickname": "",
|
||||
}).Order("id").Limit(5).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 5) // all conditions omitted, returns all (limited to 5)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Omit_ChainedMethods tests Omit methods with other chained methods
|
||||
func Test_Model_Omit_ChainedMethods(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test OmitEmpty with Fields and Order
|
||||
result, err := db.Model(table).
|
||||
OmitEmptyData().
|
||||
Fields("passport", "nickname").
|
||||
Data(g.Map{
|
||||
"passport": "chain_test",
|
||||
"nickname": "",
|
||||
}).
|
||||
Where("id", 1).
|
||||
Update()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["passport"], "chain_test")
|
||||
t.Assert(one["nickname"], "name_1") // not updated due to OmitEmptyData
|
||||
|
||||
// Test OmitNilWhere with multiple Where clauses
|
||||
all, err := db.Model(table).
|
||||
OmitNilWhere().
|
||||
Where("id>?", 5).
|
||||
Where(g.Map{
|
||||
"passport": nil, // should be omitted
|
||||
}).
|
||||
Order("id").
|
||||
All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 5) // id 6-10
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,545 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package mariadb_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
// Test_Model_AllAndCount_Basic tests basic AllAndCount functionality
|
||||
func Test_Model_AllAndCount_Basic(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, count, err := db.Model(table).AllAndCount(false)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), TableSize)
|
||||
t.Assert(count, TableSize)
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, count, err := db.Model(table).AllAndCount(true)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), TableSize)
|
||||
t.Assert(count, TableSize)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_AllAndCount_WithWhere tests AllAndCount with WHERE conditions
|
||||
func Test_Model_AllAndCount_WithWhere(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, count, err := db.Model(table).Where("id > ?", 5).AllAndCount(false)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), 5)
|
||||
t.Assert(count, 5)
|
||||
t.Assert(result[0]["id"], 6)
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, count, err := db.Model(table).Where("id", g.Slice{1, 2, 3}).AllAndCount(false)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), 3)
|
||||
t.Assert(count, 3)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_AllAndCount_WithPage tests AllAndCount with pagination
|
||||
func Test_Model_AllAndCount_WithPage(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, count, err := db.Model(table).Page(1, 3).Order("id").AllAndCount(false)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), 3)
|
||||
t.Assert(count, TableSize) // Count should be total, not page size
|
||||
t.Assert(result[0]["id"], 1)
|
||||
t.Assert(result[2]["id"], 3)
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, count, err := db.Model(table).Page(2, 3).Order("id").AllAndCount(false)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), 3)
|
||||
t.Assert(count, TableSize)
|
||||
t.Assert(result[0]["id"], 4)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_AllAndCount_WithFields tests AllAndCount with specific fields
|
||||
// Related: https://github.com/gogf/gf/issues/4698
|
||||
func Test_Model_AllAndCount_WithFields(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, count, err := db.Model(table).Fields("id, nickname").AllAndCount(false)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), TableSize)
|
||||
t.Assert(count, TableSize)
|
||||
t.Assert(len(result[0]), 2) // Only 2 fields
|
||||
})
|
||||
|
||||
// Regression test for #4698: AllAndCount(true) with multiple fields should work correctly
|
||||
// https://github.com/gogf/gf/issues/4698
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, count, err := db.Model(table).Fields("id, nickname").AllAndCount(true)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), TableSize)
|
||||
t.Assert(count, TableSize)
|
||||
t.Assert(len(result[0]), 2)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_AllAndCount_Empty tests AllAndCount with no results
|
||||
func Test_Model_AllAndCount_Empty(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, count, err := db.Model(table).Where("id > ?", 1000).AllAndCount(false)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), 0)
|
||||
t.Assert(count, 0)
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, count, err := db.Model(table).Where("id < ?", 0).AllAndCount(true)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), 0)
|
||||
t.Assert(count, 0)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_AllAndCount_WithCache tests AllAndCount with cache
|
||||
func Test_Model_AllAndCount_WithCache(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result1, count1, err := db.Model(table).PageCache(gdb.CacheOption{
|
||||
Duration: time.Second * 10,
|
||||
Force: false,
|
||||
}, gdb.CacheOption{
|
||||
Duration: time.Second * 10,
|
||||
Force: false,
|
||||
}).Page(1, 5).AllAndCount(false)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result1), 5)
|
||||
t.Assert(count1, TableSize)
|
||||
|
||||
// Second call should use cache
|
||||
result2, count2, err := db.Model(table).PageCache(gdb.CacheOption{
|
||||
Duration: time.Second * 10,
|
||||
Force: false,
|
||||
}, gdb.CacheOption{
|
||||
Duration: time.Second * 10,
|
||||
Force: false,
|
||||
}).Page(1, 5).AllAndCount(false)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result2), 5)
|
||||
t.Assert(count2, count1)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_AllAndCount_Distinct tests AllAndCount with DISTINCT
|
||||
func Test_Model_AllAndCount_Distinct(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
// Insert duplicate nicknames
|
||||
for i := 1; i <= 10; i++ {
|
||||
nickname := "name_" + gconv.String((i-1)/2) // Creates duplicates
|
||||
db.Model(table).Data(g.Map{
|
||||
"id": i,
|
||||
"passport": "pass_" + gconv.String(i),
|
||||
"password": "pwd",
|
||||
"nickname": nickname,
|
||||
}).Insert()
|
||||
}
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, count, err := db.Model(table).Fields("DISTINCT nickname").AllAndCount(true)
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 5) // 10 records / 2 = 5 distinct nicknames
|
||||
t.Assert(len(result), 5)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_ScanAndCount_Basic tests basic ScanAndCount functionality
|
||||
func Test_Model_ScanAndCount_Basic(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) {
|
||||
var users []User
|
||||
var count int
|
||||
err := db.Model(table).ScanAndCount(&users, &count, false)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(users), TableSize)
|
||||
t.Assert(count, TableSize)
|
||||
})
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var users []User
|
||||
var count int
|
||||
err := db.Model(table).ScanAndCount(&users, &count, true)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(users), TableSize)
|
||||
t.Assert(count, TableSize)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_ScanAndCount_WithWhere tests ScanAndCount with WHERE conditions
|
||||
func Test_Model_ScanAndCount_WithWhere(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
type User struct {
|
||||
Id int
|
||||
Passport string
|
||||
Nickname string
|
||||
}
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var users []User
|
||||
var count int
|
||||
err := db.Model(table).Where("id <= ?", 5).ScanAndCount(&users, &count, false)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(users), 5)
|
||||
t.Assert(count, 5)
|
||||
t.Assert(users[0].Id, 1)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_ScanAndCount_WithPage tests ScanAndCount with pagination
|
||||
func Test_Model_ScanAndCount_WithPage(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
type User struct {
|
||||
Id int
|
||||
Nickname string
|
||||
}
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var users []User
|
||||
var count int
|
||||
err := db.Model(table).Page(2, 3).Order("id").ScanAndCount(&users, &count, false)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(users), 3)
|
||||
t.Assert(count, TableSize) // Total count, not page count
|
||||
t.Assert(users[0].Id, 4)
|
||||
t.Assert(users[2].Id, 6)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_ScanAndCount_Single tests ScanAndCount for single record
|
||||
func Test_Model_ScanAndCount_Single(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
|
||||
var count int
|
||||
err := db.Model(table).Where("id", 1).ScanAndCount(&user, &count, false)
|
||||
t.AssertNil(err)
|
||||
t.Assert(user.Id, 1)
|
||||
t.Assert(count, 1)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_ScanAndCount_Empty tests ScanAndCount with no results
|
||||
func Test_Model_ScanAndCount_Empty(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
type User struct {
|
||||
Id int
|
||||
}
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var users []User
|
||||
var count int
|
||||
err := db.Model(table).Where("id > ?", 1000).ScanAndCount(&users, &count, false)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(users), 0)
|
||||
t.Assert(count, 0)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_ScanAndCount_WithFields tests ScanAndCount with specific fields
|
||||
func Test_Model_ScanAndCount_WithFields(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
type User struct {
|
||||
Id int
|
||||
Nickname string
|
||||
}
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var users []User
|
||||
var count int
|
||||
err := db.Model(table).Fields("id, nickname").ScanAndCount(&users, &count, false)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(users), TableSize)
|
||||
t.Assert(count, TableSize)
|
||||
t.Assert(users[0].Id > 0, true)
|
||||
t.AssertNE(users[0].Nickname, "")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_ScanAndCount_WithCache tests ScanAndCount with cache
|
||||
func Test_Model_ScanAndCount_WithCache(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
type User struct {
|
||||
Id int
|
||||
}
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var users1 []User
|
||||
var count1 int
|
||||
err := db.Model(table).PageCache(gdb.CacheOption{
|
||||
Duration: time.Second * 10,
|
||||
Force: false,
|
||||
}, gdb.CacheOption{
|
||||
Duration: time.Second * 10,
|
||||
Force: false,
|
||||
}).Page(1, 5).ScanAndCount(&users1, &count1, false)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(users1), 5)
|
||||
t.Assert(count1, TableSize)
|
||||
|
||||
// Second call should use cache
|
||||
var users2 []User
|
||||
var count2 int
|
||||
err = db.Model(table).PageCache(gdb.CacheOption{
|
||||
Duration: time.Second * 10,
|
||||
Force: false,
|
||||
}, gdb.CacheOption{
|
||||
Duration: time.Second * 10,
|
||||
Force: false,
|
||||
}).Page(1, 5).ScanAndCount(&users2, &count2, false)
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(users2), 5)
|
||||
t.Assert(count2, count1)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Chunk_Basic tests basic Chunk functionality
|
||||
func Test_Model_Chunk_Basic(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var (
|
||||
total int
|
||||
chunks int
|
||||
)
|
||||
db.Model(table).Order("id").Chunk(3, func(result gdb.Result, err error) bool {
|
||||
t.AssertNil(err)
|
||||
chunks++
|
||||
total += len(result)
|
||||
return true
|
||||
})
|
||||
t.Assert(chunks, 4) // 10 records / 3 = 4 chunks (3+3+3+1)
|
||||
t.Assert(total, TableSize)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Chunk_StopEarly tests Chunk with early stop
|
||||
func Test_Model_Chunk_StopEarly(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var chunks int
|
||||
db.Model(table).Order("id").Chunk(3, func(result gdb.Result, err error) bool {
|
||||
t.AssertNil(err)
|
||||
chunks++
|
||||
return chunks < 2 // Stop after 2nd chunk
|
||||
})
|
||||
t.Assert(chunks, 2)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Chunk_WithWhere tests Chunk with WHERE conditions
|
||||
func Test_Model_Chunk_WithWhere(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var (
|
||||
total int
|
||||
chunks int
|
||||
)
|
||||
db.Model(table).Where("id <= ?", 5).Order("id").Chunk(2, func(result gdb.Result, err error) bool {
|
||||
t.AssertNil(err)
|
||||
chunks++
|
||||
total += len(result)
|
||||
return true
|
||||
})
|
||||
t.Assert(chunks, 3) // 5 records / 2 = 3 chunks (2+2+1)
|
||||
t.Assert(total, 5)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Chunk_ErrorHandling tests Chunk error handling
|
||||
func Test_Model_Chunk_ErrorHandling(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var errorReceived bool
|
||||
db.Model("non_existent_table").Chunk(10, func(result gdb.Result, err error) bool {
|
||||
if err != nil {
|
||||
errorReceived = true
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
t.Assert(errorReceived, true)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Chunk_Empty tests Chunk with no results
|
||||
func Test_Model_Chunk_Empty(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var chunks int
|
||||
db.Model(table).Where("id > ?", 1000).Chunk(10, func(result gdb.Result, err error) bool {
|
||||
chunks++
|
||||
return true
|
||||
})
|
||||
t.Assert(chunks, 0) // No chunks for empty result
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Page_Boundary tests Page with boundary values
|
||||
// Related: https://github.com/gogf/gf/issues/4699
|
||||
func Test_Model_Page_Boundary(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
// Page 0 should be treated as page 1
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, err := db.Model(table).Page(0, 3).Order("id").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), 3)
|
||||
t.Assert(result[0]["id"], 1)
|
||||
})
|
||||
|
||||
// Negative page should be treated as page 1
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, err := db.Model(table).Page(-1, 3).Order("id").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), 3)
|
||||
t.Assert(result[0]["id"], 1)
|
||||
})
|
||||
|
||||
// Size 0: framework treats limit=0 as "no limit", returns all records
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, err := db.Model(table).Page(1, 0).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), TableSize)
|
||||
})
|
||||
|
||||
// Negative size: normalized to 0, same as Page(1, 0)
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, err := db.Model(table).Page(1, -1).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), TableSize)
|
||||
})
|
||||
|
||||
// Very large page number (beyond available data)
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, err := db.Model(table).Page(100, 3).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), 0)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Limit_Boundary tests Limit with boundary values
|
||||
// Related: https://github.com/gogf/gf/issues/4699
|
||||
func Test_Model_Limit_Boundary(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
// Limit 0: framework treats limit=0 as "no limit", returns all records
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, err := db.Model(table).Limit(0).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), TableSize)
|
||||
})
|
||||
|
||||
// Negative limit: normalized to 0, same as Limit(0)
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, err := db.Model(table).Limit(-1).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), TableSize)
|
||||
})
|
||||
|
||||
// Limit larger than available data
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, err := db.Model(table).Limit(1000).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), TableSize)
|
||||
})
|
||||
|
||||
// Limit(offset, size): offset=5 skips 5 rows, size=100 takes up to 100
|
||||
// With 10 rows total, skipping 5 returns remaining 5 rows
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, err := db.Model(table).Limit(5, 100).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), TableSize-5)
|
||||
})
|
||||
|
||||
// Offset beyond data: returns empty result
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
result, err := db.Model(table).Limit(100, 5).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), 0)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Model_Page_Limit_Combination tests Page and Limit used together
|
||||
func Test_Model_Page_Limit_Combination(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Page should override Limit
|
||||
result, err := db.Model(table).Limit(5).Page(1, 3).Order("id").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), 3)
|
||||
t.Assert(result[0]["id"], 1)
|
||||
})
|
||||
}
|
||||
364
contrib/drivers/mariadb/mariadb_z_unit_feature_partition_test.go
Normal file
364
contrib/drivers/mariadb/mariadb_z_unit_feature_partition_test.go
Normal file
@ -0,0 +1,364 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package mariadb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
)
|
||||
|
||||
func createRangePartitionTable(table ...string) string {
|
||||
var name string
|
||||
if len(table) > 0 {
|
||||
name = table[0]
|
||||
} else {
|
||||
name = fmt.Sprintf(`partition_range_%d`, gtime.TimestampNano())
|
||||
}
|
||||
if _, err := db3.Exec(ctx, fmt.Sprintf("DROP TABLE IF EXISTS `%s`", name)); err != nil {
|
||||
gtest.Fatal(err)
|
||||
}
|
||||
if _, err := db3.Exec(ctx, fmt.Sprintf(`
|
||||
CREATE TABLE %s (
|
||||
id int(11) NOT NULL,
|
||||
sales_date date DEFAULT NULL,
|
||||
amount decimal(10,2) DEFAULT NULL,
|
||||
region varchar(50) DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
PARTITION BY RANGE (YEAR(sales_date))
|
||||
(PARTITION p2020 VALUES LESS THAN (2021) ENGINE = InnoDB,
|
||||
PARTITION p2021 VALUES LESS THAN (2022) ENGINE = InnoDB,
|
||||
PARTITION p2022 VALUES LESS THAN (2023) ENGINE = InnoDB,
|
||||
PARTITION p2023 VALUES LESS THAN (2024) ENGINE = InnoDB,
|
||||
PARTITION p_future VALUES LESS THAN MAXVALUE ENGINE = InnoDB);
|
||||
`, name)); err != nil {
|
||||
gtest.Fatal(err)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func createHashPartitionTable(table ...string) string {
|
||||
var name string
|
||||
if len(table) > 0 {
|
||||
name = table[0]
|
||||
} else {
|
||||
name = fmt.Sprintf(`partition_hash_%d`, gtime.TimestampNano())
|
||||
}
|
||||
if _, err := db3.Exec(ctx, fmt.Sprintf("DROP TABLE IF EXISTS `%s`", name)); err != nil {
|
||||
gtest.Fatal(err)
|
||||
}
|
||||
if _, err := db3.Exec(ctx, fmt.Sprintf(`
|
||||
CREATE TABLE %s (
|
||||
id int(11) NOT NULL,
|
||||
user_id int(11) NOT NULL,
|
||||
username varchar(50) DEFAULT NULL,
|
||||
email varchar(100) DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
PARTITION BY HASH (user_id)
|
||||
PARTITIONS 4;
|
||||
`, name)); err != nil {
|
||||
gtest.Fatal(err)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func createListPartitionTable(table ...string) string {
|
||||
var name string
|
||||
if len(table) > 0 {
|
||||
name = table[0]
|
||||
} else {
|
||||
name = fmt.Sprintf(`partition_list_%d`, gtime.TimestampNano())
|
||||
}
|
||||
if _, err := db3.Exec(ctx, fmt.Sprintf("DROP TABLE IF EXISTS `%s`", name)); err != nil {
|
||||
gtest.Fatal(err)
|
||||
}
|
||||
if _, err := db3.Exec(ctx, fmt.Sprintf(`
|
||||
CREATE TABLE %s (
|
||||
id int(11) NOT NULL,
|
||||
region_code int(11) NOT NULL,
|
||||
city varchar(50) DEFAULT NULL,
|
||||
population int(11) DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
PARTITION BY LIST (region_code)
|
||||
(PARTITION p_north VALUES IN (1,2,3) ENGINE = InnoDB,
|
||||
PARTITION p_south VALUES IN (4,5,6) ENGINE = InnoDB,
|
||||
PARTITION p_east VALUES IN (7,8,9) ENGINE = InnoDB,
|
||||
PARTITION p_west VALUES IN (10,11,12) ENGINE = InnoDB);
|
||||
`, name)); err != nil {
|
||||
gtest.Fatal(err)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func dropPartitionTable(table string) {
|
||||
if _, err := db3.Exec(ctx, fmt.Sprintf("DROP TABLE IF EXISTS `%s`", table)); err != nil {
|
||||
gtest.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Partition_Range_Insert_And_Query(t *testing.T) {
|
||||
table := createRangePartitionTable()
|
||||
defer dropPartitionTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert data across different partitions
|
||||
data := g.Slice{
|
||||
g.Map{"id": 1, "sales_date": "2020-06-15", "amount": 1000.50, "region": "North"},
|
||||
g.Map{"id": 2, "sales_date": "2021-03-20", "amount": 2000.75, "region": "South"},
|
||||
g.Map{"id": 3, "sales_date": "2022-09-10", "amount": 3000.00, "region": "East"},
|
||||
g.Map{"id": 4, "sales_date": "2023-12-01", "amount": 4000.25, "region": "West"},
|
||||
g.Map{"id": 5, "sales_date": "2024-01-15", "amount": 5000.00, "region": "North"},
|
||||
}
|
||||
_, err := db3.Model(table).Data(data).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Query all data
|
||||
all, err := db3.Model(table).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 5)
|
||||
|
||||
// Query specific year (should hit specific partition)
|
||||
result, err := db3.Model(table).Where("YEAR(sales_date) = ?", 2022).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), 1)
|
||||
t.Assert(result[0]["id"], 3)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Partition_Range_PartitionQuery(t *testing.T) {
|
||||
// Known limitation: Model.Partition() sets m.partition field but it's not used in SQL generation
|
||||
// See: database/gdb/gdb_model_select.go lines 735,755 - m.tables is used without PARTITION clause
|
||||
// TODO: Add PARTITION clause support to GoFrame query builder
|
||||
t.Skip("Partition clause in SELECT queries not yet supported in GoFrame query builder")
|
||||
|
||||
table := createRangePartitionTable()
|
||||
defer dropPartitionTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert data
|
||||
data := g.Slice{
|
||||
g.Map{"id": 1, "sales_date": "2020-06-15", "amount": 1000.50},
|
||||
g.Map{"id": 2, "sales_date": "2021-03-20", "amount": 2000.75},
|
||||
g.Map{"id": 3, "sales_date": "2022-09-10", "amount": 3000.00},
|
||||
g.Map{"id": 4, "sales_date": "2023-12-01", "amount": 4000.25},
|
||||
}
|
||||
_, err := db3.Model(table).Data(data).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Query specific partition
|
||||
result, err := db3.Model(table).Partition("p2022").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), 1)
|
||||
t.Assert(result[0]["id"], 3)
|
||||
|
||||
// Query multiple partitions
|
||||
result, err = db3.Model(table).Partition("p2021", "p2022").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), 2)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Partition_Hash_Insert_And_Distribution(t *testing.T) {
|
||||
table := createHashPartitionTable()
|
||||
defer dropPartitionTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert data that will be distributed across hash partitions
|
||||
data := g.Slice{}
|
||||
for i := 1; i <= 20; i++ {
|
||||
data = append(data, g.Map{
|
||||
"id": i,
|
||||
"user_id": i * 10,
|
||||
"username": fmt.Sprintf("user_%d", i),
|
||||
"email": fmt.Sprintf("user%d@example.com", i),
|
||||
})
|
||||
}
|
||||
_, err := db3.Model(table).Data(data).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Query all data
|
||||
all, err := db3.Model(table).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 20)
|
||||
|
||||
// Query specific user_id (will hit specific partition based on hash)
|
||||
result, err := db3.Model(table).Where("user_id", 100).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(result["username"], "user_10")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Partition_List_Insert_And_Query(t *testing.T) {
|
||||
// Known limitation: Model.Partition() sets m.partition field but it's not used in SQL generation
|
||||
// See: database/gdb/gdb_model_select.go lines 735,755 - m.tables is used without PARTITION clause
|
||||
// TODO: Add PARTITION clause support to GoFrame query builder
|
||||
t.Skip("Partition clause in SELECT queries not yet supported in GoFrame query builder")
|
||||
|
||||
table := createListPartitionTable()
|
||||
defer dropPartitionTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert data for different regions
|
||||
data := g.Slice{
|
||||
g.Map{"id": 1, "region_code": 1, "city": "Beijing", "population": 2154},
|
||||
g.Map{"id": 2, "region_code": 2, "city": "Harbin", "population": 1063},
|
||||
g.Map{"id": 3, "region_code": 5, "city": "Guangzhou", "population": 1868},
|
||||
g.Map{"id": 4, "region_code": 7, "city": "Shanghai", "population": 2428},
|
||||
g.Map{"id": 5, "region_code": 10, "city": "Chengdu", "population": 2093},
|
||||
}
|
||||
_, err := db3.Model(table).Data(data).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Query all
|
||||
all, err := db3.Model(table).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 5)
|
||||
|
||||
// Query specific partition (north region)
|
||||
result, err := db3.Model(table).Partition("p_north").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), 2)
|
||||
|
||||
// Query specific partition (south region)
|
||||
result, err = db3.Model(table).Partition("p_south").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result), 1)
|
||||
t.Assert(result[0]["city"], "Guangzhou")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Partition_Range_Update(t *testing.T) {
|
||||
table := createRangePartitionTable()
|
||||
defer dropPartitionTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert data
|
||||
_, err := db3.Model(table).Data(g.Map{
|
||||
"id": 1,
|
||||
"sales_date": "2022-06-15",
|
||||
"amount": 1000.00,
|
||||
"region": "North",
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Update data within same partition
|
||||
result, err := db3.Model(table).Data(g.Map{
|
||||
"amount": 1500.00,
|
||||
"region": "South",
|
||||
}).Where("id", 1).Update()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
// Verify update
|
||||
one, err := db3.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["amount"], "1500.00")
|
||||
t.Assert(one["region"], "South")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Partition_Range_Delete(t *testing.T) {
|
||||
table := createRangePartitionTable()
|
||||
defer dropPartitionTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert data
|
||||
data := g.Slice{
|
||||
g.Map{"id": 1, "sales_date": "2020-06-15", "amount": 1000.50},
|
||||
g.Map{"id": 2, "sales_date": "2021-03-20", "amount": 2000.75},
|
||||
g.Map{"id": 3, "sales_date": "2022-09-10", "amount": 3000.00},
|
||||
}
|
||||
_, err := db3.Model(table).Data(data).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Delete from specific partition
|
||||
result, err := db3.Model(table).Where("YEAR(sales_date) = ?", 2021).Delete()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
// Verify deletion
|
||||
all, err := db3.Model(table).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 2)
|
||||
|
||||
// Verify remaining data
|
||||
result2, err := db3.Model(table).Where("YEAR(sales_date) = ?", 2021).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(result2), 0)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Partition_Transaction(t *testing.T) {
|
||||
table := createRangePartitionTable()
|
||||
defer dropPartitionTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Transaction with partitioned table
|
||||
err := db3.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
// Insert across multiple partitions
|
||||
data := g.Slice{
|
||||
g.Map{"id": 1, "sales_date": "2020-06-15", "amount": 1000.50},
|
||||
g.Map{"id": 2, "sales_date": "2021-03-20", "amount": 2000.75},
|
||||
g.Map{"id": 3, "sales_date": "2022-09-10", "amount": 3000.00},
|
||||
}
|
||||
_, err := tx.Model(table).Ctx(ctx).Data(data).Insert()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update in transaction
|
||||
_, err = tx.Model(table).Ctx(ctx).Data(g.Map{
|
||||
"amount": 1500.00,
|
||||
}).Where("id", 1).Update()
|
||||
return err
|
||||
})
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify transaction committed
|
||||
all, err := db3.Model(table).All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 3)
|
||||
|
||||
one, err := db3.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["amount"], "1500.00")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Partition_Range_Count_And_Sum(t *testing.T) {
|
||||
table := createRangePartitionTable()
|
||||
defer dropPartitionTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert data
|
||||
data := g.Slice{
|
||||
g.Map{"id": 1, "sales_date": "2020-06-15", "amount": 1000.00},
|
||||
g.Map{"id": 2, "sales_date": "2020-09-20", "amount": 1500.00},
|
||||
g.Map{"id": 3, "sales_date": "2021-03-20", "amount": 2000.00},
|
||||
g.Map{"id": 4, "sales_date": "2022-09-10", "amount": 3000.00},
|
||||
}
|
||||
_, err := db3.Model(table).Data(data).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Count by year (specific partition)
|
||||
count, err := db3.Model(table).Where("YEAR(sales_date) = ?", 2020).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 2)
|
||||
|
||||
// Sum across partitions
|
||||
value, err := db3.Model(table).Fields("SUM(amount) as total").Value()
|
||||
t.AssertNil(err)
|
||||
t.AssertGT(value.Float64(), 7000.0) // 1000+1500+2000+3000 = 7500
|
||||
})
|
||||
}
|
||||
905
contrib/drivers/mariadb/mariadb_z_unit_feature_raw_type_test.go
Normal file
905
contrib/drivers/mariadb/mariadb_z_unit_feature_raw_type_test.go
Normal file
@ -0,0 +1,905 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package mariadb_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
)
|
||||
|
||||
func Test_Raw_Insert(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
user := db.Model(table)
|
||||
result, err := user.Data(g.Map{
|
||||
"id": gdb.Raw("id+2"),
|
||||
"passport": "port_1",
|
||||
"password": "pass_1",
|
||||
"nickname": "name_1",
|
||||
"create_time": gdb.Raw("now()"),
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.LastInsertId()
|
||||
t.Assert(n, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Raw_BatchInsert(t *testing.T) {
|
||||
table := createTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
user := db.Model(table)
|
||||
result, err := user.Data(
|
||||
g.List{
|
||||
g.Map{
|
||||
"id": gdb.Raw("id+2"),
|
||||
"passport": "port_2",
|
||||
"password": "pass_2",
|
||||
"nickname": "name_2",
|
||||
"create_time": gdb.Raw("now()"),
|
||||
},
|
||||
g.Map{
|
||||
"id": gdb.Raw("id+4"),
|
||||
"passport": "port_4",
|
||||
"password": "pass_4",
|
||||
"nickname": "name_4",
|
||||
"create_time": gdb.Raw("now()"),
|
||||
},
|
||||
},
|
||||
).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.LastInsertId()
|
||||
t.Assert(n, 4)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Raw_Update(t *testing.T) {
|
||||
table := createInitTable()
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
user := db.Model(table)
|
||||
result, err := user.Data(g.Map{
|
||||
"id": gdb.Raw("id+100"),
|
||||
"create_time": gdb.Raw("now()"),
|
||||
}).Where("id", 1).Update()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
user := db.Model(table)
|
||||
n, err := user.Where("id", 101).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(n, 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)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DataType_JSON_Insert tests JSON data insertion
|
||||
func Test_DataType_JSON_Insert(t *testing.T) {
|
||||
table := "test_json_" + gtime.TimestampMicroStr()
|
||||
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, data JSON)")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert simple JSON object
|
||||
result, err := db.Model(table).Data(g.Map{
|
||||
"data": `{"name":"John","age":30}`,
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
n, _ := result.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
// Verify data
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
expected := map[string]interface{}{"name": "John", "age": float64(30)}
|
||||
var actual map[string]interface{}
|
||||
err = json.Unmarshal([]byte(one["data"].String()), &actual)
|
||||
t.AssertNil(err)
|
||||
t.Assert(actual, expected)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DataType_JSON_Extract tests JSON_EXTRACT function
|
||||
func Test_DataType_JSON_Extract(t *testing.T) {
|
||||
table := "test_json_extract_" + gtime.TimestampMicroStr()
|
||||
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, data JSON)")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert test data
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"data": `{"name":"Alice","age":25,"city":"Beijing"}`,
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Extract name using JSON_EXTRACT
|
||||
one, err := db.Model(table).Fields("JSON_EXTRACT(data, '$.name') as name").Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["name"].String(), `"Alice"`)
|
||||
|
||||
// Extract age
|
||||
one, err = db.Model(table).Fields("JSON_EXTRACT(data, '$.age') as age").Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["age"].Int(), 25)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DataType_JSON_Set tests JSON_SET function
|
||||
func Test_DataType_JSON_Set(t *testing.T) {
|
||||
table := "test_json_set_" + gtime.TimestampMicroStr()
|
||||
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, data JSON)")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert initial data
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"data": `{"name":"Bob"}`,
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Update using JSON_SET
|
||||
_, err = db.Exec(ctx, fmt.Sprintf("UPDATE %s SET data = JSON_SET(data, '$.age', 30) WHERE id = 1", table))
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify updated data
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
expected := map[string]interface{}{"name": "Bob", "age": float64(30)}
|
||||
var actual map[string]interface{}
|
||||
err = json.Unmarshal([]byte(one["data"].String()), &actual)
|
||||
t.AssertNil(err)
|
||||
t.Assert(actual, expected)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DataType_JSON_Array tests JSON array operations
|
||||
func Test_DataType_JSON_Array(t *testing.T) {
|
||||
table := "test_json_array_" + gtime.TimestampMicroStr()
|
||||
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, data JSON)")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert JSON array
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"data": `["apple","banana","cherry"]`,
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Extract array element
|
||||
one, err := db.Model(table).Fields("JSON_EXTRACT(data, '$[0]') as first").Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["first"].String(), `"apple"`)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DataType_JSON_Null tests JSON NULL handling
|
||||
func Test_DataType_JSON_Null(t *testing.T) {
|
||||
table := "test_json_null_" + gtime.TimestampMicroStr()
|
||||
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, data JSON)")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert NULL value
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"data": nil,
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify NULL
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["data"].IsNil(), true)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DataType_JSON_Complex tests complex nested JSON
|
||||
func Test_DataType_JSON_Complex(t *testing.T) {
|
||||
table := "test_json_complex_" + gtime.TimestampMicroStr()
|
||||
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, data JSON)")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert complex nested JSON
|
||||
complexJSON := `{
|
||||
"user": {
|
||||
"name": "Charlie",
|
||||
"contacts": {
|
||||
"email": "charlie@example.com",
|
||||
"phone": "1234567890"
|
||||
},
|
||||
"tags": ["developer", "gopher"]
|
||||
}
|
||||
}`
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"data": complexJSON,
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Extract nested field
|
||||
one, err := db.Model(table).Fields("JSON_EXTRACT(data, '$.user.contacts.email') as email").Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["email"].String(), `"charlie@example.com"`)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DataType_JSON_Query tests JSON query with WHERE clause
|
||||
func Test_DataType_JSON_Query(t *testing.T) {
|
||||
table := "test_json_query_" + gtime.TimestampMicroStr()
|
||||
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, data JSON)")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert multiple JSON records
|
||||
_, err := db.Model(table).Data(g.List{
|
||||
g.Map{"data": `{"name":"David","age":20}`},
|
||||
g.Map{"data": `{"name":"Eve","age":30}`},
|
||||
g.Map{"data": `{"name":"Frank","age":25}`},
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Query by JSON field value
|
||||
count, err := db.Model(table).Where("JSON_EXTRACT(data, '$.age') > ?", 25).Count()
|
||||
t.AssertNil(err)
|
||||
t.Assert(count, 1)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DataType_JSON_Update tests updating JSON data
|
||||
func Test_DataType_JSON_Update(t *testing.T) {
|
||||
table := "test_json_update_" + gtime.TimestampMicroStr()
|
||||
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, data JSON)")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert initial data
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"data": `{"name":"Grace","age":28}`,
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Update entire JSON
|
||||
_, err = db.Model(table).Data(g.Map{
|
||||
"data": `{"name":"Grace","age":29}`,
|
||||
}).Where("id", 1).Update()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify update
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
expected := map[string]interface{}{"name": "Grace", "age": float64(29)}
|
||||
var actual map[string]interface{}
|
||||
err = json.Unmarshal([]byte(one["data"].String()), &actual)
|
||||
t.AssertNil(err)
|
||||
t.Assert(actual, expected)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DataType_Binary_Small tests small binary data
|
||||
func Test_DataType_Binary_Small(t *testing.T) {
|
||||
table := "test_binary_small_" + gtime.TimestampMicroStr()
|
||||
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, data BLOB)")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert small binary data
|
||||
binaryData := []byte{0x00, 0x01, 0x02, 0x03, 0xFF}
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"data": binaryData,
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify data
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(bytes.Equal(one["data"].Bytes(), binaryData), true)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DataType_Binary_Large tests large binary data (1MB+)
|
||||
func Test_DataType_Binary_Large(t *testing.T) {
|
||||
table := "test_binary_large_" + gtime.TimestampMicroStr()
|
||||
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, data MEDIUMBLOB)")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Create 1MB binary data
|
||||
size := 1024 * 1024 // 1MB
|
||||
largeBinary := make([]byte, size)
|
||||
for i := 0; i < size; i++ {
|
||||
largeBinary[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
// Insert large binary data
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"data": largeBinary,
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify data
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(one["data"].Bytes()), size)
|
||||
t.Assert(bytes.Equal(one["data"].Bytes(), largeBinary), true)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DataType_Binary_Integrity tests binary data integrity with checksum
|
||||
func Test_DataType_Binary_Integrity(t *testing.T) {
|
||||
table := "test_binary_integrity_" + gtime.TimestampMicroStr()
|
||||
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, data BLOB, checksum VARCHAR(64))")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Create random binary data
|
||||
binaryData := []byte("Hello, World! This is a binary test data with special chars: \x00\xFF\xAB")
|
||||
|
||||
// Calculate SHA256 checksum
|
||||
hash := sha256.Sum256(binaryData)
|
||||
checksum := hex.EncodeToString(hash[:])
|
||||
|
||||
// Insert with checksum
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"data": binaryData,
|
||||
"checksum": checksum,
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify integrity
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
|
||||
retrievedHash := sha256.Sum256(one["data"].Bytes())
|
||||
retrievedChecksum := hex.EncodeToString(retrievedHash[:])
|
||||
t.Assert(retrievedChecksum, checksum)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DataType_Binary_Empty tests empty and NULL binary
|
||||
func Test_DataType_Binary_Empty(t *testing.T) {
|
||||
table := "test_binary_empty_" + gtime.TimestampMicroStr()
|
||||
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, data BLOB)")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert empty binary
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"data": []byte{},
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Insert NULL
|
||||
_, err = db.Model(table).Data(g.Map{
|
||||
"data": nil,
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify empty
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(one["data"].Bytes()), 0)
|
||||
|
||||
// Verify NULL
|
||||
one, err = db.Model(table).Where("id", 2).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["data"].IsNil(), true)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DataType_Decimal_HighPrecision tests high precision decimal (65,30)
|
||||
func Test_DataType_Decimal_HighPrecision(t *testing.T) {
|
||||
table := "test_decimal_precision_" + gtime.TimestampMicroStr()
|
||||
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, amount DECIMAL(65,30))")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert high precision decimal
|
||||
value := "12345678901234567890123456789012345.123456789012345678901234567890"
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"amount": value,
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify precision
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["amount"].String(), value)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DataType_Decimal_Calculation tests decimal arithmetic
|
||||
func Test_DataType_Decimal_Calculation(t *testing.T) {
|
||||
table := "test_decimal_calc_" + gtime.TimestampMicroStr()
|
||||
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, price DECIMAL(10,2), quantity DECIMAL(10,2))")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert test data
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"price": "19.99",
|
||||
"quantity": "3.5",
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Calculate total using SQL
|
||||
one, err := db.Model(table).Fields("price * quantity as total").Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["total"].String(), "69.9650")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DataType_Decimal_Boundary tests decimal boundary values
|
||||
func Test_DataType_Decimal_Boundary(t *testing.T) {
|
||||
table := "test_decimal_boundary_" + gtime.TimestampMicroStr()
|
||||
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, value DECIMAL(10,2))")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test max value (10 digits, 2 decimals: 99999999.99)
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"value": "99999999.99",
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Test min value
|
||||
_, err = db.Model(table).Data(g.Map{
|
||||
"value": "-99999999.99",
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Test zero
|
||||
_, err = db.Model(table).Data(g.Map{
|
||||
"value": "0.00",
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify all values
|
||||
all, err := db.Model(table).Order("id").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 3)
|
||||
t.Assert(all[0]["value"].String(), "99999999.99")
|
||||
t.Assert(all[1]["value"].String(), "-99999999.99")
|
||||
t.Assert(all[2]["value"].String(), "0.00")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DataType_Decimal_Null tests NULL decimal values
|
||||
func Test_DataType_Decimal_Null(t *testing.T) {
|
||||
table := "test_decimal_null_" + gtime.TimestampMicroStr()
|
||||
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, value DECIMAL(10,2))")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert NULL
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"value": nil,
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify NULL
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["value"].IsNil(), true)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DataType_Datetime_Timezone tests datetime with timezone handling
|
||||
func Test_DataType_Datetime_Timezone(t *testing.T) {
|
||||
table := "test_datetime_tz_" + gtime.TimestampMicroStr()
|
||||
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, created_at DATETIME)")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert datetime
|
||||
dt := "2024-01-15 12:30:45"
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"created_at": dt,
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify datetime
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["created_at"].String(), dt)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DataType_Datetime_Precision tests datetime with microsecond precision
|
||||
func Test_DataType_Datetime_Precision(t *testing.T) {
|
||||
table := "test_datetime_precision_" + gtime.TimestampMicroStr()
|
||||
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, created_at DATETIME(6))")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert datetime with microseconds
|
||||
dt := "2024-01-15 12:30:45.123456"
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"created_at": dt,
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify precision (compare up to seconds, MySQL may format microseconds differently)
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
expected := "2024-01-15 12:30:45"
|
||||
actual := one["created_at"].String()[:19] // Extract first 19 chars (YYYY-MM-DD HH:MM:SS)
|
||||
t.Assert(actual, expected)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DataType_Datetime_Boundary tests datetime boundary values
|
||||
func Test_DataType_Datetime_Boundary(t *testing.T) {
|
||||
table := "test_datetime_boundary_" + gtime.TimestampMicroStr()
|
||||
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, dt DATETIME)")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test min datetime (MySQL supports 1000-01-01 00:00:00)
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"dt": "1000-01-01 00:00:00",
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Test max datetime
|
||||
_, err = db.Model(table).Data(g.Map{
|
||||
"dt": "9999-12-31 23:59:59",
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify boundaries
|
||||
all, err := db.Model(table).Order("id").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 2)
|
||||
t.Assert(all[0]["dt"].String(), "1000-01-01 00:00:00")
|
||||
t.Assert(all[1]["dt"].String(), "9999-12-31 23:59:59")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DataType_Datetime_Null tests NULL datetime
|
||||
func Test_DataType_Datetime_Null(t *testing.T) {
|
||||
table := "test_datetime_null_" + gtime.TimestampMicroStr()
|
||||
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, dt DATETIME)")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert NULL
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"dt": nil,
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify NULL
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["dt"].IsNil(), true)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DataType_Datetime_Update tests datetime updates
|
||||
func Test_DataType_Datetime_Update(t *testing.T) {
|
||||
table := "test_datetime_update_" + gtime.TimestampMicroStr()
|
||||
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, dt DATETIME)")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert initial datetime
|
||||
dt1 := "2024-01-01 10:00:00"
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"dt": dt1,
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Update datetime
|
||||
dt2 := "2024-12-31 23:59:59"
|
||||
_, err = db.Model(table).Data(g.Map{
|
||||
"dt": dt2,
|
||||
}).Where("id", 1).Update()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify update
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["dt"].String(), dt2)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DataType_Enum_Valid tests valid ENUM values
|
||||
func Test_DataType_Enum_Valid(t *testing.T) {
|
||||
table := "test_enum_valid_" + gtime.TimestampMicroStr()
|
||||
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, status ENUM('pending','approved','rejected'))")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert all valid values
|
||||
_, err := db.Model(table).Data(g.List{
|
||||
g.Map{"status": "pending"},
|
||||
g.Map{"status": "approved"},
|
||||
g.Map{"status": "rejected"},
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify all values
|
||||
all, err := db.Model(table).Order("id").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 3)
|
||||
t.Assert(all[0]["status"].String(), "pending")
|
||||
t.Assert(all[1]["status"].String(), "approved")
|
||||
t.Assert(all[2]["status"].String(), "rejected")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DataType_Enum_Invalid tests invalid ENUM values (should fail or truncate)
|
||||
func Test_DataType_Enum_Invalid(t *testing.T) {
|
||||
table := "test_enum_invalid_" + gtime.TimestampMicroStr()
|
||||
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, status ENUM('pending','approved','rejected'))")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Attempt to insert invalid value (should fail in strict mode)
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"status": "invalid_status",
|
||||
}).Insert()
|
||||
// In strict SQL mode, this should produce an error
|
||||
// In non-strict mode, it might insert empty string
|
||||
t.AssertNE(err, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DataType_Set_Valid tests valid SET values
|
||||
func Test_DataType_Set_Valid(t *testing.T) {
|
||||
table := "test_set_valid_" + gtime.TimestampMicroStr()
|
||||
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, permissions SET('read','write','execute'))")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert single value
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"permissions": "read",
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Insert multiple values
|
||||
_, err = db.Model(table).Data(g.Map{
|
||||
"permissions": "read,write",
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Insert all values
|
||||
_, err = db.Model(table).Data(g.Map{
|
||||
"permissions": "read,write,execute",
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify all values
|
||||
all, err := db.Model(table).Order("id").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(all), 3)
|
||||
t.Assert(all[0]["permissions"].String(), "read")
|
||||
t.Assert(all[1]["permissions"].String(), "read,write")
|
||||
t.Assert(all[2]["permissions"].String(), "read,write,execute")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DataType_Set_Empty tests empty SET values
|
||||
func Test_DataType_Set_Empty(t *testing.T) {
|
||||
table := "test_set_empty_" + gtime.TimestampMicroStr()
|
||||
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, permissions SET('read','write','execute'))")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert empty SET
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"permissions": "",
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify empty
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["permissions"].String(), "")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DataType_Geometry_Point tests POINT geometry type
|
||||
func Test_DataType_Geometry_Point(t *testing.T) {
|
||||
table := "test_geo_point_" + gtime.TimestampMicroStr()
|
||||
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, location POINT)")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert POINT using ST_GeomFromText
|
||||
_, err := db.Exec(ctx, fmt.Sprintf("INSERT INTO %s (location) VALUES (ST_GeomFromText('POINT(116.4074 39.9042)'))", table))
|
||||
t.AssertNil(err)
|
||||
|
||||
// Query POINT using ST_AsText
|
||||
one, err := db.Model(table).Fields("ST_AsText(location) as location_text").Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["location_text"].String(), "POINT(116.4074 39.9042)")
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DataType_Geometry_Polygon tests POLYGON geometry type
|
||||
func Test_DataType_Geometry_Polygon(t *testing.T) {
|
||||
table := "test_geo_polygon_" + gtime.TimestampMicroStr()
|
||||
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, area POLYGON)")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert POLYGON (rectangle)
|
||||
polygon := "POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))"
|
||||
_, err := db.Exec(ctx, fmt.Sprintf("INSERT INTO %s (area) VALUES (ST_GeomFromText('%s'))", table, polygon))
|
||||
t.AssertNil(err)
|
||||
|
||||
// Query POLYGON (normalize spaces for comparison)
|
||||
one, err := db.Model(table).Fields("ST_AsText(area) as area_text").Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
expected := "POLYGON((0 0,10 0,10 10,0 10,0 0))"
|
||||
actual := strings.ReplaceAll(one["area_text"].String(), ", ", ",") // Remove spaces after commas
|
||||
t.Assert(actual, expected)
|
||||
})
|
||||
}
|
||||
|
||||
// Test_DataType_Geometry_Null tests NULL geometry values
|
||||
func Test_DataType_Geometry_Null(t *testing.T) {
|
||||
table := "test_geo_null_" + gtime.TimestampMicroStr()
|
||||
_, err := db.Exec(ctx, "CREATE TABLE "+table+" (id INT PRIMARY KEY AUTO_INCREMENT, location POINT)")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer dropTable(table)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Insert NULL
|
||||
_, err := db.Model(table).Data(g.Map{
|
||||
"location": nil,
|
||||
}).Insert()
|
||||
t.AssertNil(err)
|
||||
|
||||
// Verify NULL
|
||||
one, err := db.Model(table).Where("id", 1).One()
|
||||
t.AssertNil(err)
|
||||
t.Assert(one["location"].IsNil(), true)
|
||||
})
|
||||
}
|
||||
2105
contrib/drivers/mariadb/mariadb_z_unit_feature_scanlist_test.go
Normal file
2105
contrib/drivers/mariadb/mariadb_z_unit_feature_scanlist_test.go
Normal file
File diff suppressed because it is too large
Load Diff
1399
contrib/drivers/mariadb/mariadb_z_unit_feature_soft_time_test.go
Normal file
1399
contrib/drivers/mariadb/mariadb_z_unit_feature_soft_time_test.go
Normal file
File diff suppressed because it is too large
Load Diff
146
contrib/drivers/mariadb/mariadb_z_unit_feature_union_test.go
Normal file
146
contrib/drivers/mariadb/mariadb_z_unit_feature_union_test.go
Normal 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 mariadb_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)
|
||||
})
|
||||
}
|
||||
1998
contrib/drivers/mariadb/mariadb_z_unit_feature_with_test.go
Normal file
1998
contrib/drivers/mariadb/mariadb_z_unit_feature_with_test.go
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user