feat(ui): add relationship management features in schema editor
- Implement functionality to create, update, delete, and view relationships between tables. - Introduce new UI screens for managing relationships, including forms for adding and editing relationships. - Enhance table editor with navigation to relationship management. - Ensure relationships are displayed in a structured table format for better usability.
This commit is contained in:
196
GODOC.md
Normal file
196
GODOC.md
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
# RelSpec API Documentation (godoc)
|
||||||
|
|
||||||
|
This document explains how to access and use the RelSpec API documentation.
|
||||||
|
|
||||||
|
## Viewing Documentation Locally
|
||||||
|
|
||||||
|
### Using `go doc` Command Line
|
||||||
|
|
||||||
|
View package documentation:
|
||||||
|
```bash
|
||||||
|
# Main package overview
|
||||||
|
go doc
|
||||||
|
|
||||||
|
# Specific package
|
||||||
|
go doc ./pkg/models
|
||||||
|
go doc ./pkg/readers
|
||||||
|
go doc ./pkg/writers
|
||||||
|
go doc ./pkg/ui
|
||||||
|
|
||||||
|
# Specific type or function
|
||||||
|
go doc ./pkg/models Database
|
||||||
|
go doc ./pkg/readers Reader
|
||||||
|
go doc ./pkg/writers Writer
|
||||||
|
```
|
||||||
|
|
||||||
|
View all documentation for a package:
|
||||||
|
```bash
|
||||||
|
go doc -all ./pkg/models
|
||||||
|
go doc -all ./pkg/readers
|
||||||
|
go doc -all ./pkg/writers
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using `godoc` Web Server
|
||||||
|
|
||||||
|
**Quick Start (Recommended):**
|
||||||
|
```bash
|
||||||
|
make godoc
|
||||||
|
```
|
||||||
|
|
||||||
|
This will automatically install godoc if needed and start the server on port 6060.
|
||||||
|
|
||||||
|
**Manual Installation:**
|
||||||
|
```bash
|
||||||
|
go install golang.org/x/tools/cmd/godoc@latest
|
||||||
|
godoc -http=:6060
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open your browser to:
|
||||||
|
```
|
||||||
|
http://localhost:6060/pkg/git.warky.dev/wdevs/relspecgo/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Package Documentation
|
||||||
|
|
||||||
|
### Core Packages
|
||||||
|
|
||||||
|
- **`pkg/models`** - Core data structures (Database, Schema, Table, Column, etc.)
|
||||||
|
- **`pkg/readers`** - Input format readers (dbml, pgsql, gorm, prisma, etc.)
|
||||||
|
- **`pkg/writers`** - Output format writers (dbml, pgsql, gorm, prisma, etc.)
|
||||||
|
|
||||||
|
### Utility Packages
|
||||||
|
|
||||||
|
- **`pkg/diff`** - Schema comparison and difference detection
|
||||||
|
- **`pkg/merge`** - Schema merging utilities
|
||||||
|
- **`pkg/transform`** - Validation and normalization
|
||||||
|
- **`pkg/ui`** - Interactive terminal UI for schema editing
|
||||||
|
|
||||||
|
### Support Packages
|
||||||
|
|
||||||
|
- **`pkg/pgsql`** - PostgreSQL-specific utilities
|
||||||
|
- **`pkg/inspector`** - Database introspection capabilities
|
||||||
|
- **`pkg/reflectutil`** - Reflection utilities for Go code analysis
|
||||||
|
- **`pkg/commontypes`** - Shared type definitions
|
||||||
|
|
||||||
|
### Reader Implementations
|
||||||
|
|
||||||
|
Each reader is in its own subpackage under `pkg/readers/`:
|
||||||
|
|
||||||
|
- `pkg/readers/dbml` - DBML format reader
|
||||||
|
- `pkg/readers/dctx` - DCTX format reader
|
||||||
|
- `pkg/readers/drawdb` - DrawDB JSON reader
|
||||||
|
- `pkg/readers/graphql` - GraphQL schema reader
|
||||||
|
- `pkg/readers/json` - JSON schema reader
|
||||||
|
- `pkg/readers/yaml` - YAML schema reader
|
||||||
|
- `pkg/readers/gorm` - Go GORM models reader
|
||||||
|
- `pkg/readers/bun` - Go Bun models reader
|
||||||
|
- `pkg/readers/drizzle` - TypeScript Drizzle ORM reader
|
||||||
|
- `pkg/readers/prisma` - Prisma schema reader
|
||||||
|
- `pkg/readers/typeorm` - TypeScript TypeORM reader
|
||||||
|
- `pkg/readers/pgsql` - PostgreSQL database reader
|
||||||
|
- `pkg/readers/sqlite` - SQLite database reader
|
||||||
|
|
||||||
|
### Writer Implementations
|
||||||
|
|
||||||
|
Each writer is in its own subpackage under `pkg/writers/`:
|
||||||
|
|
||||||
|
- `pkg/writers/dbml` - DBML format writer
|
||||||
|
- `pkg/writers/dctx` - DCTX format writer
|
||||||
|
- `pkg/writers/drawdb` - DrawDB JSON writer
|
||||||
|
- `pkg/writers/graphql` - GraphQL schema writer
|
||||||
|
- `pkg/writers/json` - JSON schema writer
|
||||||
|
- `pkg/writers/yaml` - YAML schema writer
|
||||||
|
- `pkg/writers/gorm` - Go GORM models writer
|
||||||
|
- `pkg/writers/bun` - Go Bun models writer
|
||||||
|
- `pkg/writers/drizzle` - TypeScript Drizzle ORM writer
|
||||||
|
- `pkg/writers/prisma` - Prisma schema writer
|
||||||
|
- `pkg/writers/typeorm` - TypeScript TypeORM writer
|
||||||
|
- `pkg/writers/pgsql` - PostgreSQL SQL writer
|
||||||
|
- `pkg/writers/sqlite` - SQLite SQL writer
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Reading a Schema
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"git.warky.dev/wdevs/relspecgo/pkg/readers"
|
||||||
|
"git.warky.dev/wdevs/relspecgo/pkg/readers/dbml"
|
||||||
|
)
|
||||||
|
|
||||||
|
reader := dbml.NewReader(&readers.ReaderOptions{
|
||||||
|
FilePath: "schema.dbml",
|
||||||
|
})
|
||||||
|
db, err := reader.ReadDatabase()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Writing a Schema
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
||||||
|
"git.warky.dev/wdevs/relspecgo/pkg/writers/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
writer := gorm.NewWriter(&writers.WriterOptions{
|
||||||
|
OutputPath: "./models",
|
||||||
|
PackageName: "models",
|
||||||
|
})
|
||||||
|
err := writer.WriteDatabase(db)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comparing Schemas
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "git.warky.dev/wdevs/relspecgo/pkg/diff"
|
||||||
|
|
||||||
|
result := diff.CompareDatabases(sourceDB, targetDB)
|
||||||
|
err := diff.FormatDiff(result, diff.OutputFormatText, os.Stdout)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Merging Schemas
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "git.warky.dev/wdevs/relspecgo/pkg/merge"
|
||||||
|
|
||||||
|
result := merge.MergeDatabases(targetDB, sourceDB, nil)
|
||||||
|
fmt.Printf("Added %d tables\n", result.TablesAdded)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation Standards
|
||||||
|
|
||||||
|
All public APIs follow Go documentation conventions:
|
||||||
|
|
||||||
|
- Package documentation in `doc.go` files
|
||||||
|
- Type, function, and method comments start with the item name
|
||||||
|
- Examples where applicable
|
||||||
|
- Clear description of parameters and return values
|
||||||
|
- Usage notes and caveats where relevant
|
||||||
|
|
||||||
|
## Generating Documentation
|
||||||
|
|
||||||
|
To regenerate documentation after code changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify documentation builds correctly
|
||||||
|
go doc -all ./pkg/... > /dev/null
|
||||||
|
|
||||||
|
# Check for undocumented exports
|
||||||
|
go vet ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing Documentation
|
||||||
|
|
||||||
|
When adding new packages or exported items:
|
||||||
|
|
||||||
|
1. Add package documentation in a `doc.go` file
|
||||||
|
2. Document all exported types, functions, and methods
|
||||||
|
3. Include usage examples for complex APIs
|
||||||
|
4. Follow Go documentation style guide
|
||||||
|
5. Verify with `go doc` before committing
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Go Documentation Guide](https://go.dev/doc/comment)
|
||||||
|
- [Effective Go - Commentary](https://go.dev/doc/effective_go#commentary)
|
||||||
|
- [godoc Documentation](https://pkg.go.dev/golang.org/x/tools/cmd/godoc)
|
||||||
25
Makefile
25
Makefile
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: all build test test-unit test-integration lint coverage clean install help docker-up docker-down docker-test docker-test-integration start stop release release-version
|
.PHONY: all build test test-unit test-integration lint coverage clean install help docker-up docker-down docker-test docker-test-integration start stop release release-version godoc
|
||||||
|
|
||||||
# Binary name
|
# Binary name
|
||||||
BINARY_NAME=relspec
|
BINARY_NAME=relspec
|
||||||
@@ -101,6 +101,29 @@ deps: ## Download dependencies
|
|||||||
$(GOMOD) tidy
|
$(GOMOD) tidy
|
||||||
@echo "Dependencies updated"
|
@echo "Dependencies updated"
|
||||||
|
|
||||||
|
godoc: ## Start godoc server on http://localhost:6060
|
||||||
|
@echo "Starting godoc server..."
|
||||||
|
@GOBIN=$$(go env GOPATH)/bin; \
|
||||||
|
if command -v godoc > /dev/null 2>&1; then \
|
||||||
|
echo "godoc server running on http://localhost:6060"; \
|
||||||
|
echo "View documentation at: http://localhost:6060/pkg/git.warky.dev/wdevs/relspecgo/"; \
|
||||||
|
echo "Press Ctrl+C to stop"; \
|
||||||
|
godoc -http=:6060; \
|
||||||
|
elif [ -f "$$GOBIN/godoc" ]; then \
|
||||||
|
echo "godoc server running on http://localhost:6060"; \
|
||||||
|
echo "View documentation at: http://localhost:6060/pkg/git.warky.dev/wdevs/relspecgo/"; \
|
||||||
|
echo "Press Ctrl+C to stop"; \
|
||||||
|
$$GOBIN/godoc -http=:6060; \
|
||||||
|
else \
|
||||||
|
echo "godoc not installed. Installing..."; \
|
||||||
|
go install golang.org/x/tools/cmd/godoc@latest; \
|
||||||
|
echo "godoc installed. Starting server..."; \
|
||||||
|
echo "godoc server running on http://localhost:6060"; \
|
||||||
|
echo "View documentation at: http://localhost:6060/pkg/git.warky.dev/wdevs/relspecgo/"; \
|
||||||
|
echo "Press Ctrl+C to stop"; \
|
||||||
|
$$GOBIN/godoc -http=:6060; \
|
||||||
|
fi
|
||||||
|
|
||||||
start: docker-up ## Alias for docker-up (start PostgreSQL test database)
|
start: docker-up ## Alias for docker-up (start PostgreSQL test database)
|
||||||
|
|
||||||
stop: docker-down ## Alias for docker-down (stop PostgreSQL test database)
|
stop: docker-down ## Alias for docker-down (stop PostgreSQL test database)
|
||||||
|
|||||||
4
TODO.md
4
TODO.md
@@ -25,7 +25,7 @@
|
|||||||
- [✔️] Basic UI (I went with tview)
|
- [✔️] Basic UI (I went with tview)
|
||||||
- [✔️] Save / Load Database
|
- [✔️] Save / Load Database
|
||||||
- [✔️] Schemas / Domains / Tables
|
- [✔️] Schemas / Domains / Tables
|
||||||
- [ ] Add Relations
|
- [✔️] Add Relations
|
||||||
- [ ] Add Indexes
|
- [ ] Add Indexes
|
||||||
- [ ] Add Views
|
- [ ] Add Views
|
||||||
- [ ] Add Sequences
|
- [ ] Add Sequences
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- [ ] API documentation (godoc)
|
- [✔️] API documentation (godoc)
|
||||||
- [ ] Usage examples for each format combination
|
- [ ] Usage examples for each format combination
|
||||||
|
|
||||||
## Advanced Features
|
## Advanced Features
|
||||||
|
|||||||
108
doc.go
Normal file
108
doc.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
// Package relspecgo provides bidirectional conversion between database schema formats.
|
||||||
|
//
|
||||||
|
// RelSpec is a comprehensive database schema tool that reads, writes, and transforms
|
||||||
|
// database schemas across multiple formats including live databases, ORM models,
|
||||||
|
// schema definition languages, and data interchange formats.
|
||||||
|
//
|
||||||
|
// # Features
|
||||||
|
//
|
||||||
|
// - Read from 15+ formats: PostgreSQL, SQLite, DBML, GORM, Prisma, Drizzle, and more
|
||||||
|
// - Write to 15+ formats: SQL, ORM models, schema definitions, JSON/YAML
|
||||||
|
// - Interactive TUI editor for visual schema management
|
||||||
|
// - Schema diff and merge capabilities
|
||||||
|
// - Format-agnostic intermediate representation
|
||||||
|
//
|
||||||
|
// # Architecture
|
||||||
|
//
|
||||||
|
// RelSpec uses a hub-and-spoke architecture with models.Database as the central type:
|
||||||
|
//
|
||||||
|
// Input Format → Reader → models.Database → Writer → Output Format
|
||||||
|
//
|
||||||
|
// This allows any supported input format to be converted to any supported output format
|
||||||
|
// without requiring N² conversion implementations.
|
||||||
|
//
|
||||||
|
// # Key Packages
|
||||||
|
//
|
||||||
|
// - pkg/models: Core data structures (Database, Schema, Table, Column, etc.)
|
||||||
|
// - pkg/readers: Input format readers (dbml, pgsql, gorm, etc.)
|
||||||
|
// - pkg/writers: Output format writers (dbml, pgsql, gorm, etc.)
|
||||||
|
// - pkg/ui: Interactive terminal UI for schema editing
|
||||||
|
// - pkg/diff: Schema comparison and difference detection
|
||||||
|
// - pkg/merge: Schema merging utilities
|
||||||
|
// - pkg/transform: Validation and normalization
|
||||||
|
//
|
||||||
|
// # Installation
|
||||||
|
//
|
||||||
|
// go install git.warky.dev/wdevs/relspecgo/cmd/relspec@latest
|
||||||
|
//
|
||||||
|
// # Usage
|
||||||
|
//
|
||||||
|
// Command-line conversion:
|
||||||
|
//
|
||||||
|
// relspec convert --from dbml --from-path schema.dbml \
|
||||||
|
// --to gorm --to-path ./models
|
||||||
|
//
|
||||||
|
// Interactive editor:
|
||||||
|
//
|
||||||
|
// relspec edit --from pgsql --from-conn "postgres://..." \
|
||||||
|
// --to dbml --to-path schema.dbml
|
||||||
|
//
|
||||||
|
// Schema comparison:
|
||||||
|
//
|
||||||
|
// relspec diff --source-type pgsql --source-conn "postgres://..." \
|
||||||
|
// --target-type dbml --target-path schema.dbml
|
||||||
|
//
|
||||||
|
// Merge schemas:
|
||||||
|
//
|
||||||
|
// relspec merge --target schema1.dbml --sources schema2.dbml,schema3.dbml
|
||||||
|
//
|
||||||
|
// # Supported Formats
|
||||||
|
//
|
||||||
|
// Input/Output Formats:
|
||||||
|
// - dbml: Database Markup Language
|
||||||
|
// - dctx: DCTX schema files
|
||||||
|
// - drawdb: DrawDB JSON format
|
||||||
|
// - graphql: GraphQL schema definition
|
||||||
|
// - json: JSON schema representation
|
||||||
|
// - yaml: YAML schema representation
|
||||||
|
// - gorm: Go GORM models
|
||||||
|
// - bun: Go Bun models
|
||||||
|
// - drizzle: TypeScript Drizzle ORM
|
||||||
|
// - prisma: Prisma schema language
|
||||||
|
// - typeorm: TypeScript TypeORM entities
|
||||||
|
// - pgsql: PostgreSQL (live DB or SQL)
|
||||||
|
// - sqlite: SQLite (database file or SQL)
|
||||||
|
//
|
||||||
|
// # Library Usage
|
||||||
|
//
|
||||||
|
// RelSpec can be used as a Go library:
|
||||||
|
//
|
||||||
|
// import (
|
||||||
|
// "git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||||
|
// "git.warky.dev/wdevs/relspecgo/pkg/readers/dbml"
|
||||||
|
// "git.warky.dev/wdevs/relspecgo/pkg/writers/gorm"
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// // Read DBML
|
||||||
|
// reader := dbml.NewReader(&readers.ReaderOptions{
|
||||||
|
// FilePath: "schema.dbml",
|
||||||
|
// })
|
||||||
|
// db, err := reader.ReadDatabase()
|
||||||
|
//
|
||||||
|
// // Write GORM models
|
||||||
|
// writer := gorm.NewWriter(&writers.WriterOptions{
|
||||||
|
// OutputPath: "./models",
|
||||||
|
// PackageName: "models",
|
||||||
|
// })
|
||||||
|
// err = writer.WriteDatabase(db)
|
||||||
|
//
|
||||||
|
// # Documentation
|
||||||
|
//
|
||||||
|
// Full documentation available at: https://git.warky.dev/wdevs/relspecgo
|
||||||
|
//
|
||||||
|
// API documentation: go doc git.warky.dev/wdevs/relspecgo/...
|
||||||
|
//
|
||||||
|
// # License
|
||||||
|
//
|
||||||
|
// See LICENSE file in the repository root.
|
||||||
|
package relspecgo
|
||||||
2
go.mod
2
go.mod
@@ -12,6 +12,7 @@ require (
|
|||||||
github.com/uptrace/bun v1.2.16
|
github.com/uptrace/bun v1.2.16
|
||||||
golang.org/x/text v0.28.0
|
golang.org/x/text v0.28.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
modernc.org/sqlite v1.44.3
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -43,5 +44,4 @@ require (
|
|||||||
modernc.org/libc v1.67.6 // indirect
|
modernc.org/libc v1.67.6 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
modernc.org/sqlite v1.44.3 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
32
go.sum
32
go.sum
@@ -10,8 +10,12 @@ github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeekl
|
|||||||
github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU=
|
github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU=
|
||||||
github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw=
|
github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
@@ -87,6 +91,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||||
|
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
@@ -102,8 +108,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
|||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -146,6 +152,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
|
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||||
|
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
@@ -153,11 +161,31 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
|
|||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||||
|
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
|
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
||||||
|
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
|
||||||
|
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||||
|
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||||
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
|
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
|
||||||
|
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||||
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
|
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
|
||||||
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
|
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
|
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||||
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY=
|
modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY=
|
||||||
modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
||||||
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
|
|||||||
28
pkg/commontypes/doc.go
Normal file
28
pkg/commontypes/doc.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// Package commontypes provides shared type definitions used across multiple packages.
|
||||||
|
//
|
||||||
|
// # Overview
|
||||||
|
//
|
||||||
|
// The commontypes package contains common data structures, constants, and type
|
||||||
|
// definitions that are shared between different parts of RelSpec but don't belong
|
||||||
|
// to the core models package.
|
||||||
|
//
|
||||||
|
// # Purpose
|
||||||
|
//
|
||||||
|
// This package helps avoid circular dependencies by providing a common location
|
||||||
|
// for types that are used by multiple packages without creating import cycles.
|
||||||
|
//
|
||||||
|
// # Contents
|
||||||
|
//
|
||||||
|
// Common types may include:
|
||||||
|
// - Shared enums and constants
|
||||||
|
// - Utility type aliases
|
||||||
|
// - Common error types
|
||||||
|
// - Shared configuration structures
|
||||||
|
//
|
||||||
|
// # Usage
|
||||||
|
//
|
||||||
|
// import "git.warky.dev/wdevs/relspecgo/pkg/commontypes"
|
||||||
|
//
|
||||||
|
// // Use common types
|
||||||
|
// var formatType commontypes.FormatType
|
||||||
|
package commontypes
|
||||||
43
pkg/diff/doc.go
Normal file
43
pkg/diff/doc.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// Package diff provides utilities for comparing database schemas and identifying differences.
|
||||||
|
//
|
||||||
|
// # Overview
|
||||||
|
//
|
||||||
|
// The diff package compares two database models at various granularity levels (database,
|
||||||
|
// schema, table, column) and produces detailed reports of differences including:
|
||||||
|
// - Missing items (present in source but not in target)
|
||||||
|
// - Extra items (present in target but not in source)
|
||||||
|
// - Modified items (present in both but with different properties)
|
||||||
|
//
|
||||||
|
// # Usage
|
||||||
|
//
|
||||||
|
// Compare two databases and format the output:
|
||||||
|
//
|
||||||
|
// result := diff.CompareDatabases(sourceDB, targetDB)
|
||||||
|
// err := diff.FormatDiff(result, diff.OutputFormatText, os.Stdout)
|
||||||
|
//
|
||||||
|
// # Output Formats
|
||||||
|
//
|
||||||
|
// The package supports multiple output formats:
|
||||||
|
// - OutputFormatText: Human-readable text format
|
||||||
|
// - OutputFormatJSON: Structured JSON output
|
||||||
|
// - OutputFormatYAML: Structured YAML output
|
||||||
|
//
|
||||||
|
// # Comparison Scope
|
||||||
|
//
|
||||||
|
// The comparison covers:
|
||||||
|
// - Schemas: Name, description, and contents
|
||||||
|
// - Tables: Name, description, and all sub-elements
|
||||||
|
// - Columns: Type, nullability, defaults, constraints
|
||||||
|
// - Indexes: Columns, uniqueness, type
|
||||||
|
// - Constraints: Type, columns, references
|
||||||
|
// - Relationships: Type, from/to tables and columns
|
||||||
|
// - Views: Definition and columns
|
||||||
|
// - Sequences: Start value, increment, min/max values
|
||||||
|
//
|
||||||
|
// # Use Cases
|
||||||
|
//
|
||||||
|
// - Schema migration planning
|
||||||
|
// - Database synchronization verification
|
||||||
|
// - Change tracking and auditing
|
||||||
|
// - CI/CD pipeline validation
|
||||||
|
package diff
|
||||||
40
pkg/inspector/doc.go
Normal file
40
pkg/inspector/doc.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// Package inspector provides database introspection capabilities for live databases.
|
||||||
|
//
|
||||||
|
// # Overview
|
||||||
|
//
|
||||||
|
// The inspector package contains utilities for connecting to live databases and
|
||||||
|
// extracting their schema information through system catalog queries and metadata
|
||||||
|
// inspection.
|
||||||
|
//
|
||||||
|
// # Features
|
||||||
|
//
|
||||||
|
// - Database connection management
|
||||||
|
// - Schema metadata extraction
|
||||||
|
// - Table structure analysis
|
||||||
|
// - Constraint and index discovery
|
||||||
|
// - Foreign key relationship mapping
|
||||||
|
//
|
||||||
|
// # Supported Databases
|
||||||
|
//
|
||||||
|
// - PostgreSQL (via pgx driver)
|
||||||
|
// - SQLite (via modernc.org/sqlite driver)
|
||||||
|
//
|
||||||
|
// # Usage
|
||||||
|
//
|
||||||
|
// This package is used internally by database readers (pgsql, sqlite) to perform
|
||||||
|
// live schema introspection:
|
||||||
|
//
|
||||||
|
// inspector := inspector.NewPostgreSQLInspector(connString)
|
||||||
|
// schemas, err := inspector.GetSchemas()
|
||||||
|
// tables, err := inspector.GetTables(schemaName)
|
||||||
|
//
|
||||||
|
// # Architecture
|
||||||
|
//
|
||||||
|
// Each database type has its own inspector implementation that understands the
|
||||||
|
// specific system catalogs and metadata structures of that database system.
|
||||||
|
//
|
||||||
|
// # Security
|
||||||
|
//
|
||||||
|
// Inspectors use read-only operations and never modify database structure.
|
||||||
|
// Connection credentials should be handled securely.
|
||||||
|
package inspector
|
||||||
36
pkg/pgsql/doc.go
Normal file
36
pkg/pgsql/doc.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// Package pgsql provides PostgreSQL-specific utilities and helpers.
|
||||||
|
//
|
||||||
|
// # Overview
|
||||||
|
//
|
||||||
|
// The pgsql package contains PostgreSQL-specific functionality including:
|
||||||
|
// - SQL reserved keyword validation
|
||||||
|
// - Data type mappings and conversions
|
||||||
|
// - PostgreSQL-specific schema introspection helpers
|
||||||
|
//
|
||||||
|
// # Components
|
||||||
|
//
|
||||||
|
// keywords.go - SQL reserved keywords validation
|
||||||
|
//
|
||||||
|
// Provides functions to check if identifiers conflict with SQL reserved words
|
||||||
|
// and need quoting for safe usage in PostgreSQL queries.
|
||||||
|
//
|
||||||
|
// datatypes.go - PostgreSQL data type utilities
|
||||||
|
//
|
||||||
|
// Contains mappings between PostgreSQL data types and their equivalents in other
|
||||||
|
// systems, as well as type conversion and normalization functions.
|
||||||
|
//
|
||||||
|
// # Usage
|
||||||
|
//
|
||||||
|
// // Check if identifier needs quoting
|
||||||
|
// if pgsql.IsReservedKeyword("user") {
|
||||||
|
// // Quote the identifier
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Normalize data type
|
||||||
|
// normalizedType := pgsql.NormalizeDataType("varchar(255)")
|
||||||
|
//
|
||||||
|
// # Purpose
|
||||||
|
//
|
||||||
|
// This package supports the PostgreSQL reader and writer implementations by providing
|
||||||
|
// shared utilities for handling PostgreSQL-specific schema elements and constraints.
|
||||||
|
package pgsql
|
||||||
53
pkg/readers/doc.go
Normal file
53
pkg/readers/doc.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// Package readers provides interfaces and implementations for reading database schemas
|
||||||
|
// from various input formats and data sources.
|
||||||
|
//
|
||||||
|
// # Overview
|
||||||
|
//
|
||||||
|
// The readers package defines a common Reader interface that all format-specific readers
|
||||||
|
// implement. This allows RelSpec to read database schemas from multiple sources including:
|
||||||
|
// - Live databases (PostgreSQL, SQLite)
|
||||||
|
// - Schema definition files (DBML, DCTX, DrawDB, GraphQL)
|
||||||
|
// - ORM model files (GORM, Bun, Drizzle, Prisma, TypeORM)
|
||||||
|
// - Data interchange formats (JSON, YAML)
|
||||||
|
//
|
||||||
|
// # Architecture
|
||||||
|
//
|
||||||
|
// Each reader implementation is located in its own subpackage (e.g., pkg/readers/dbml,
|
||||||
|
// pkg/readers/pgsql) and implements the Reader interface, supporting three levels of
|
||||||
|
// granularity:
|
||||||
|
// - ReadDatabase() - Read complete database with all schemas
|
||||||
|
// - ReadSchema() - Read single schema with all tables
|
||||||
|
// - ReadTable() - Read single table with all columns and metadata
|
||||||
|
//
|
||||||
|
// # Usage
|
||||||
|
//
|
||||||
|
// Readers are instantiated with ReaderOptions containing source-specific configuration:
|
||||||
|
//
|
||||||
|
// // Read from file
|
||||||
|
// reader := dbml.NewReader(&readers.ReaderOptions{
|
||||||
|
// FilePath: "schema.dbml",
|
||||||
|
// })
|
||||||
|
// db, err := reader.ReadDatabase()
|
||||||
|
//
|
||||||
|
// // Read from database
|
||||||
|
// reader := pgsql.NewReader(&readers.ReaderOptions{
|
||||||
|
// ConnectionString: "postgres://user:pass@localhost/mydb",
|
||||||
|
// })
|
||||||
|
// db, err := reader.ReadDatabase()
|
||||||
|
//
|
||||||
|
// # Supported Formats
|
||||||
|
//
|
||||||
|
// - dbml: Database Markup Language files
|
||||||
|
// - dctx: DCTX schema files
|
||||||
|
// - drawdb: DrawDB JSON format
|
||||||
|
// - graphql: GraphQL schema definition language
|
||||||
|
// - json: JSON database schema
|
||||||
|
// - yaml: YAML database schema
|
||||||
|
// - gorm: Go GORM model structs
|
||||||
|
// - bun: Go Bun model structs
|
||||||
|
// - drizzle: TypeScript Drizzle ORM schemas
|
||||||
|
// - prisma: Prisma schema language
|
||||||
|
// - typeorm: TypeScript TypeORM entities
|
||||||
|
// - pgsql: PostgreSQL live database introspection
|
||||||
|
// - sqlite: SQLite database files
|
||||||
|
package readers
|
||||||
@@ -106,7 +106,7 @@ func (r *Reader) queryColumns(tableName string) (map[string]*models.Column, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for autoincrement (SQLite uses INTEGER PRIMARY KEY AUTOINCREMENT)
|
// Check for autoincrement (SQLite uses INTEGER PRIMARY KEY AUTOINCREMENT)
|
||||||
if pk > 0 && strings.ToUpper(dataType) == "INTEGER" {
|
if pk > 0 && strings.EqualFold(dataType, "INTEGER") {
|
||||||
column.AutoIncrement = r.isAutoIncrement(tableName, name)
|
column.AutoIncrement = r.isAutoIncrement(tableName, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
36
pkg/reflectutil/doc.go
Normal file
36
pkg/reflectutil/doc.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// Package reflectutil provides reflection utilities for analyzing Go code structures.
|
||||||
|
//
|
||||||
|
// # Overview
|
||||||
|
//
|
||||||
|
// The reflectutil package offers helper functions for working with Go's reflection
|
||||||
|
// capabilities, particularly for parsing Go struct definitions and extracting type
|
||||||
|
// information. This is used by readers that parse ORM model files.
|
||||||
|
//
|
||||||
|
// # Features
|
||||||
|
//
|
||||||
|
// - Struct tag parsing and extraction
|
||||||
|
// - Type information analysis
|
||||||
|
// - Field metadata extraction
|
||||||
|
// - ORM tag interpretation (GORM, Bun, etc.)
|
||||||
|
//
|
||||||
|
// # Usage
|
||||||
|
//
|
||||||
|
// This package is primarily used internally by readers like GORM and Bun to parse
|
||||||
|
// Go struct definitions and convert them to database schema models.
|
||||||
|
//
|
||||||
|
// // Example: Parse struct tags
|
||||||
|
// tags := reflectutil.ParseStructTags(field)
|
||||||
|
// columnName := tags.Get("db")
|
||||||
|
//
|
||||||
|
// # Supported ORM Tags
|
||||||
|
//
|
||||||
|
// The package understands tag conventions from:
|
||||||
|
// - GORM (gorm tag)
|
||||||
|
// - Bun (bun tag)
|
||||||
|
// - Standard database/sql (db tag)
|
||||||
|
//
|
||||||
|
// # Purpose
|
||||||
|
//
|
||||||
|
// This package enables RelSpec to read existing ORM models and convert them to
|
||||||
|
// a unified schema representation for transformation to other formats.
|
||||||
|
package reflectutil
|
||||||
34
pkg/transform/doc.go
Normal file
34
pkg/transform/doc.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// Package transform provides validation and transformation utilities for database models.
|
||||||
|
//
|
||||||
|
// # Overview
|
||||||
|
//
|
||||||
|
// The transform package contains a Transformer type that provides methods for validating
|
||||||
|
// and normalizing database schemas. It ensures schema correctness and consistency across
|
||||||
|
// different format conversions.
|
||||||
|
//
|
||||||
|
// # Features
|
||||||
|
//
|
||||||
|
// - Database validation (structure and naming conventions)
|
||||||
|
// - Schema validation (completeness and integrity)
|
||||||
|
// - Table validation (column definitions and constraints)
|
||||||
|
// - Data type normalization
|
||||||
|
//
|
||||||
|
// # Usage
|
||||||
|
//
|
||||||
|
// transformer := transform.NewTransformer()
|
||||||
|
// err := transformer.ValidateDatabase(db)
|
||||||
|
// if err != nil {
|
||||||
|
// log.Fatal("Invalid database schema:", err)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// # Validation Scope
|
||||||
|
//
|
||||||
|
// The transformer validates:
|
||||||
|
// - Required fields presence
|
||||||
|
// - Naming convention adherence
|
||||||
|
// - Data type compatibility
|
||||||
|
// - Constraint consistency
|
||||||
|
// - Relationship integrity
|
||||||
|
//
|
||||||
|
// Note: Some validation methods are currently stubs and will be implemented as needed.
|
||||||
|
package transform
|
||||||
57
pkg/ui/doc.go
Normal file
57
pkg/ui/doc.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
// Package ui provides an interactive terminal user interface (TUI) for editing database schemas.
|
||||||
|
//
|
||||||
|
// # Overview
|
||||||
|
//
|
||||||
|
// The ui package implements a full-featured terminal-based schema editor using tview,
|
||||||
|
// allowing users to visually create, modify, and manage database schemas without writing
|
||||||
|
// code or SQL.
|
||||||
|
//
|
||||||
|
// # Features
|
||||||
|
//
|
||||||
|
// The schema editor supports:
|
||||||
|
// - Database management: Edit name, description, and properties
|
||||||
|
// - Schema management: Create, edit, delete schemas
|
||||||
|
// - Table management: Create, edit, delete tables
|
||||||
|
// - Column management: Add, modify, delete columns with full property support
|
||||||
|
// - Relationship management: Define and edit table relationships
|
||||||
|
// - Domain management: Organize tables into logical domains
|
||||||
|
// - Import & merge: Combine schemas from multiple sources
|
||||||
|
// - Save: Export to any supported format
|
||||||
|
//
|
||||||
|
// # Architecture
|
||||||
|
//
|
||||||
|
// The package is organized into several components:
|
||||||
|
// - editor.go: Main editor and application lifecycle
|
||||||
|
// - *_screens.go: UI screens for each entity type
|
||||||
|
// - *_dataops.go: Business logic and data operations
|
||||||
|
// - dialogs.go: Reusable dialog components
|
||||||
|
// - load_save_screens.go: File I/O and format selection
|
||||||
|
// - main_menu.go: Primary navigation menu
|
||||||
|
//
|
||||||
|
// # Usage
|
||||||
|
//
|
||||||
|
// editor := ui.NewSchemaEditor(database)
|
||||||
|
// if err := editor.Run(); err != nil {
|
||||||
|
// log.Fatal(err)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Or with pre-configured load/save options:
|
||||||
|
//
|
||||||
|
// editor := ui.NewSchemaEditorWithConfigs(database, loadConfig, saveConfig)
|
||||||
|
// if err := editor.Run(); err != nil {
|
||||||
|
// log.Fatal(err)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// # Navigation
|
||||||
|
//
|
||||||
|
// - Arrow keys: Navigate between items
|
||||||
|
// - Enter: Select/edit item
|
||||||
|
// - Tab/Shift+Tab: Navigate between buttons
|
||||||
|
// - Escape: Go back/cancel
|
||||||
|
// - Letter shortcuts: Quick actions (e.g., 'n' for new, 'e' for edit, 'd' for delete)
|
||||||
|
//
|
||||||
|
// # Integration
|
||||||
|
//
|
||||||
|
// The editor integrates with all readers and writers, supporting load/save operations
|
||||||
|
// for any format supported by RelSpec (DBML, PostgreSQL, GORM, Prisma, etc.).
|
||||||
|
package ui
|
||||||
115
pkg/ui/relation_dataops.go
Normal file
115
pkg/ui/relation_dataops.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import "git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||||
|
|
||||||
|
// Relationship data operations - business logic for relationship management
|
||||||
|
|
||||||
|
// CreateRelationship creates a new relationship and adds it to a table
|
||||||
|
func (se *SchemaEditor) CreateRelationship(schemaIndex, tableIndex int, rel *models.Relationship) *models.Relationship {
|
||||||
|
if schemaIndex < 0 || schemaIndex >= len(se.db.Schemas) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
schema := se.db.Schemas[schemaIndex]
|
||||||
|
if tableIndex < 0 || tableIndex >= len(schema.Tables) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
table := schema.Tables[tableIndex]
|
||||||
|
if table.Relationships == nil {
|
||||||
|
table.Relationships = make(map[string]*models.Relationship)
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Relationships[rel.Name] = rel
|
||||||
|
table.UpdateDate()
|
||||||
|
return rel
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateRelationship updates an existing relationship
|
||||||
|
func (se *SchemaEditor) UpdateRelationship(schemaIndex, tableIndex int, oldName string, rel *models.Relationship) bool {
|
||||||
|
if schemaIndex < 0 || schemaIndex >= len(se.db.Schemas) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
schema := se.db.Schemas[schemaIndex]
|
||||||
|
if tableIndex < 0 || tableIndex >= len(schema.Tables) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
table := schema.Tables[tableIndex]
|
||||||
|
if table.Relationships == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete old entry if name changed
|
||||||
|
if oldName != rel.Name {
|
||||||
|
delete(table.Relationships, oldName)
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Relationships[rel.Name] = rel
|
||||||
|
table.UpdateDate()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRelationship removes a relationship from a table
|
||||||
|
func (se *SchemaEditor) DeleteRelationship(schemaIndex, tableIndex int, relName string) bool {
|
||||||
|
if schemaIndex < 0 || schemaIndex >= len(se.db.Schemas) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
schema := se.db.Schemas[schemaIndex]
|
||||||
|
if tableIndex < 0 || tableIndex >= len(schema.Tables) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
table := schema.Tables[tableIndex]
|
||||||
|
if table.Relationships == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(table.Relationships, relName)
|
||||||
|
table.UpdateDate()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRelationship returns a relationship by name
|
||||||
|
func (se *SchemaEditor) GetRelationship(schemaIndex, tableIndex int, relName string) *models.Relationship {
|
||||||
|
if schemaIndex < 0 || schemaIndex >= len(se.db.Schemas) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
schema := se.db.Schemas[schemaIndex]
|
||||||
|
if tableIndex < 0 || tableIndex >= len(schema.Tables) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
table := schema.Tables[tableIndex]
|
||||||
|
if table.Relationships == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return table.Relationships[relName]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRelationshipNames returns all relationship names for a table
|
||||||
|
func (se *SchemaEditor) GetRelationshipNames(schemaIndex, tableIndex int) []string {
|
||||||
|
if schemaIndex < 0 || schemaIndex >= len(se.db.Schemas) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
schema := se.db.Schemas[schemaIndex]
|
||||||
|
if tableIndex < 0 || tableIndex >= len(schema.Tables) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
table := schema.Tables[tableIndex]
|
||||||
|
if table.Relationships == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
names := make([]string, 0, len(table.Relationships))
|
||||||
|
for name := range table.Relationships {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
486
pkg/ui/relation_screens.go
Normal file
486
pkg/ui/relation_screens.go
Normal file
@@ -0,0 +1,486 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gdamore/tcell/v2"
|
||||||
|
"github.com/rivo/tview"
|
||||||
|
|
||||||
|
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// showRelationshipList displays all relationships for a table
|
||||||
|
func (se *SchemaEditor) showRelationshipList(schemaIndex, tableIndex int) {
|
||||||
|
table := se.GetTable(schemaIndex, tableIndex)
|
||||||
|
if table == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
flex := tview.NewFlex().SetDirection(tview.FlexRow)
|
||||||
|
|
||||||
|
// Title
|
||||||
|
title := tview.NewTextView().
|
||||||
|
SetText(fmt.Sprintf("[::b]Relationships for Table: %s", table.Name)).
|
||||||
|
SetDynamicColors(true).
|
||||||
|
SetTextAlign(tview.AlignCenter)
|
||||||
|
|
||||||
|
// Create relationships table
|
||||||
|
relTable := tview.NewTable().SetBorders(true).SetSelectable(true, false).SetFixed(1, 0)
|
||||||
|
|
||||||
|
// Add header row
|
||||||
|
headers := []string{"Name", "Type", "From Columns", "To Table", "To Columns", "Description"}
|
||||||
|
headerWidths := []int{20, 15, 20, 20, 20}
|
||||||
|
for i, header := range headers {
|
||||||
|
padding := ""
|
||||||
|
if i < len(headerWidths) {
|
||||||
|
padding = strings.Repeat(" ", headerWidths[i]-len(header))
|
||||||
|
}
|
||||||
|
cell := tview.NewTableCell(header + padding).
|
||||||
|
SetTextColor(tcell.ColorYellow).
|
||||||
|
SetSelectable(false).
|
||||||
|
SetAlign(tview.AlignLeft)
|
||||||
|
relTable.SetCell(0, i, cell)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get relationship names
|
||||||
|
relNames := se.GetRelationshipNames(schemaIndex, tableIndex)
|
||||||
|
for row, relName := range relNames {
|
||||||
|
rel := table.Relationships[relName]
|
||||||
|
|
||||||
|
// Name
|
||||||
|
nameStr := fmt.Sprintf("%-20s", rel.Name)
|
||||||
|
nameCell := tview.NewTableCell(nameStr).SetSelectable(true)
|
||||||
|
relTable.SetCell(row+1, 0, nameCell)
|
||||||
|
|
||||||
|
// Type
|
||||||
|
typeStr := fmt.Sprintf("%-15s", string(rel.Type))
|
||||||
|
typeCell := tview.NewTableCell(typeStr).SetSelectable(true)
|
||||||
|
relTable.SetCell(row+1, 1, typeCell)
|
||||||
|
|
||||||
|
// From Columns
|
||||||
|
fromColsStr := strings.Join(rel.FromColumns, ", ")
|
||||||
|
fromColsStr = fmt.Sprintf("%-20s", fromColsStr)
|
||||||
|
fromColsCell := tview.NewTableCell(fromColsStr).SetSelectable(true)
|
||||||
|
relTable.SetCell(row+1, 2, fromColsCell)
|
||||||
|
|
||||||
|
// To Table
|
||||||
|
toTableStr := rel.ToTable
|
||||||
|
if rel.ToSchema != "" && rel.ToSchema != table.Schema {
|
||||||
|
toTableStr = rel.ToSchema + "." + rel.ToTable
|
||||||
|
}
|
||||||
|
toTableStr = fmt.Sprintf("%-20s", toTableStr)
|
||||||
|
toTableCell := tview.NewTableCell(toTableStr).SetSelectable(true)
|
||||||
|
relTable.SetCell(row+1, 3, toTableCell)
|
||||||
|
|
||||||
|
// To Columns
|
||||||
|
toColsStr := strings.Join(rel.ToColumns, ", ")
|
||||||
|
toColsStr = fmt.Sprintf("%-20s", toColsStr)
|
||||||
|
toColsCell := tview.NewTableCell(toColsStr).SetSelectable(true)
|
||||||
|
relTable.SetCell(row+1, 4, toColsCell)
|
||||||
|
|
||||||
|
// Description
|
||||||
|
descCell := tview.NewTableCell(rel.Description).SetSelectable(true)
|
||||||
|
relTable.SetCell(row+1, 5, descCell)
|
||||||
|
}
|
||||||
|
|
||||||
|
relTable.SetTitle(" Relationships ").SetBorder(true).SetTitleAlign(tview.AlignLeft)
|
||||||
|
|
||||||
|
// Action buttons
|
||||||
|
btnFlex := tview.NewFlex()
|
||||||
|
btnNew := tview.NewButton("New Relationship [n]").SetSelectedFunc(func() {
|
||||||
|
se.showNewRelationshipDialog(schemaIndex, tableIndex)
|
||||||
|
})
|
||||||
|
btnEdit := tview.NewButton("Edit [e]").SetSelectedFunc(func() {
|
||||||
|
row, _ := relTable.GetSelection()
|
||||||
|
if row > 0 && row <= len(relNames) {
|
||||||
|
relName := relNames[row-1]
|
||||||
|
se.showEditRelationshipDialog(schemaIndex, tableIndex, relName)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
btnDelete := tview.NewButton("Delete [d]").SetSelectedFunc(func() {
|
||||||
|
row, _ := relTable.GetSelection()
|
||||||
|
if row > 0 && row <= len(relNames) {
|
||||||
|
relName := relNames[row-1]
|
||||||
|
se.showDeleteRelationshipConfirm(schemaIndex, tableIndex, relName)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
btnBack := tview.NewButton("Back [b]").SetSelectedFunc(func() {
|
||||||
|
se.pages.RemovePage("relationships")
|
||||||
|
se.pages.SwitchToPage("table-editor")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set up button navigation
|
||||||
|
btnNew.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
|
if event.Key() == tcell.KeyBacktab {
|
||||||
|
se.app.SetFocus(relTable)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if event.Key() == tcell.KeyTab {
|
||||||
|
se.app.SetFocus(btnEdit)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return event
|
||||||
|
})
|
||||||
|
|
||||||
|
btnEdit.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
|
if event.Key() == tcell.KeyBacktab {
|
||||||
|
se.app.SetFocus(btnNew)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if event.Key() == tcell.KeyTab {
|
||||||
|
se.app.SetFocus(btnDelete)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return event
|
||||||
|
})
|
||||||
|
|
||||||
|
btnDelete.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
|
if event.Key() == tcell.KeyBacktab {
|
||||||
|
se.app.SetFocus(btnEdit)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if event.Key() == tcell.KeyTab {
|
||||||
|
se.app.SetFocus(btnBack)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return event
|
||||||
|
})
|
||||||
|
|
||||||
|
btnBack.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
|
if event.Key() == tcell.KeyBacktab {
|
||||||
|
se.app.SetFocus(btnDelete)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if event.Key() == tcell.KeyTab {
|
||||||
|
se.app.SetFocus(relTable)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return event
|
||||||
|
})
|
||||||
|
|
||||||
|
btnFlex.AddItem(btnNew, 0, 1, true).
|
||||||
|
AddItem(btnEdit, 0, 1, false).
|
||||||
|
AddItem(btnDelete, 0, 1, false).
|
||||||
|
AddItem(btnBack, 0, 1, false)
|
||||||
|
|
||||||
|
relTable.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
|
if event.Key() == tcell.KeyEscape {
|
||||||
|
se.pages.RemovePage("relationships")
|
||||||
|
se.pages.SwitchToPage("table-editor")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if event.Key() == tcell.KeyTab {
|
||||||
|
se.app.SetFocus(btnNew)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if event.Key() == tcell.KeyEnter {
|
||||||
|
row, _ := relTable.GetSelection()
|
||||||
|
if row > 0 && row <= len(relNames) {
|
||||||
|
relName := relNames[row-1]
|
||||||
|
se.showEditRelationshipDialog(schemaIndex, tableIndex, relName)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if event.Rune() == 'n' {
|
||||||
|
se.showNewRelationshipDialog(schemaIndex, tableIndex)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if event.Rune() == 'e' {
|
||||||
|
row, _ := relTable.GetSelection()
|
||||||
|
if row > 0 && row <= len(relNames) {
|
||||||
|
relName := relNames[row-1]
|
||||||
|
se.showEditRelationshipDialog(schemaIndex, tableIndex, relName)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if event.Rune() == 'd' {
|
||||||
|
row, _ := relTable.GetSelection()
|
||||||
|
if row > 0 && row <= len(relNames) {
|
||||||
|
relName := relNames[row-1]
|
||||||
|
se.showDeleteRelationshipConfirm(schemaIndex, tableIndex, relName)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if event.Rune() == 'b' {
|
||||||
|
se.pages.RemovePage("relationships")
|
||||||
|
se.pages.SwitchToPage("table-editor")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return event
|
||||||
|
})
|
||||||
|
|
||||||
|
flex.AddItem(title, 1, 0, false).
|
||||||
|
AddItem(relTable, 0, 1, true).
|
||||||
|
AddItem(btnFlex, 1, 0, false)
|
||||||
|
|
||||||
|
se.pages.AddPage("relationships", flex, true, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// showNewRelationshipDialog shows dialog to create a new relationship
|
||||||
|
func (se *SchemaEditor) showNewRelationshipDialog(schemaIndex, tableIndex int) {
|
||||||
|
table := se.GetTable(schemaIndex, tableIndex)
|
||||||
|
if table == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form := tview.NewForm()
|
||||||
|
|
||||||
|
// Collect all tables for dropdown
|
||||||
|
var allTables []string
|
||||||
|
var tableMap []struct{ schemaIdx, tableIdx int }
|
||||||
|
for si, schema := range se.db.Schemas {
|
||||||
|
for ti, t := range schema.Tables {
|
||||||
|
tableName := t.Name
|
||||||
|
if schema.Name != table.Schema {
|
||||||
|
tableName = schema.Name + "." + t.Name
|
||||||
|
}
|
||||||
|
allTables = append(allTables, tableName)
|
||||||
|
tableMap = append(tableMap, struct{ schemaIdx, tableIdx int }{si, ti})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
relName := ""
|
||||||
|
relType := models.OneToMany
|
||||||
|
fromColumns := ""
|
||||||
|
toColumns := ""
|
||||||
|
description := ""
|
||||||
|
selectedTableIdx := 0
|
||||||
|
|
||||||
|
form.AddInputField("Name", "", 40, nil, func(value string) {
|
||||||
|
relName = value
|
||||||
|
})
|
||||||
|
|
||||||
|
form.AddDropDown("Type", []string{
|
||||||
|
string(models.OneToOne),
|
||||||
|
string(models.OneToMany),
|
||||||
|
string(models.ManyToMany),
|
||||||
|
}, 1, func(option string, optionIndex int) {
|
||||||
|
relType = models.RelationType(option)
|
||||||
|
})
|
||||||
|
|
||||||
|
form.AddInputField("From Columns (comma-separated)", "", 40, nil, func(value string) {
|
||||||
|
fromColumns = value
|
||||||
|
})
|
||||||
|
|
||||||
|
form.AddDropDown("To Table", allTables, 0, func(option string, optionIndex int) {
|
||||||
|
selectedTableIdx = optionIndex
|
||||||
|
})
|
||||||
|
|
||||||
|
form.AddInputField("To Columns (comma-separated)", "", 40, nil, func(value string) {
|
||||||
|
toColumns = value
|
||||||
|
})
|
||||||
|
|
||||||
|
form.AddInputField("Description", "", 60, nil, func(value string) {
|
||||||
|
description = value
|
||||||
|
})
|
||||||
|
|
||||||
|
form.AddButton("Save", func() {
|
||||||
|
if relName == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse columns
|
||||||
|
fromCols := strings.Split(fromColumns, ",")
|
||||||
|
for i := range fromCols {
|
||||||
|
fromCols[i] = strings.TrimSpace(fromCols[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
toCols := strings.Split(toColumns, ",")
|
||||||
|
for i := range toCols {
|
||||||
|
toCols[i] = strings.TrimSpace(toCols[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get target table
|
||||||
|
targetSchema := se.db.Schemas[tableMap[selectedTableIdx].schemaIdx]
|
||||||
|
targetTable := targetSchema.Tables[tableMap[selectedTableIdx].tableIdx]
|
||||||
|
|
||||||
|
rel := models.InitRelationship(relName, relType)
|
||||||
|
rel.FromTable = table.Name
|
||||||
|
rel.FromSchema = table.Schema
|
||||||
|
rel.FromColumns = fromCols
|
||||||
|
rel.ToTable = targetTable.Name
|
||||||
|
rel.ToSchema = targetTable.Schema
|
||||||
|
rel.ToColumns = toCols
|
||||||
|
rel.Description = description
|
||||||
|
|
||||||
|
se.CreateRelationship(schemaIndex, tableIndex, rel)
|
||||||
|
|
||||||
|
se.pages.RemovePage("new-relationship")
|
||||||
|
se.pages.RemovePage("relationships")
|
||||||
|
se.showRelationshipList(schemaIndex, tableIndex)
|
||||||
|
})
|
||||||
|
|
||||||
|
form.AddButton("Back", func() {
|
||||||
|
se.pages.RemovePage("new-relationship")
|
||||||
|
})
|
||||||
|
|
||||||
|
form.SetBorder(true).SetTitle(" New Relationship ").SetTitleAlign(tview.AlignLeft)
|
||||||
|
form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
|
if event.Key() == tcell.KeyEscape {
|
||||||
|
se.pages.RemovePage("new-relationship")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return event
|
||||||
|
})
|
||||||
|
|
||||||
|
se.pages.AddPage("new-relationship", form, true, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// showEditRelationshipDialog shows dialog to edit a relationship
|
||||||
|
func (se *SchemaEditor) showEditRelationshipDialog(schemaIndex, tableIndex int, relName string) {
|
||||||
|
table := se.GetTable(schemaIndex, tableIndex)
|
||||||
|
if table == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rel := se.GetRelationship(schemaIndex, tableIndex, relName)
|
||||||
|
if rel == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form := tview.NewForm()
|
||||||
|
|
||||||
|
// Collect all tables for dropdown
|
||||||
|
var allTables []string
|
||||||
|
var tableMap []struct{ schemaIdx, tableIdx int }
|
||||||
|
selectedTableIdx := 0
|
||||||
|
for si, schema := range se.db.Schemas {
|
||||||
|
for ti, t := range schema.Tables {
|
||||||
|
tableName := t.Name
|
||||||
|
if schema.Name != table.Schema {
|
||||||
|
tableName = schema.Name + "." + t.Name
|
||||||
|
}
|
||||||
|
allTables = append(allTables, tableName)
|
||||||
|
tableMap = append(tableMap, struct{ schemaIdx, tableIdx int }{si, ti})
|
||||||
|
|
||||||
|
// Check if this is the current target table
|
||||||
|
if t.Name == rel.ToTable && schema.Name == rel.ToSchema {
|
||||||
|
selectedTableIdx = len(allTables) - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newName := rel.Name
|
||||||
|
relType := rel.Type
|
||||||
|
fromColumns := strings.Join(rel.FromColumns, ", ")
|
||||||
|
toColumns := strings.Join(rel.ToColumns, ", ")
|
||||||
|
description := rel.Description
|
||||||
|
|
||||||
|
form.AddInputField("Name", rel.Name, 40, nil, func(value string) {
|
||||||
|
newName = value
|
||||||
|
})
|
||||||
|
|
||||||
|
// Find initial type index
|
||||||
|
typeIdx := 1 // OneToMany default
|
||||||
|
typeOptions := []string{
|
||||||
|
string(models.OneToOne),
|
||||||
|
string(models.OneToMany),
|
||||||
|
string(models.ManyToMany),
|
||||||
|
}
|
||||||
|
for i, opt := range typeOptions {
|
||||||
|
if opt == string(rel.Type) {
|
||||||
|
typeIdx = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form.AddDropDown("Type", typeOptions, typeIdx, func(option string, optionIndex int) {
|
||||||
|
relType = models.RelationType(option)
|
||||||
|
})
|
||||||
|
|
||||||
|
form.AddInputField("From Columns (comma-separated)", fromColumns, 40, nil, func(value string) {
|
||||||
|
fromColumns = value
|
||||||
|
})
|
||||||
|
|
||||||
|
form.AddDropDown("To Table", allTables, selectedTableIdx, func(option string, optionIndex int) {
|
||||||
|
selectedTableIdx = optionIndex
|
||||||
|
})
|
||||||
|
|
||||||
|
form.AddInputField("To Columns (comma-separated)", toColumns, 40, nil, func(value string) {
|
||||||
|
toColumns = value
|
||||||
|
})
|
||||||
|
|
||||||
|
form.AddInputField("Description", rel.Description, 60, nil, func(value string) {
|
||||||
|
description = value
|
||||||
|
})
|
||||||
|
|
||||||
|
form.AddButton("Save", func() {
|
||||||
|
if newName == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse columns
|
||||||
|
fromCols := strings.Split(fromColumns, ",")
|
||||||
|
for i := range fromCols {
|
||||||
|
fromCols[i] = strings.TrimSpace(fromCols[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
toCols := strings.Split(toColumns, ",")
|
||||||
|
for i := range toCols {
|
||||||
|
toCols[i] = strings.TrimSpace(toCols[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get target table
|
||||||
|
targetSchema := se.db.Schemas[tableMap[selectedTableIdx].schemaIdx]
|
||||||
|
targetTable := targetSchema.Tables[tableMap[selectedTableIdx].tableIdx]
|
||||||
|
|
||||||
|
updatedRel := models.InitRelationship(newName, relType)
|
||||||
|
updatedRel.FromTable = table.Name
|
||||||
|
updatedRel.FromSchema = table.Schema
|
||||||
|
updatedRel.FromColumns = fromCols
|
||||||
|
updatedRel.ToTable = targetTable.Name
|
||||||
|
updatedRel.ToSchema = targetTable.Schema
|
||||||
|
updatedRel.ToColumns = toCols
|
||||||
|
updatedRel.Description = description
|
||||||
|
updatedRel.GUID = rel.GUID
|
||||||
|
|
||||||
|
se.UpdateRelationship(schemaIndex, tableIndex, relName, updatedRel)
|
||||||
|
|
||||||
|
se.pages.RemovePage("edit-relationship")
|
||||||
|
se.pages.RemovePage("relationships")
|
||||||
|
se.showRelationshipList(schemaIndex, tableIndex)
|
||||||
|
})
|
||||||
|
|
||||||
|
form.AddButton("Back", func() {
|
||||||
|
se.pages.RemovePage("edit-relationship")
|
||||||
|
})
|
||||||
|
|
||||||
|
form.SetBorder(true).SetTitle(" Edit Relationship ").SetTitleAlign(tview.AlignLeft)
|
||||||
|
form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
|
if event.Key() == tcell.KeyEscape {
|
||||||
|
se.pages.RemovePage("edit-relationship")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return event
|
||||||
|
})
|
||||||
|
|
||||||
|
se.pages.AddPage("edit-relationship", form, true, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// showDeleteRelationshipConfirm shows confirmation dialog for deleting a relationship
|
||||||
|
func (se *SchemaEditor) showDeleteRelationshipConfirm(schemaIndex, tableIndex int, relName string) {
|
||||||
|
modal := tview.NewModal().
|
||||||
|
SetText(fmt.Sprintf("Delete relationship '%s'? This action cannot be undone.", relName)).
|
||||||
|
AddButtons([]string{"Cancel", "Delete"}).
|
||||||
|
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
||||||
|
if buttonLabel == "Delete" {
|
||||||
|
se.DeleteRelationship(schemaIndex, tableIndex, relName)
|
||||||
|
se.pages.RemovePage("delete-relationship-confirm")
|
||||||
|
se.pages.RemovePage("relationships")
|
||||||
|
se.showRelationshipList(schemaIndex, tableIndex)
|
||||||
|
} else {
|
||||||
|
se.pages.RemovePage("delete-relationship-confirm")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
modal.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
|
if event.Key() == tcell.KeyEscape {
|
||||||
|
se.pages.RemovePage("delete-relationship-confirm")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return event
|
||||||
|
})
|
||||||
|
|
||||||
|
se.pages.AddAndSwitchToPage("delete-relationship-confirm", modal, true)
|
||||||
|
}
|
||||||
@@ -270,6 +270,9 @@ func (se *SchemaEditor) showTableEditor(schemaIndex, tableIndex int, table *mode
|
|||||||
se.showColumnEditor(schemaIndex, tableIndex, row-1, column)
|
se.showColumnEditor(schemaIndex, tableIndex, row-1, column)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
btnRelations := tview.NewButton("Relations [r]").SetSelectedFunc(func() {
|
||||||
|
se.showRelationshipList(schemaIndex, tableIndex)
|
||||||
|
})
|
||||||
btnDelTable := tview.NewButton("Delete Table [d]").SetSelectedFunc(func() {
|
btnDelTable := tview.NewButton("Delete Table [d]").SetSelectedFunc(func() {
|
||||||
se.showDeleteTableConfirm(schemaIndex, tableIndex)
|
se.showDeleteTableConfirm(schemaIndex, tableIndex)
|
||||||
})
|
})
|
||||||
@@ -308,6 +311,18 @@ func (se *SchemaEditor) showTableEditor(schemaIndex, tableIndex int, table *mode
|
|||||||
se.app.SetFocus(btnEditColumn)
|
se.app.SetFocus(btnEditColumn)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if event.Key() == tcell.KeyTab {
|
||||||
|
se.app.SetFocus(btnRelations)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return event
|
||||||
|
})
|
||||||
|
|
||||||
|
btnRelations.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
|
if event.Key() == tcell.KeyBacktab {
|
||||||
|
se.app.SetFocus(btnEditTable)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if event.Key() == tcell.KeyTab {
|
if event.Key() == tcell.KeyTab {
|
||||||
se.app.SetFocus(btnDelTable)
|
se.app.SetFocus(btnDelTable)
|
||||||
return nil
|
return nil
|
||||||
@@ -317,7 +332,7 @@ func (se *SchemaEditor) showTableEditor(schemaIndex, tableIndex int, table *mode
|
|||||||
|
|
||||||
btnDelTable.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
btnDelTable.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
if event.Key() == tcell.KeyBacktab {
|
if event.Key() == tcell.KeyBacktab {
|
||||||
se.app.SetFocus(btnEditTable)
|
se.app.SetFocus(btnRelations)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if event.Key() == tcell.KeyTab {
|
if event.Key() == tcell.KeyTab {
|
||||||
@@ -342,6 +357,7 @@ func (se *SchemaEditor) showTableEditor(schemaIndex, tableIndex int, table *mode
|
|||||||
btnFlex.AddItem(btnNewCol, 0, 1, true).
|
btnFlex.AddItem(btnNewCol, 0, 1, true).
|
||||||
AddItem(btnEditColumn, 0, 1, false).
|
AddItem(btnEditColumn, 0, 1, false).
|
||||||
AddItem(btnEditTable, 0, 1, false).
|
AddItem(btnEditTable, 0, 1, false).
|
||||||
|
AddItem(btnRelations, 0, 1, false).
|
||||||
AddItem(btnDelTable, 0, 1, false).
|
AddItem(btnDelTable, 0, 1, false).
|
||||||
AddItem(btnBack, 0, 1, false)
|
AddItem(btnBack, 0, 1, false)
|
||||||
|
|
||||||
@@ -373,6 +389,10 @@ func (se *SchemaEditor) showTableEditor(schemaIndex, tableIndex int, table *mode
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if event.Rune() == 'r' {
|
||||||
|
se.showRelationshipList(schemaIndex, tableIndex)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if event.Rune() == 'b' {
|
if event.Rune() == 'b' {
|
||||||
se.pages.RemovePage("table-editor")
|
se.pages.RemovePage("table-editor")
|
||||||
se.pages.SwitchToPage("schema-editor")
|
se.pages.SwitchToPage("schema-editor")
|
||||||
|
|||||||
67
pkg/writers/doc.go
Normal file
67
pkg/writers/doc.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
// Package writers provides interfaces and implementations for writing database schemas
|
||||||
|
// to various output formats and destinations.
|
||||||
|
//
|
||||||
|
// # Overview
|
||||||
|
//
|
||||||
|
// The writers package defines a common Writer interface that all format-specific writers
|
||||||
|
// implement. This allows RelSpec to export database schemas to multiple formats including:
|
||||||
|
// - SQL schema files (PostgreSQL, SQLite)
|
||||||
|
// - Schema definition files (DBML, DCTX, DrawDB, GraphQL)
|
||||||
|
// - ORM model files (GORM, Bun, Drizzle, Prisma, TypeORM)
|
||||||
|
// - Data interchange formats (JSON, YAML)
|
||||||
|
//
|
||||||
|
// # Architecture
|
||||||
|
//
|
||||||
|
// Each writer implementation is located in its own subpackage (e.g., pkg/writers/dbml,
|
||||||
|
// pkg/writers/pgsql) and implements the Writer interface, supporting three levels of
|
||||||
|
// granularity:
|
||||||
|
// - WriteDatabase() - Write complete database with all schemas
|
||||||
|
// - WriteSchema() - Write single schema with all tables
|
||||||
|
// - WriteTable() - Write single table with all columns and metadata
|
||||||
|
//
|
||||||
|
// # Usage
|
||||||
|
//
|
||||||
|
// Writers are instantiated with WriterOptions containing destination-specific configuration:
|
||||||
|
//
|
||||||
|
// // Write to file
|
||||||
|
// writer := dbml.NewWriter(&writers.WriterOptions{
|
||||||
|
// OutputPath: "schema.dbml",
|
||||||
|
// })
|
||||||
|
// err := writer.WriteDatabase(db)
|
||||||
|
//
|
||||||
|
// // Write ORM models with package name
|
||||||
|
// writer := gorm.NewWriter(&writers.WriterOptions{
|
||||||
|
// OutputPath: "./models",
|
||||||
|
// PackageName: "models",
|
||||||
|
// })
|
||||||
|
// err := writer.WriteDatabase(db)
|
||||||
|
//
|
||||||
|
// // Write with schema flattening for SQLite
|
||||||
|
// writer := sqlite.NewWriter(&writers.WriterOptions{
|
||||||
|
// OutputPath: "schema.sql",
|
||||||
|
// FlattenSchema: true,
|
||||||
|
// })
|
||||||
|
// err := writer.WriteDatabase(db)
|
||||||
|
//
|
||||||
|
// # Schema Flattening
|
||||||
|
//
|
||||||
|
// The FlattenSchema option controls how schema-qualified table names are handled:
|
||||||
|
// - false (default): Uses dot notation (schema.table)
|
||||||
|
// - true: Joins with underscore (schema_table), useful for SQLite
|
||||||
|
//
|
||||||
|
// # Supported Formats
|
||||||
|
//
|
||||||
|
// - dbml: Database Markup Language files
|
||||||
|
// - dctx: DCTX schema files
|
||||||
|
// - drawdb: DrawDB JSON format
|
||||||
|
// - graphql: GraphQL schema definition language
|
||||||
|
// - json: JSON database schema
|
||||||
|
// - yaml: YAML database schema
|
||||||
|
// - gorm: Go GORM model structs
|
||||||
|
// - bun: Go Bun model structs
|
||||||
|
// - drizzle: TypeScript Drizzle ORM schemas
|
||||||
|
// - prisma: Prisma schema language
|
||||||
|
// - typeorm: TypeScript TypeORM entities
|
||||||
|
// - pgsql: PostgreSQL SQL schema
|
||||||
|
// - sqlite: SQLite SQL schema with automatic flattening
|
||||||
|
package writers
|
||||||
Reference in New Issue
Block a user