mirror of
https://github.com/dnote/dnote
synced 2026-03-16 23:45:52 +01:00
Write migration and test
This commit is contained in:
parent
78d941da90
commit
4fe23fe996
21 changed files with 1435 additions and 1185 deletions
160
pkg/cli/database/cmd/generate-schema.go
Normal file
160
pkg/cli/database/cmd/generate-schema.go
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
/* 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/>.
|
||||
*/
|
||||
|
||||
// generate-schema creates a 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
|
||||
}
|
||||
76
pkg/cli/database/cmd/generate-schema_test.go
Normal file
76
pkg/cli/database/cmd/generate-schema_test.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
/* 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/>.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/dnote/dnote/pkg/assert"
|
||||
)
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
outputPath := filepath.Join(tmpDir, "schema.sql")
|
||||
|
||||
// Run the function
|
||||
if err := run(tmpDir, outputPath); err != nil {
|
||||
t.Fatalf("run() failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify schema.sql was created
|
||||
content, err := os.ReadFile(outputPath)
|
||||
if err != nil {
|
||||
t.Fatalf("reading schema.sql: %v", err)
|
||||
}
|
||||
|
||||
schema := string(content)
|
||||
|
||||
// Verify it has the header
|
||||
assert.Equal(t, strings.HasPrefix(schema, "-- This is the final state"), true, "schema.sql should have header comment")
|
||||
|
||||
// Verify schema contains expected tables
|
||||
expectedTables := []string{
|
||||
"CREATE TABLE books",
|
||||
"CREATE TABLE system",
|
||||
"CREATE TABLE \"notes\"",
|
||||
"CREATE VIRTUAL TABLE note_fts",
|
||||
}
|
||||
|
||||
for _, expected := range expectedTables {
|
||||
assert.Equal(t, strings.Contains(schema, expected), true, fmt.Sprintf("schema should contain %s", expected))
|
||||
}
|
||||
|
||||
// Verify schema contains triggers
|
||||
expectedTriggers := []string{
|
||||
"CREATE TRIGGER notes_after_insert",
|
||||
"CREATE TRIGGER notes_after_delete",
|
||||
"CREATE TRIGGER notes_after_update",
|
||||
}
|
||||
|
||||
for _, expected := range expectedTriggers {
|
||||
assert.Equal(t, strings.Contains(schema, expected), true, fmt.Sprintf("schema should contain %s", expected))
|
||||
}
|
||||
|
||||
// Verify schema does not contain sqlite internal tables
|
||||
assert.Equal(t, strings.Contains(schema, "sqlite_sequence"), false, "schema should not contain sqlite_sequence")
|
||||
}
|
||||
|
|
@ -109,8 +109,8 @@ func TestNoteInsert(t *testing.T) {
|
|||
for idx, tc := range testCases {
|
||||
func() {
|
||||
// Setup
|
||||
db := InitTestDB(t, "../tmp/dnote-test.db", nil)
|
||||
defer TeardownTestDB(t, db)
|
||||
db := InitTestMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
n := Note{
|
||||
UUID: tc.uuid,
|
||||
|
|
@ -243,8 +243,8 @@ func TestNoteUpdate(t *testing.T) {
|
|||
for idx, tc := range testCases {
|
||||
func() {
|
||||
// Setup
|
||||
db := InitTestDB(t, "../tmp/dnote-test.db", nil)
|
||||
defer TeardownTestDB(t, db)
|
||||
db := InitTestMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
n1 := Note{
|
||||
UUID: tc.uuid,
|
||||
|
|
@ -335,8 +335,8 @@ func TestNoteUpdateUUID(t *testing.T) {
|
|||
for idx, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("testCase%d", idx), func(t *testing.T) {
|
||||
// Setup
|
||||
db := InitTestDB(t, "../tmp/dnote-test.db", nil)
|
||||
defer TeardownTestDB(t, db)
|
||||
db := InitTestMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
n1 := Note{
|
||||
UUID: "n1-uuid",
|
||||
|
|
@ -390,8 +390,8 @@ func TestNoteUpdateUUID(t *testing.T) {
|
|||
|
||||
func TestNoteExpunge(t *testing.T) {
|
||||
// Setup
|
||||
db := InitTestDB(t, "../tmp/dnote-test.db", nil)
|
||||
defer TeardownTestDB(t, db)
|
||||
db := InitTestMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
n1 := Note{
|
||||
UUID: "n1-uuid",
|
||||
|
|
@ -513,8 +513,8 @@ func TestBookInsert(t *testing.T) {
|
|||
for idx, tc := range testCases {
|
||||
func() {
|
||||
// Setup
|
||||
db := InitTestDB(t, "../tmp/dnote-test.db", nil)
|
||||
defer TeardownTestDB(t, db)
|
||||
db := InitTestMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
b := Book{
|
||||
UUID: tc.uuid,
|
||||
|
|
@ -594,8 +594,8 @@ func TestBookUpdate(t *testing.T) {
|
|||
for idx, tc := range testCases {
|
||||
func() {
|
||||
// Setup
|
||||
db := InitTestDB(t, "../tmp/dnote-test.db", nil)
|
||||
defer TeardownTestDB(t, db)
|
||||
db := InitTestMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
b1 := Book{
|
||||
UUID: "b1-uuid",
|
||||
|
|
@ -673,8 +673,8 @@ func TestBookUpdateUUID(t *testing.T) {
|
|||
t.Run(fmt.Sprintf("testCase%d", idx), func(t *testing.T) {
|
||||
|
||||
// Setup
|
||||
db := InitTestDB(t, "../tmp/dnote-test.db", nil)
|
||||
defer TeardownTestDB(t, db)
|
||||
db := InitTestMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
b1 := Book{
|
||||
UUID: "b1-uuid",
|
||||
|
|
@ -724,8 +724,8 @@ func TestBookUpdateUUID(t *testing.T) {
|
|||
|
||||
func TestBookExpunge(t *testing.T) {
|
||||
// Setup
|
||||
db := InitTestDB(t, "../tmp/dnote-test.db", nil)
|
||||
defer TeardownTestDB(t, db)
|
||||
db := InitTestMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
b1 := Book{
|
||||
UUID: "b1-uuid",
|
||||
|
|
@ -779,8 +779,8 @@ func TestBookExpunge(t *testing.T) {
|
|||
// TestNoteFTS tests that note full text search indices stay in sync with the notes after insert, update and delete
|
||||
func TestNoteFTS(t *testing.T) {
|
||||
// set up
|
||||
db := InitTestDB(t, "../tmp/dnote-test.db", nil)
|
||||
defer TeardownTestDB(t, db)
|
||||
db := InitTestMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
// execute - insert
|
||||
n := Note{
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@ func TestInsertSystem(t *testing.T) {
|
|||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("insert %s %s", tc.key, tc.val), func(t *testing.T) {
|
||||
// Setup
|
||||
db := InitTestDB(t, "../tmp/dnote-test.db", nil)
|
||||
defer TeardownTestDB(t, db)
|
||||
db := InitTestMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
// execute
|
||||
tx, err := db.Begin()
|
||||
|
|
@ -95,8 +95,8 @@ func TestUpsertSystem(t *testing.T) {
|
|||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("insert %s %s", tc.key, tc.val), func(t *testing.T) {
|
||||
// Setup
|
||||
db := InitTestDB(t, "../tmp/dnote-test.db", nil)
|
||||
defer TeardownTestDB(t, db)
|
||||
db := InitTestMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
MustExec(t, "inserting a system configuration", db, "INSERT INTO system (key, value) VALUES (?, ?)", "baz", "quz")
|
||||
|
||||
|
|
@ -134,8 +134,8 @@ func TestUpsertSystem(t *testing.T) {
|
|||
func TestGetSystem(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("get string value"), func(t *testing.T) {
|
||||
// Setup
|
||||
db := InitTestDB(t, "../tmp/dnote-test.db", nil)
|
||||
defer TeardownTestDB(t, db)
|
||||
db := InitTestMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
// execute
|
||||
MustExec(t, "inserting a system configuration", db, "INSERT INTO system (key, value) VALUES (?, ?)", "foo", "bar")
|
||||
|
|
@ -157,8 +157,8 @@ func TestGetSystem(t *testing.T) {
|
|||
|
||||
t.Run(fmt.Sprintf("get int64 value"), func(t *testing.T) {
|
||||
// Setup
|
||||
db := InitTestDB(t, "../tmp/dnote-test.db", nil)
|
||||
defer TeardownTestDB(t, db)
|
||||
db := InitTestMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
// execute
|
||||
MustExec(t, "inserting a system configuration", db, "INSERT INTO system (key, value) VALUES (?, ?)", "foo", 1234)
|
||||
|
|
@ -198,8 +198,8 @@ func TestUpdateSystem(t *testing.T) {
|
|||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("update %s %s", tc.key, tc.val), func(t *testing.T) {
|
||||
// Setup
|
||||
db := InitTestDB(t, "../tmp/dnote-test.db", nil)
|
||||
defer TeardownTestDB(t, db)
|
||||
db := InitTestMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
MustExec(t, "inserting a system configuration", db, "INSERT INTO system (key, value) VALUES (?, ?)", "foo", "fuz")
|
||||
MustExec(t, "inserting a system configuration", db, "INSERT INTO system (key, value) VALUES (?, ?)", "baz", "quz")
|
||||
|
|
@ -238,8 +238,8 @@ func TestUpdateSystem(t *testing.T) {
|
|||
func TestGetActiveNote(t *testing.T) {
|
||||
t.Run("not deleted", func(t *testing.T) {
|
||||
// set up
|
||||
db := InitTestDB(t, "../tmp/dnote-test.db", nil)
|
||||
defer TeardownTestDB(t, db)
|
||||
db := InitTestMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
n1UUID := "n1-uuid"
|
||||
MustExec(t, "inserting n1", db, "INSERT INTO notes (uuid, book_uuid, body, added_on, edited_on, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", n1UUID, "b1-uuid", "n1 content", 1542058875, 1542058876, 1, false, true)
|
||||
|
|
@ -267,8 +267,8 @@ func TestGetActiveNote(t *testing.T) {
|
|||
|
||||
t.Run("deleted", func(t *testing.T) {
|
||||
// set up
|
||||
db := InitTestDB(t, "../tmp/dnote-test.db", nil)
|
||||
defer TeardownTestDB(t, db)
|
||||
db := InitTestMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
n1UUID := "n1-uuid"
|
||||
MustExec(t, "inserting n1", db, "INSERT INTO notes (uuid, book_uuid, body, added_on, edited_on, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", n1UUID, "b1-uuid", "n1 content", 1542058875, 1542058876, 1, true, true)
|
||||
|
|
@ -291,8 +291,8 @@ func TestGetActiveNote(t *testing.T) {
|
|||
|
||||
func TestUpdateNoteContent(t *testing.T) {
|
||||
// set up
|
||||
db := InitTestDB(t, "../tmp/dnote-test.db", nil)
|
||||
defer TeardownTestDB(t, db)
|
||||
db := InitTestMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
uuid := "n1-uuid"
|
||||
MustExec(t, "inserting n1", db, "INSERT INTO notes (uuid, book_uuid, body, added_on, edited_on, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", uuid, "b1-uuid", "n1 content", 1542058875, 0, 1, false, false)
|
||||
|
|
@ -323,8 +323,8 @@ func TestUpdateNoteContent(t *testing.T) {
|
|||
|
||||
func TestUpdateNoteBook(t *testing.T) {
|
||||
// set up
|
||||
db := InitTestDB(t, "../tmp/dnote-test.db", nil)
|
||||
defer TeardownTestDB(t, db)
|
||||
db := InitTestMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
b1UUID := "b1-uuid"
|
||||
b2UUID := "b2-uuid"
|
||||
|
|
@ -360,8 +360,8 @@ func TestUpdateNoteBook(t *testing.T) {
|
|||
|
||||
func TestUpdateBookName(t *testing.T) {
|
||||
// set up
|
||||
db := InitTestDB(t, "../tmp/dnote-test.db", nil)
|
||||
defer TeardownTestDB(t, db)
|
||||
db := InitTestMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
b1UUID := "b1-uuid"
|
||||
MustExec(t, "inserting b1", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", b1UUID, "b1-label", 8, false, false)
|
||||
|
|
|
|||
36
pkg/cli/database/schema.sql
Normal file
36
pkg/cli/database/schema.sql
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
-- This is the final state of the CLI database after all migrations.
|
||||
-- Auto-generated by generate-schema.go. Do not edit manually.
|
||||
CREATE TABLE books
|
||||
(
|
||||
uuid text PRIMARY KEY,
|
||||
label text NOT NULL
|
||||
, dirty bool DEFAULT false, usn int DEFAULT 0 NOT NULL, deleted bool DEFAULT false);
|
||||
CREATE TABLE system
|
||||
(
|
||||
key string NOT NULL,
|
||||
value text NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_books_label ON books(label);
|
||||
CREATE UNIQUE INDEX idx_books_uuid ON books(uuid);
|
||||
CREATE TABLE "notes"
|
||||
(
|
||||
uuid text NOT NULL,
|
||||
book_uuid text NOT NULL,
|
||||
body text NOT NULL,
|
||||
added_on integer NOT NULL,
|
||||
edited_on integer DEFAULT 0,
|
||||
dirty bool DEFAULT false,
|
||||
usn int DEFAULT 0 NOT NULL,
|
||||
deleted bool DEFAULT false
|
||||
);
|
||||
CREATE VIRTUAL TABLE note_fts USING fts5(content=notes, body, tokenize="porter unicode61 categories 'L* N* Co Ps Pe'");
|
||||
CREATE TRIGGER notes_after_insert AFTER INSERT ON notes BEGIN
|
||||
INSERT INTO note_fts(rowid, body) VALUES (new.rowid, new.body);
|
||||
END;
|
||||
CREATE TRIGGER notes_after_delete AFTER DELETE ON notes BEGIN
|
||||
INSERT INTO note_fts(note_fts, rowid, body) VALUES ('delete', old.rowid, old.body);
|
||||
END;
|
||||
CREATE TRIGGER notes_after_update AFTER UPDATE ON notes BEGIN
|
||||
INSERT INTO note_fts(note_fts, rowid, body) VALUES ('delete', old.rowid, old.body);
|
||||
INSERT INTO note_fts(rowid, body) VALUES (new.rowid, new.body);
|
||||
END;
|
||||
27
pkg/cli/database/test_embed.go
Normal file
27
pkg/cli/database/test_embed.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSchemaEmbed(t *testing.T) {
|
||||
db := InitTestMemoryDB(t)
|
||||
defer db.Close()
|
||||
|
||||
// Try to insert a book to verify schema is loaded
|
||||
_, err := db.Exec("INSERT INTO books (uuid, label) VALUES (?, ?)", "test-uuid", "test-label")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to insert into books: %v", err)
|
||||
}
|
||||
|
||||
// Verify it was inserted
|
||||
var label string
|
||||
err = db.QueryRow("SELECT label FROM books WHERE uuid = ?", "test-uuid").Scan(&label)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to query books: %v", err)
|
||||
}
|
||||
if label != "test-label" {
|
||||
t.Fatalf("Expected label 'test-label', got '%s'", label)
|
||||
}
|
||||
t.Log("Schema embed test passed!")
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ package database
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
|
@ -30,55 +31,13 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var defaultSchemaSQL = `CREATE TABLE books
|
||||
(
|
||||
uuid text PRIMARY KEY,
|
||||
label text NOT NULL
|
||||
, dirty bool DEFAULT false, usn int DEFAULT 0 NOT NULL, deleted bool DEFAULT false);
|
||||
CREATE TABLE system
|
||||
(
|
||||
key string NOT NULL,
|
||||
value text NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_books_label ON books(label);
|
||||
CREATE UNIQUE INDEX idx_books_uuid ON books(uuid);
|
||||
CREATE TABLE IF NOT EXISTS "notes"
|
||||
(
|
||||
uuid text NOT NULL,
|
||||
book_uuid text NOT NULL,
|
||||
body text NOT NULL,
|
||||
added_on integer NOT NULL,
|
||||
edited_on integer DEFAULT 0,
|
||||
dirty bool DEFAULT false,
|
||||
usn int DEFAULT 0 NOT NULL,
|
||||
deleted bool DEFAULT false
|
||||
);
|
||||
CREATE VIRTUAL TABLE note_fts USING fts5(content=notes, body, tokenize="porter unicode61 categories 'L* N* Co Ps Pe'")
|
||||
/* note_fts(body) */;
|
||||
CREATE TABLE IF NOT EXISTS 'note_fts_data'(id INTEGER PRIMARY KEY, block BLOB);
|
||||
CREATE TABLE IF NOT EXISTS 'note_fts_idx'(segid, term, pgno, PRIMARY KEY(segid, term)) WITHOUT ROWID;
|
||||
CREATE TABLE IF NOT EXISTS 'note_fts_docsize'(id INTEGER PRIMARY KEY, sz BLOB);
|
||||
CREATE TABLE IF NOT EXISTS 'note_fts_config'(k PRIMARY KEY, v) WITHOUT ROWID;
|
||||
CREATE TRIGGER notes_after_insert AFTER INSERT ON notes BEGIN
|
||||
INSERT INTO note_fts(rowid, body) VALUES (new.rowid, new.body);
|
||||
END;
|
||||
CREATE TRIGGER notes_after_delete AFTER DELETE ON notes BEGIN
|
||||
INSERT INTO note_fts(note_fts, rowid, body) VALUES ('delete', old.rowid, old.body);
|
||||
END;
|
||||
CREATE TRIGGER notes_after_update AFTER UPDATE ON notes BEGIN
|
||||
INSERT INTO note_fts(note_fts, rowid, body) VALUES ('delete', old.rowid, old.body);
|
||||
INSERT INTO note_fts(rowid, body) VALUES (new.rowid, new.body);
|
||||
END;
|
||||
CREATE TABLE actions
|
||||
(
|
||||
uuid text PRIMARY KEY,
|
||||
schema integer NOT NULL,
|
||||
type text NOT NULL,
|
||||
data text NOT NULL,
|
||||
timestamp integer NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_notes_uuid ON notes(uuid);
|
||||
CREATE INDEX idx_notes_book_uuid ON notes(book_uuid);`
|
||||
//go:embed schema.sql
|
||||
var defaultSchemaSQL string
|
||||
|
||||
// GetDefaultSchemaSQL returns the default schema SQL for tests
|
||||
func GetDefaultSchemaSQL() string {
|
||||
return defaultSchemaSQL
|
||||
}
|
||||
|
||||
// MustScan scans the given row and fails a test in case of any errors
|
||||
func MustScan(t *testing.T, message string, row *sql.Row, args ...interface{}) {
|
||||
|
|
@ -98,29 +57,50 @@ func MustExec(t *testing.T, message string, db *DB, query string, args ...interf
|
|||
return result
|
||||
}
|
||||
|
||||
// TestDBOptions contains options for test database
|
||||
type TestDBOptions struct {
|
||||
SchemaSQLPath string
|
||||
SkipMigration bool
|
||||
// InitTestMemoryDB initializes an in-memory test database with the default schema.
|
||||
func InitTestMemoryDB(t *testing.T) *DB {
|
||||
db := InitTestMemoryDBRaw(t, "")
|
||||
MarkMigrationComplete(t, db)
|
||||
return db
|
||||
}
|
||||
|
||||
// InitTestDB initializes a test database and opens connection to it
|
||||
func InitTestDB(t *testing.T, dbPath string, options *TestDBOptions) *DB {
|
||||
// InitTestFileDB initializes a file-based test database with the default schema.
|
||||
func InitTestFileDB(t *testing.T) (*DB, string) {
|
||||
dbPath := filepath.Join(t.TempDir(), "dnote.db")
|
||||
|
||||
return InitTestFileDBRaw(t, dbPath), dbPath
|
||||
}
|
||||
|
||||
// InitTestFileDBRaw initializes a file-based test database at the specified path with the default schema.
|
||||
func InitTestFileDBRaw(t *testing.T, dbPath string) *DB {
|
||||
db, err := Open(dbPath)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "opening database connection"))
|
||||
t.Fatal(errors.Wrap(err, "opening database"))
|
||||
}
|
||||
|
||||
dir, _ := filepath.Split(dbPath)
|
||||
err = os.MkdirAll(dir, 0777)
|
||||
if _, err := db.Exec(defaultSchemaSQL); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "running schema sql"))
|
||||
}
|
||||
|
||||
MarkMigrationComplete(t, db)
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
// InitTestMemoryDBRaw initializes an in-memory test database without marking migrations complete.
|
||||
// If schemaPath is empty, uses the default schema. Used for migration testing.
|
||||
func InitTestMemoryDBRaw(t *testing.T, schemaPath string) *DB {
|
||||
uuid := mustGenerateTestUUID(t)
|
||||
dbName := fmt.Sprintf("file:%s?mode=memory&cache=shared", uuid)
|
||||
|
||||
db, err := Open(dbName)
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "creating the directory for test database file"))
|
||||
t.Fatal(errors.Wrap(err, "opening in-memory database"))
|
||||
}
|
||||
|
||||
var schemaSQL string
|
||||
if options != nil && options.SchemaSQLPath != "" {
|
||||
b := utils.ReadFileAbs(options.SchemaSQLPath)
|
||||
schemaSQL = string(b)
|
||||
if schemaPath != "" {
|
||||
schemaSQL = string(utils.ReadFileAbs(schemaPath))
|
||||
} else {
|
||||
schemaSQL = defaultSchemaSQL
|
||||
}
|
||||
|
|
@ -129,10 +109,6 @@ func InitTestDB(t *testing.T, dbPath string, options *TestDBOptions) *DB {
|
|||
t.Fatal(errors.Wrap(err, "running schema sql"))
|
||||
}
|
||||
|
||||
if options == nil || !options.SkipMigration {
|
||||
MarkMigrationComplete(t, db)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
|
|
@ -161,10 +137,19 @@ func OpenTestDB(t *testing.T, dnoteDir string) *DB {
|
|||
|
||||
// MarkMigrationComplete marks all migrations as complete in the database
|
||||
func MarkMigrationComplete(t *testing.T, db *DB) {
|
||||
if _, err := db.Exec("INSERT INTO system (key, value) VALUES (? , ?);", consts.SystemSchema, 12); err != nil {
|
||||
if _, err := db.Exec("INSERT INTO system (key, value) VALUES (? , ?);", consts.SystemSchema, 14); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "inserting schema"))
|
||||
}
|
||||
if _, err := db.Exec("INSERT INTO system (key, value) VALUES (? , ?);", consts.SystemRemoteSchema, 1); err != nil {
|
||||
t.Fatal(errors.Wrap(err, "inserting remote schema"))
|
||||
}
|
||||
}
|
||||
|
||||
// mustGenerateTestUUID generates a UUID for test databases and fails the test on error
|
||||
func mustGenerateTestUUID(t *testing.T) string {
|
||||
uuid, err := utils.GenerateUUID()
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "generating UUID for test database"))
|
||||
}
|
||||
return uuid
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue