/* Copyright (C) 2019, 2020, 2021, 2022, 2023, 2024, 2025 Dnote contributors * * This file is part of Dnote. * * Dnote is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Dnote is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Dnote. If not, see . */ // Command schema generates the CLI database schema.sql file. package main import ( "fmt" "os" "path/filepath" "strings" "github.com/dnote/dnote/pkg/cli/config" "github.com/dnote/dnote/pkg/cli/context" "github.com/dnote/dnote/pkg/cli/database" "github.com/dnote/dnote/pkg/cli/infra" "github.com/dnote/dnote/pkg/cli/migrate" ) func main() { tmpDir, err := os.MkdirTemp("", "dnote-schema-gen-*") if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } defer os.RemoveAll(tmpDir) schemaPath := filepath.Join("pkg", "cli", "database", "schema.sql") if err := run(tmpDir, schemaPath); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } } func run(tmpDir, outputPath string) error { schema, err := generateSchema(tmpDir) if err != nil { return err } if err := os.WriteFile(outputPath, []byte(schema), 0644); err != nil { return fmt.Errorf("writing schema file: %w", err) } fmt.Printf("Schema generated successfully at %s\n", outputPath) return nil } // generateSchema creates a fresh database, runs all migrations, and extracts the schema func generateSchema(tmpDir string) (string, error) { // Create dnote directory structure in temp dir dnoteDir := filepath.Join(tmpDir, "dnote") if err := os.MkdirAll(dnoteDir, 0755); err != nil { return "", fmt.Errorf("creating dnote dir: %w", err) } // Use a file-based database dbPath := filepath.Join(tmpDir, "schema.db") // Create context ctx := context.DnoteCtx{ Paths: context.Paths{ Home: tmpDir, Config: tmpDir, Data: tmpDir, Cache: tmpDir, }, Version: "schema-gen", } // Open database db, err := database.Open(dbPath) if err != nil { return "", fmt.Errorf("opening database: %w", err) } defer db.Close() ctx.DB = db // Initialize database with base tables if err := infra.InitDB(ctx); err != nil { return "", fmt.Errorf("initializing database: %w", err) } // Initialize system data if err := infra.InitSystem(ctx); err != nil { return "", fmt.Errorf("initializing system: %w", err) } // Create minimal config file if err := config.Write(ctx, config.Config{}); err != nil { return "", fmt.Errorf("writing initial config: %w", err) } // Run all local migrations if err := migrate.Run(ctx, migrate.LocalSequence, migrate.LocalMode); err != nil { return "", fmt.Errorf("running migrations: %w", err) } // Extract schema before closing database schema, err := extractSchema(db) if err != nil { return "", fmt.Errorf("extracting schema: %w", err) } return schema, nil } // extractSchema extracts the complete schema by querying sqlite_master func extractSchema(db *database.DB) (string, error) { // Query sqlite_master for all schema objects, excluding FTS shadow tables // FTS shadow tables are internal tables automatically created by FTS virtual tables rows, err := db.Conn.Query(`SELECT sql FROM sqlite_master WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%' AND (type != 'table' OR (type = 'table' AND name NOT IN ( SELECT m1.name FROM sqlite_master m1 JOIN sqlite_master m2 ON m1.name LIKE m2.name || '_%' WHERE m2.type = 'table' AND m2.sql LIKE '%VIRTUAL TABLE%' )))`) if err != nil { return "", fmt.Errorf("querying sqlite_master: %w", err) } defer rows.Close() var schemas []string for rows.Next() { var sql string if err := rows.Scan(&sql); err != nil { return "", fmt.Errorf("scanning row: %w", err) } schemas = append(schemas, sql) } if err := rows.Err(); err != nil { return "", fmt.Errorf("iterating rows: %w", err) } // Add autogenerated header comment header := `-- This is the final state of the CLI database after all migrations. -- Auto-generated by generate-schema.go. Do not edit manually. ` return header + strings.Join(schemas, ";\n") + ";\n", nil }