dnote/pkg/cli/database/schema/main.go
2025-10-31 23:41:21 -07:00

163 lines
4.7 KiB
Go

/* Copyright 2025 Dnote Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 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/consts"
"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)
}
// Add INSERT statements for migration versions.
systemData := "\n-- Migration version data.\n"
systemData += fmt.Sprintf("INSERT INTO system (key, value) VALUES ('%s', %d);\n", consts.SystemSchema, len(migrate.LocalSequence))
systemData += fmt.Sprintf("INSERT INTO system (key, value) VALUES ('%s', %d);\n", consts.SystemRemoteSchema, len(migrate.RemoteSequence))
return schema + systemData, 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
}