mirror of
https://github.com/dnote/dnote
synced 2026-03-14 14:35:50 +01:00
163 lines
4.7 KiB
Go
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
|
|
}
|