Initial vibecoded proof of concept
This commit is contained in:
parent
74812459af
commit
461318a656
61 changed files with 13306 additions and 0 deletions
151
lua/notex/database/init.lua
Normal file
151
lua/notex/database/init.lua
Normal file
|
@ -0,0 +1,151 @@
|
|||
-- Database connection and initialization module
|
||||
local M = {}
|
||||
|
||||
local sqlite3 = require('lsqlite3')
|
||||
local utils = require('notex.utils')
|
||||
|
||||
-- Database connection state
|
||||
local db = nil
|
||||
local db_path = nil
|
||||
|
||||
-- Initialize database connection
|
||||
function M.init(path)
|
||||
if db then
|
||||
return true, "Database already initialized"
|
||||
end
|
||||
|
||||
db_path = path or vim.fn.stdpath('data') .. '/notex/notex.db'
|
||||
|
||||
-- Ensure directory exists
|
||||
local dir = vim.fn.fnamemodify(db_path, ':h')
|
||||
vim.fn.mkdir(dir, 'p')
|
||||
|
||||
local ok, err = pcall(function()
|
||||
db = sqlite3.open(db_path)
|
||||
end)
|
||||
|
||||
if not ok then
|
||||
return false, "Failed to open database: " .. tostring(err)
|
||||
end
|
||||
|
||||
-- Enable foreign key constraints
|
||||
db:exec("PRAGMA foreign_keys = ON")
|
||||
|
||||
-- Set WAL mode for better performance
|
||||
db:exec("PRAGMA journal_mode = WAL")
|
||||
|
||||
-- Set synchronous mode for performance vs safety balance
|
||||
db:exec("PRAGMA synchronous = NORMAL")
|
||||
|
||||
return true, "Database initialized successfully"
|
||||
end
|
||||
|
||||
-- Get database connection
|
||||
function M.get_connection()
|
||||
if not db then
|
||||
error("Database not initialized. Call init() first.")
|
||||
end
|
||||
return db
|
||||
end
|
||||
|
||||
-- Close database connection
|
||||
function M.close()
|
||||
if db then
|
||||
db:close()
|
||||
db = nil
|
||||
return true, "Database closed successfully"
|
||||
end
|
||||
return false, "Database not initialized"
|
||||
end
|
||||
|
||||
-- Execute query with error handling
|
||||
function M.execute(query, params)
|
||||
local conn = M.get_connection()
|
||||
local stmt = conn:prepare(query)
|
||||
|
||||
if not stmt then
|
||||
return false, "Failed to prepare query: " .. query
|
||||
end
|
||||
|
||||
local result = {}
|
||||
local ok, err = pcall(function()
|
||||
if params then
|
||||
stmt:bind_names(params)
|
||||
end
|
||||
|
||||
for row in stmt:nrows() do
|
||||
table.insert(result, row)
|
||||
end
|
||||
|
||||
stmt:finalize()
|
||||
end)
|
||||
|
||||
if not ok then
|
||||
stmt:finalize()
|
||||
return false, "Query execution failed: " .. tostring(err)
|
||||
end
|
||||
|
||||
return true, result
|
||||
end
|
||||
|
||||
-- Execute transaction
|
||||
function M.transaction(queries)
|
||||
local conn = M.get_connection()
|
||||
local ok, err = pcall(function()
|
||||
conn:exec("BEGIN TRANSACTION")
|
||||
|
||||
for _, query_data in ipairs(queries) do
|
||||
local stmt = conn:prepare(query_data.query)
|
||||
if query_data.params then
|
||||
stmt:bind_names(query_data.params)
|
||||
end
|
||||
stmt:step()
|
||||
stmt:finalize()
|
||||
end
|
||||
|
||||
conn:exec("COMMIT")
|
||||
end)
|
||||
|
||||
if not ok then
|
||||
conn:exec("ROLLBACK")
|
||||
return false, "Transaction failed: " .. tostring(err)
|
||||
end
|
||||
|
||||
return true, "Transaction completed successfully"
|
||||
end
|
||||
|
||||
-- Get database status
|
||||
function M.status()
|
||||
if not db then
|
||||
return {
|
||||
initialized = false,
|
||||
path = nil,
|
||||
size_bytes = 0,
|
||||
wal_mode = false
|
||||
}
|
||||
end
|
||||
|
||||
local size = 0
|
||||
local file = io.open(db_path, "r")
|
||||
if file then
|
||||
size = file:seek("end")
|
||||
file:close()
|
||||
end
|
||||
|
||||
local wal_mode = false
|
||||
local wal_file = db_path .. "-wal"
|
||||
local wal = io.open(wal_file, "r")
|
||||
if wal then
|
||||
wal_mode = true
|
||||
wal:close()
|
||||
end
|
||||
|
||||
return {
|
||||
initialized = true,
|
||||
path = db_path,
|
||||
size_bytes = size,
|
||||
wal_mode = wal_mode
|
||||
}
|
||||
end
|
||||
|
||||
return M
|
210
lua/notex/database/migrations.lua
Normal file
210
lua/notex/database/migrations.lua
Normal file
|
@ -0,0 +1,210 @@
|
|||
-- Database migration management
|
||||
local M = {}
|
||||
|
||||
local database = require('notex.database.init')
|
||||
|
||||
-- Current database version
|
||||
local CURRENT_VERSION = 1
|
||||
|
||||
-- Migration table and version tracking
|
||||
local function create_migration_table()
|
||||
local query = [[
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
version INTEGER PRIMARY KEY,
|
||||
applied_at INTEGER NOT NULL
|
||||
)
|
||||
]]
|
||||
return database.execute(query)
|
||||
end
|
||||
|
||||
-- Get current database version
|
||||
local function get_database_version()
|
||||
local ok, result = database.execute("SELECT version FROM schema_migrations ORDER BY version DESC LIMIT 1")
|
||||
|
||||
if ok and #result > 0 then
|
||||
return result[1].version
|
||||
end
|
||||
|
||||
return 0
|
||||
end
|
||||
|
||||
-- Record migration
|
||||
local function record_migration(version)
|
||||
local query = [[
|
||||
INSERT INTO schema_migrations (version, applied_at)
|
||||
VALUES (:version, :applied_at)
|
||||
]]
|
||||
|
||||
return database.execute(query, {
|
||||
version = version,
|
||||
applied_at = os.time()
|
||||
})
|
||||
end
|
||||
|
||||
-- Migration definitions
|
||||
local migrations = {
|
||||
[1] = {
|
||||
description = "Initial schema creation",
|
||||
up = function()
|
||||
local schema = require('notex.database.schema')
|
||||
return schema.init()
|
||||
end,
|
||||
down = function()
|
||||
local queries = {
|
||||
"DROP TABLE IF EXISTS properties",
|
||||
"DROP TABLE IF EXISTS queries",
|
||||
"DROP TABLE IF EXISTS schema_metadata",
|
||||
"DROP TABLE IF EXISTS documents"
|
||||
}
|
||||
|
||||
for _, query in ipairs(queries) do
|
||||
local ok = database.execute(query)
|
||||
if not ok then
|
||||
return false, "Failed to drop table in rollback"
|
||||
end
|
||||
end
|
||||
|
||||
return true, "Rollback completed successfully"
|
||||
end
|
||||
}
|
||||
}
|
||||
|
||||
-- Initialize migration system
|
||||
function M.init()
|
||||
local ok, err = create_migration_table()
|
||||
if not ok then
|
||||
return false, "Failed to create migration table: " .. err
|
||||
end
|
||||
|
||||
local current_version = get_database_version()
|
||||
|
||||
if current_version == 0 then
|
||||
-- Fresh installation - apply current version
|
||||
return M.migrate_to(CURRENT_VERSION)
|
||||
end
|
||||
|
||||
return true, string.format("Database at version %d", current_version)
|
||||
end
|
||||
|
||||
-- Migrate to specific version
|
||||
function M.migrate_to(target_version)
|
||||
local current_version = get_database_version()
|
||||
|
||||
if target_version < current_version then
|
||||
return false, "Downgrade migrations not supported"
|
||||
end
|
||||
|
||||
if target_version > CURRENT_VERSION then
|
||||
return false, string.format("Target version %d exceeds maximum version %d", target_version, CURRENT_VERSION)
|
||||
end
|
||||
|
||||
-- Apply migrations sequentially
|
||||
for version = current_version + 1, target_version do
|
||||
if not migrations[version] then
|
||||
return false, string.format("Migration %d not found", version)
|
||||
end
|
||||
|
||||
local migration = migrations[version]
|
||||
|
||||
print(string.format("Applying migration %d: %s", version, migration.description))
|
||||
|
||||
local ok, err = migration.up()
|
||||
if not ok then
|
||||
return false, string.format("Migration %d failed: %s", version, err)
|
||||
end
|
||||
|
||||
local record_ok, record_err = record_migration(version)
|
||||
if not record_ok then
|
||||
return false, string.format("Failed to record migration %d: %s", version, record_err)
|
||||
end
|
||||
end
|
||||
|
||||
return true, string.format("Migrated to version %d successfully", target_version)
|
||||
end
|
||||
|
||||
-- Get migration status
|
||||
function M.status()
|
||||
local current_version = get_database_version()
|
||||
local pending_migrations = {}
|
||||
|
||||
for version = current_version + 1, CURRENT_VERSION do
|
||||
if migrations[version] then
|
||||
table.insert(pending_migrations, {
|
||||
version = version,
|
||||
description = migrations[version].description
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
current_version = current_version,
|
||||
latest_version = CURRENT_VERSION,
|
||||
pending_migrations = pending_migrations,
|
||||
needs_migration = #pending_migrations > 0
|
||||
}
|
||||
end
|
||||
|
||||
-- Get list of all migrations
|
||||
function M.list()
|
||||
local migration_list = {}
|
||||
|
||||
for version, migration in pairs(migrations) do
|
||||
table.insert(migration_list, {
|
||||
version = version,
|
||||
description = migration.description,
|
||||
applied = version <= get_database_version()
|
||||
})
|
||||
end
|
||||
|
||||
table.sort(migration_list, function(a, b) return a.version < b.version end)
|
||||
|
||||
return migration_list
|
||||
end
|
||||
|
||||
-- Validate database schema
|
||||
function M.validate()
|
||||
local status = M.status()
|
||||
|
||||
if status.needs_migration then
|
||||
return false, string.format("Database needs migration from version %d to %d",
|
||||
status.current_version, status.latest_version)
|
||||
end
|
||||
|
||||
-- Check if all required tables exist
|
||||
local tables = { "documents", "properties", "queries", "schema_metadata", "schema_migrations" }
|
||||
|
||||
for _, table_name in ipairs(tables) do
|
||||
local ok, result = database.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name=:table_name",
|
||||
{ table_name = table_name }
|
||||
)
|
||||
|
||||
if not ok or #result == 0 then
|
||||
return false, string.format("Required table '%s' not found", table_name)
|
||||
end
|
||||
end
|
||||
|
||||
return true, "Database schema is valid"
|
||||
end
|
||||
|
||||
-- Reset database (for development/testing only)
|
||||
function M.reset()
|
||||
local queries = {
|
||||
"DROP TABLE IF EXISTS properties",
|
||||
"DROP TABLE IF EXISTS queries",
|
||||
"DROP TABLE IF EXISTS schema_metadata",
|
||||
"DROP TABLE IF EXISTS documents",
|
||||
"DROP TABLE IF EXISTS schema_migrations"
|
||||
}
|
||||
|
||||
for _, query in ipairs(queries) do
|
||||
local ok = database.execute(query)
|
||||
if not ok then
|
||||
return false, "Failed to reset database"
|
||||
end
|
||||
end
|
||||
|
||||
return M.migrate_to(CURRENT_VERSION)
|
||||
end
|
||||
|
||||
return M
|
264
lua/notex/database/schema.lua
Normal file
264
lua/notex/database/schema.lua
Normal file
|
@ -0,0 +1,264 @@
|
|||
-- Database schema and model definitions
|
||||
local M = {}
|
||||
|
||||
local database = require('notex.database.init')
|
||||
|
||||
-- Table definitions
|
||||
local SCHEMA = {
|
||||
documents = [[
|
||||
CREATE TABLE IF NOT EXISTS documents (
|
||||
id TEXT PRIMARY KEY,
|
||||
file_path TEXT UNIQUE NOT NULL,
|
||||
content_hash TEXT NOT NULL,
|
||||
last_modified INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
)
|
||||
]],
|
||||
|
||||
properties = [[
|
||||
CREATE TABLE IF NOT EXISTS properties (
|
||||
id TEXT PRIMARY KEY,
|
||||
document_id TEXT NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
value_type TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE
|
||||
)
|
||||
]],
|
||||
|
||||
queries = [[
|
||||
CREATE TABLE IF NOT EXISTS queries (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
definition TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
last_used INTEGER DEFAULT 0,
|
||||
use_count INTEGER DEFAULT 0
|
||||
)
|
||||
]],
|
||||
|
||||
schema_metadata = [[
|
||||
CREATE TABLE IF NOT EXISTS schema_metadata (
|
||||
property_key TEXT PRIMARY KEY,
|
||||
detected_type TEXT NOT NULL,
|
||||
validation_rules TEXT,
|
||||
document_count INTEGER DEFAULT 0,
|
||||
created_at INTEGER NOT NULL
|
||||
)
|
||||
]]
|
||||
}
|
||||
|
||||
-- Index definitions
|
||||
local INDICES = {
|
||||
documents_file_path = "CREATE UNIQUE INDEX IF NOT EXISTS idx_documents_file_path ON documents(file_path)",
|
||||
properties_document_id = "CREATE INDEX IF NOT EXISTS idx_properties_document_id ON properties(document_id)",
|
||||
properties_key = "CREATE INDEX IF NOT EXISTS idx_properties_key ON properties(key)",
|
||||
queries_last_used = "CREATE INDEX IF NOT EXISTS idx_queries_last_used ON queries(last_used)",
|
||||
properties_composite = "CREATE INDEX IF NOT EXISTS idx_properties_composite ON properties(document_id, key)",
|
||||
properties_type = "CREATE INDEX IF NOT EXISTS idx_properties_type ON properties(key, value_type)"
|
||||
}
|
||||
|
||||
-- Initialize schema
|
||||
function M.init()
|
||||
local ok, result = database.transaction({
|
||||
{ query = SCHEMA.documents },
|
||||
{ query = SCHEMA.properties },
|
||||
{ query = SCHEMA.queries },
|
||||
{ query = SCHEMA.schema_metadata },
|
||||
{ query = INDICES.documents_file_path },
|
||||
{ query = INDICES.properties_document_id },
|
||||
{ query = INDICES.properties_key },
|
||||
{ query = INDICES.queries_last_used },
|
||||
{ query = INDICES.properties_composite },
|
||||
{ query = INDICES.properties_type }
|
||||
})
|
||||
|
||||
if not ok then
|
||||
return false, result
|
||||
end
|
||||
|
||||
return true, "Schema initialized successfully"
|
||||
end
|
||||
|
||||
-- Document model functions
|
||||
M.documents = {}
|
||||
|
||||
function M.documents.create(document_data)
|
||||
local query = [[
|
||||
INSERT INTO documents (id, file_path, content_hash, last_modified, created_at, updated_at)
|
||||
VALUES (:id, :file_path, :content_hash, :last_modified, :created_at, :updated_at)
|
||||
]]
|
||||
|
||||
return database.execute(query, document_data)
|
||||
end
|
||||
|
||||
function M.documents.get_by_id(id)
|
||||
local query = "SELECT * FROM documents WHERE id = :id"
|
||||
local ok, result = database.execute(query, { id = id })
|
||||
|
||||
if ok and #result > 0 then
|
||||
return true, result[1]
|
||||
end
|
||||
|
||||
return ok, nil
|
||||
end
|
||||
|
||||
function M.documents.get_by_path(file_path)
|
||||
local query = "SELECT * FROM documents WHERE file_path = :file_path"
|
||||
local ok, result = database.execute(query, { file_path = file_path })
|
||||
|
||||
if ok and #result > 0 then
|
||||
return true, result[1]
|
||||
end
|
||||
|
||||
return ok, nil
|
||||
end
|
||||
|
||||
function M.documents.update(document_data)
|
||||
local query = [[
|
||||
UPDATE documents
|
||||
SET content_hash = :content_hash,
|
||||
last_modified = :last_modified,
|
||||
updated_at = :updated_at
|
||||
WHERE id = :id
|
||||
]]
|
||||
|
||||
return database.execute(query, document_data)
|
||||
end
|
||||
|
||||
function M.documents.delete(id)
|
||||
local query = "DELETE FROM documents WHERE id = :id"
|
||||
return database.execute(query, { id = id })
|
||||
end
|
||||
|
||||
-- Property model functions
|
||||
M.properties = {}
|
||||
|
||||
function M.properties.create(property_data)
|
||||
local query = [[
|
||||
INSERT INTO properties (id, document_id, key, value, value_type, created_at, updated_at)
|
||||
VALUES (:id, :document_id, :key, :value, :value_type, :created_at, :updated_at)
|
||||
]]
|
||||
|
||||
return database.execute(query, property_data)
|
||||
end
|
||||
|
||||
function M.properties.get_by_document(document_id)
|
||||
local query = "SELECT * FROM properties WHERE document_id = :document_id"
|
||||
local ok, result = database.execute(query, { document_id = document_id })
|
||||
return ok, result or {}
|
||||
end
|
||||
|
||||
function M.properties.get_by_key(key)
|
||||
local query = "SELECT * FROM properties WHERE key = :key"
|
||||
local ok, result = database.execute(query, { key = key })
|
||||
return ok, result or {}
|
||||
end
|
||||
|
||||
function M.properties.update(property_data)
|
||||
local query = [[
|
||||
UPDATE properties
|
||||
SET value = :value,
|
||||
value_type = :value_type,
|
||||
updated_at = :updated_at
|
||||
WHERE id = :id
|
||||
]]
|
||||
|
||||
return database.execute(query, property_data)
|
||||
end
|
||||
|
||||
function M.properties.delete(id)
|
||||
local query = "DELETE FROM properties WHERE id = :id"
|
||||
return database.execute(query, { id = id })
|
||||
end
|
||||
|
||||
function M.properties.delete_by_document(document_id)
|
||||
local query = "DELETE FROM properties WHERE document_id = :document_id"
|
||||
return database.execute(query, { document_id = document_id })
|
||||
end
|
||||
|
||||
-- Query model functions
|
||||
M.queries = {}
|
||||
|
||||
function M.queries.create(query_data)
|
||||
local query = [[
|
||||
INSERT INTO queries (id, name, definition, created_at)
|
||||
VALUES (:id, :name, :definition, :created_at)
|
||||
]]
|
||||
|
||||
return database.execute(query, query_data)
|
||||
end
|
||||
|
||||
function M.queries.get_all()
|
||||
local query = "SELECT * FROM queries ORDER BY last_used DESC"
|
||||
return database.execute(query)
|
||||
end
|
||||
|
||||
function M.queries.get_by_id(id)
|
||||
local query = "SELECT * FROM queries WHERE id = :id"
|
||||
local ok, result = database.execute(query, { id = id })
|
||||
|
||||
if ok and #result > 0 then
|
||||
return true, result[1]
|
||||
end
|
||||
|
||||
return ok, nil
|
||||
end
|
||||
|
||||
function M.queries.update_usage(id)
|
||||
local query = [[
|
||||
UPDATE queries
|
||||
SET last_used = :last_used,
|
||||
use_count = use_count + 1
|
||||
WHERE id = :id
|
||||
]]
|
||||
|
||||
return database.execute(query, {
|
||||
id = id,
|
||||
last_used = os.time()
|
||||
})
|
||||
end
|
||||
|
||||
function M.queries.delete(id)
|
||||
local query = "DELETE FROM queries WHERE id = :id"
|
||||
return database.execute(query, { id = id })
|
||||
end
|
||||
|
||||
-- Schema metadata functions
|
||||
M.schema = {}
|
||||
|
||||
function M.schema.update_property(property_key, detected_type, validation_rules, document_count)
|
||||
local query = [[
|
||||
INSERT OR REPLACE INTO schema_metadata (property_key, detected_type, validation_rules, document_count, created_at)
|
||||
VALUES (:property_key, :detected_type, :validation_rules, :document_count, :created_at)
|
||||
]]
|
||||
|
||||
return database.execute(query, {
|
||||
property_key = property_key,
|
||||
detected_type = detected_type,
|
||||
validation_rules = validation_rules,
|
||||
document_count = document_count,
|
||||
created_at = os.time()
|
||||
})
|
||||
end
|
||||
|
||||
function M.schema.get_all()
|
||||
local query = "SELECT * FROM schema_metadata ORDER BY document_count DESC"
|
||||
return database.execute(query)
|
||||
end
|
||||
|
||||
function M.schema.get_by_key(property_key)
|
||||
local query = "SELECT * FROM schema_metadata WHERE property_key = :property_key"
|
||||
local ok, result = database.execute(query, { property_key = property_key })
|
||||
|
||||
if ok and #result > 0 then
|
||||
return true, result[1]
|
||||
end
|
||||
|
||||
return ok, nil
|
||||
end
|
||||
|
||||
return M
|
Loading…
Add table
Add a link
Reference in a new issue