mirror of
https://github.com/dnote/dnote
synced 2026-03-14 22:45:50 +01:00
166 lines
4.9 KiB
Go
166 lines
4.9 KiB
Go
/* 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
// 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
|
|
}
|