Some checks failed
CI / Test (1.24) (push) Successful in -27m40s
CI / Test (1.25) (push) Successful in -27m39s
CI / Build (push) Successful in -28m9s
CI / Lint (push) Successful in -27m56s
Integration Tests / Integration Tests (push) Failing after -28m11s
Release / Build and Release (push) Successful in -26m13s
- Introduce 'split' command to extract selected tables and schemas. - Supports various input and output formats. - Allows filtering of schemas and tables during extraction.
319 lines
10 KiB
Go
319 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
|
)
|
|
|
|
var (
|
|
splitSourceType string
|
|
splitSourcePath string
|
|
splitSourceConn string
|
|
splitTargetType string
|
|
splitTargetPath string
|
|
splitSchemas string
|
|
splitTables string
|
|
splitPackageName string
|
|
splitDatabaseName string
|
|
splitExcludeSchema string
|
|
splitExcludeTables string
|
|
)
|
|
|
|
var splitCmd = &cobra.Command{
|
|
Use: "split",
|
|
Short: "Split database schemas to extract selected tables into a separate database",
|
|
Long: `Extract selected schemas and tables from a database and write them to a separate output.
|
|
|
|
The split command allows you to:
|
|
- Select specific schemas to include in the output
|
|
- Select specific tables within schemas
|
|
- Exclude specific schemas or tables if preferred
|
|
- Export the selected subset to any supported format
|
|
|
|
Input formats:
|
|
- dbml: DBML schema files
|
|
- dctx: DCTX schema files
|
|
- drawdb: DrawDB JSON files
|
|
- graphql: GraphQL schema files (.graphql, SDL)
|
|
- json: JSON database schema
|
|
- yaml: YAML database schema
|
|
- gorm: GORM model files (Go, file or directory)
|
|
- bun: Bun model files (Go, file or directory)
|
|
- drizzle: Drizzle ORM schema files (TypeScript, file or directory)
|
|
- prisma: Prisma schema files (.prisma)
|
|
- typeorm: TypeORM entity files (TypeScript)
|
|
- pgsql: PostgreSQL database (live connection)
|
|
|
|
Output formats:
|
|
- dbml: DBML schema files
|
|
- dctx: DCTX schema files
|
|
- drawdb: DrawDB JSON files
|
|
- graphql: GraphQL schema files (.graphql, SDL)
|
|
- json: JSON database schema
|
|
- yaml: YAML database schema
|
|
- gorm: GORM model files (Go)
|
|
- bun: Bun model files (Go)
|
|
- drizzle: Drizzle ORM schema files (TypeScript)
|
|
- prisma: Prisma schema files (.prisma)
|
|
- typeorm: TypeORM entity files (TypeScript)
|
|
- pgsql: PostgreSQL SQL schema
|
|
|
|
Examples:
|
|
# Split specific schemas from DBML
|
|
relspec split --from dbml --from-path schema.dbml \
|
|
--schemas public,auth \
|
|
--to json --to-path subset.json
|
|
|
|
# Extract specific tables from PostgreSQL
|
|
relspec split --from pgsql \
|
|
--from-conn "postgres://user:pass@localhost:5432/mydb" \
|
|
--schemas public \
|
|
--tables users,orders,products \
|
|
--to dbml --to-path subset.dbml
|
|
|
|
# Exclude specific tables
|
|
relspec split --from json --from-path schema.json \
|
|
--exclude-tables "audit_log,system_config,temp_data" \
|
|
--to json --to-path public_schema.json
|
|
|
|
# Split and convert to GORM
|
|
relspec split --from json --from-path schema.json \
|
|
--tables "users,posts,comments" \
|
|
--to gorm --to-path models/ --package models \
|
|
--database-name MyAppDB
|
|
|
|
# Exclude specific schema and tables
|
|
relspec split --from pgsql \
|
|
--from-conn "postgres://user:pass@localhost/db" \
|
|
--exclude-schema pg_catalog,information_schema \
|
|
--exclude-tables "temp_users,debug_logs" \
|
|
--to json --to-path public_schema.json`,
|
|
RunE: runSplit,
|
|
}
|
|
|
|
func init() {
|
|
splitCmd.Flags().StringVar(&splitSourceType, "from", "", "Source format (dbml, dctx, drawdb, graphql, json, yaml, gorm, bun, drizzle, prisma, typeorm, pgsql)")
|
|
splitCmd.Flags().StringVar(&splitSourcePath, "from-path", "", "Source file path (for file-based formats)")
|
|
splitCmd.Flags().StringVar(&splitSourceConn, "from-conn", "", "Source connection string (for database formats)")
|
|
|
|
splitCmd.Flags().StringVar(&splitTargetType, "to", "", "Target format (dbml, dctx, drawdb, graphql, json, yaml, gorm, bun, drizzle, prisma, typeorm, pgsql)")
|
|
splitCmd.Flags().StringVar(&splitTargetPath, "to-path", "", "Target output path (file or directory)")
|
|
splitCmd.Flags().StringVar(&splitPackageName, "package", "", "Package name (for code generation formats like gorm/bun)")
|
|
splitCmd.Flags().StringVar(&splitDatabaseName, "database-name", "", "Override database name in output")
|
|
|
|
splitCmd.Flags().StringVar(&splitSchemas, "schemas", "", "Comma-separated list of schema names to include")
|
|
splitCmd.Flags().StringVar(&splitTables, "tables", "", "Comma-separated list of table names to include (case-insensitive)")
|
|
splitCmd.Flags().StringVar(&splitExcludeSchema, "exclude-schema", "", "Comma-separated list of schema names to exclude")
|
|
splitCmd.Flags().StringVar(&splitExcludeTables, "exclude-tables", "", "Comma-separated list of table names to exclude (case-insensitive)")
|
|
|
|
err := splitCmd.MarkFlagRequired("from")
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error marking from flag as required: %v\n", err)
|
|
}
|
|
err = splitCmd.MarkFlagRequired("to")
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error marking to flag as required: %v\n", err)
|
|
}
|
|
err = splitCmd.MarkFlagRequired("to-path")
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error marking to-path flag as required: %v\n", err)
|
|
}
|
|
}
|
|
|
|
func runSplit(cmd *cobra.Command, args []string) error {
|
|
fmt.Fprintf(os.Stderr, "\n=== RelSpec Schema Split ===\n")
|
|
fmt.Fprintf(os.Stderr, "Started at: %s\n\n", getCurrentTimestamp())
|
|
|
|
// Read source database
|
|
fmt.Fprintf(os.Stderr, "[1/3] Reading source schema...\n")
|
|
fmt.Fprintf(os.Stderr, " Format: %s\n", splitSourceType)
|
|
if splitSourcePath != "" {
|
|
fmt.Fprintf(os.Stderr, " Path: %s\n", splitSourcePath)
|
|
}
|
|
if splitSourceConn != "" {
|
|
fmt.Fprintf(os.Stderr, " Conn: %s\n", maskPassword(splitSourceConn))
|
|
}
|
|
|
|
db, err := readDatabaseForConvert(splitSourceType, splitSourcePath, splitSourceConn)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read source: %w", err)
|
|
}
|
|
|
|
fmt.Fprintf(os.Stderr, " ✓ Successfully read database '%s'\n", db.Name)
|
|
fmt.Fprintf(os.Stderr, " Found: %d schema(s)\n", len(db.Schemas))
|
|
|
|
totalTables := 0
|
|
for _, schema := range db.Schemas {
|
|
totalTables += len(schema.Tables)
|
|
}
|
|
fmt.Fprintf(os.Stderr, " Found: %d table(s)\n\n", totalTables)
|
|
|
|
// Filter the database
|
|
fmt.Fprintf(os.Stderr, "[2/3] Filtering schemas and tables...\n")
|
|
filteredDB, err := filterDatabase(db)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to filter database: %w", err)
|
|
}
|
|
|
|
if splitDatabaseName != "" {
|
|
filteredDB.Name = splitDatabaseName
|
|
}
|
|
|
|
filteredTables := 0
|
|
for _, schema := range filteredDB.Schemas {
|
|
filteredTables += len(schema.Tables)
|
|
}
|
|
fmt.Fprintf(os.Stderr, " ✓ Filtered to: %d schema(s), %d table(s)\n\n", len(filteredDB.Schemas), filteredTables)
|
|
|
|
// Write to target format
|
|
fmt.Fprintf(os.Stderr, "[3/3] Writing to target format...\n")
|
|
fmt.Fprintf(os.Stderr, " Format: %s\n", splitTargetType)
|
|
fmt.Fprintf(os.Stderr, " Output: %s\n", splitTargetPath)
|
|
if splitPackageName != "" {
|
|
fmt.Fprintf(os.Stderr, " Package: %s\n", splitPackageName)
|
|
}
|
|
|
|
err = writeDatabase(
|
|
filteredDB,
|
|
splitTargetType,
|
|
splitTargetPath,
|
|
splitPackageName,
|
|
"", // no schema filter for split
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write output: %w", err)
|
|
}
|
|
|
|
fmt.Fprintf(os.Stderr, " ✓ Successfully written to '%s'\n\n", splitTargetPath)
|
|
fmt.Fprintf(os.Stderr, "=== Split Completed Successfully ===\n")
|
|
fmt.Fprintf(os.Stderr, "Completed at: %s\n\n", getCurrentTimestamp())
|
|
|
|
return nil
|
|
}
|
|
|
|
// filterDatabase filters the database based on provided criteria
|
|
func filterDatabase(db *models.Database) (*models.Database, error) {
|
|
filteredDB := &models.Database{
|
|
Name: db.Name,
|
|
Description: db.Description,
|
|
Comment: db.Comment,
|
|
DatabaseType: db.DatabaseType,
|
|
DatabaseVersion: db.DatabaseVersion,
|
|
SourceFormat: db.SourceFormat,
|
|
UpdatedAt: db.UpdatedAt,
|
|
GUID: db.GUID,
|
|
Schemas: []*models.Schema{},
|
|
Domains: db.Domains, // Keep domains for now
|
|
}
|
|
|
|
// Parse filter flags
|
|
includeSchemas := parseCommaSeparated(splitSchemas)
|
|
includeTables := parseCommaSeparated(splitTables)
|
|
excludeSchemas := parseCommaSeparated(splitExcludeSchema)
|
|
excludeTables := parseCommaSeparated(splitExcludeTables)
|
|
|
|
// Convert table names to lowercase for case-insensitive matching
|
|
includeTablesLower := make(map[string]bool)
|
|
for _, t := range includeTables {
|
|
includeTablesLower[strings.ToLower(t)] = true
|
|
}
|
|
|
|
excludeTablesLower := make(map[string]bool)
|
|
for _, t := range excludeTables {
|
|
excludeTablesLower[strings.ToLower(t)] = true
|
|
}
|
|
|
|
// Iterate through schemas
|
|
for _, schema := range db.Schemas {
|
|
// Check if schema should be excluded
|
|
if contains(excludeSchemas, schema.Name) {
|
|
continue
|
|
}
|
|
|
|
// Check if schema should be included
|
|
if len(includeSchemas) > 0 && !contains(includeSchemas, schema.Name) {
|
|
continue
|
|
}
|
|
|
|
// Create a copy of the schema with filtered tables
|
|
filteredSchema := &models.Schema{
|
|
Name: schema.Name,
|
|
Description: schema.Description,
|
|
Owner: schema.Owner,
|
|
Permissions: schema.Permissions,
|
|
Comment: schema.Comment,
|
|
Metadata: schema.Metadata,
|
|
Scripts: schema.Scripts,
|
|
Sequence: schema.Sequence,
|
|
Relations: schema.Relations,
|
|
Enums: schema.Enums,
|
|
UpdatedAt: schema.UpdatedAt,
|
|
GUID: schema.GUID,
|
|
Tables: []*models.Table{},
|
|
Views: schema.Views,
|
|
Sequences: schema.Sequences,
|
|
}
|
|
|
|
// Filter tables within the schema
|
|
for _, table := range schema.Tables {
|
|
tableLower := strings.ToLower(table.Name)
|
|
|
|
// Check if table should be excluded
|
|
if excludeTablesLower[tableLower] {
|
|
continue
|
|
}
|
|
|
|
// If specific tables are requested, only include those
|
|
if len(includeTablesLower) > 0 {
|
|
if !includeTablesLower[tableLower] {
|
|
continue
|
|
}
|
|
}
|
|
filteredSchema.Tables = append(filteredSchema.Tables, table)
|
|
}
|
|
|
|
// Only add schema if it has tables (unless no table filter was specified)
|
|
if len(filteredSchema.Tables) > 0 || (len(includeTablesLower) == 0 && len(excludeTablesLower) == 0) {
|
|
filteredDB.Schemas = append(filteredDB.Schemas, filteredSchema)
|
|
}
|
|
}
|
|
|
|
if len(filteredDB.Schemas) == 0 {
|
|
return nil, fmt.Errorf("no schemas matched the filter criteria")
|
|
}
|
|
|
|
return filteredDB, nil
|
|
}
|
|
|
|
// parseCommaSeparated parses a comma-separated string into a slice, trimming whitespace
|
|
func parseCommaSeparated(s string) []string {
|
|
if s == "" {
|
|
return []string{}
|
|
}
|
|
|
|
parts := strings.Split(s, ",")
|
|
result := make([]string, 0, len(parts))
|
|
for _, p := range parts {
|
|
trimmed := strings.TrimSpace(p)
|
|
if trimmed != "" {
|
|
result = append(result, trimmed)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// contains checks if a string is in a slice
|
|
func contains(slice []string, item string) bool {
|
|
for _, s := range slice {
|
|
if s == item {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|