mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-16 14:10:52 +01:00
zwischenstand
This commit is contained in:
106
src-tauri-db/build.rs
Normal file
106
src-tauri-db/build.rs
Normal file
@ -0,0 +1,106 @@
|
||||
use serde::Deserialize;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, Write};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Schema {
|
||||
haex: Haex,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Haex {
|
||||
settings: String,
|
||||
extensions: String,
|
||||
extension_permissions: String,
|
||||
notifications: String,
|
||||
passwords: Passwords,
|
||||
crdt: Crdt,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Passwords {
|
||||
groups: String,
|
||||
group_items: String,
|
||||
item_details: String,
|
||||
item_key_values: String,
|
||||
item_histories: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Crdt {
|
||||
logs: String,
|
||||
snapshots: String,
|
||||
configs: String,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Pfad zur Eingabe-JSON und zur Ausgabe-Rust-Datei festlegen.
|
||||
// `OUT_DIR` ist ein spezielles Verzeichnis, das Cargo für generierte Dateien bereitstellt.
|
||||
let schema_path = Path::new("database/tableNames.json");
|
||||
let out_dir =
|
||||
env::var("OUT_DIR").expect("OUT_DIR ist nicht gesetzt. Führen Sie dies mit Cargo aus.");
|
||||
let dest_path = Path::new(&out_dir).join("tableNames.rs");
|
||||
|
||||
// --- 2. JSON-Datei lesen und mit serde parsen ---
|
||||
let file = File::open(&schema_path).expect("Konnte tableNames.json nicht öffnen");
|
||||
let reader = BufReader::new(file);
|
||||
let schema: Schema =
|
||||
serde_json::from_reader(reader).expect("Konnte tableNames.json nicht parsen");
|
||||
let haex = schema.haex;
|
||||
|
||||
// --- 3. Den zu generierenden Rust-Code als String erstellen ---
|
||||
// Wir verwenden das `format!`-Makro, um die Werte aus den geparsten Structs
|
||||
// in einen vordefinierten Code-Template-String einzufügen.
|
||||
// Das `r#""#`-Format erlaubt uns, mehrzeilige Strings mit Anführungszeichen zu verwenden.
|
||||
let code = format!(
|
||||
r#"
|
||||
// HINWEIS: Diese Datei wurde automatisch von build.rs generiert.
|
||||
// Manuelle Änderungen werden bei der nächsten Kompilierung überschrieben!
|
||||
|
||||
pub const TABLE_SETTINGS: &str = "{settings}";
|
||||
pub const TABLE_EXTENSIONS: &str = "{extensions}";
|
||||
pub const TABLE_EXTENSION_PERMISSIONS: &str = "{extension_permissions}";
|
||||
pub const TABLE_NOTIFICATIONS: &str = "{notifications}";
|
||||
|
||||
// Passwords
|
||||
pub const TABLE_PASSWORDS_GROUPS: &str = "{pw_groups}";
|
||||
pub const TABLE_PASSWORDS_GROUP_ITEMS: &str = "{pw_group_items}";
|
||||
pub const TABLE_PASSWORDS_ITEM_DETAILS: &str = "{pw_item_details}";
|
||||
pub const TABLE_PASSWORDS_ITEM_KEY_VALUES: &str = "{pw_item_key_values}";
|
||||
pub const TABLE_PASSWORDS_ITEM_HISTORIES: &str = "{pw_item_histories}";
|
||||
|
||||
// CRDT
|
||||
pub const TABLE_CRDT_LOGS: &str = "{crdt_logs}";
|
||||
pub const TABLE_CRDT_SNAPSHOTS: &str = "{crdt_snapshots}";
|
||||
pub const TABLE_CRDT_CONFIGS: &str = "{crdt_configs}";
|
||||
|
||||
"#,
|
||||
// Hier werden die Werte aus dem `haex`-Struct in die Platzhalter oben eingesetzt.
|
||||
settings = haex.settings,
|
||||
extensions = haex.extensions,
|
||||
extension_permissions = haex.extension_permissions,
|
||||
notifications = haex.notifications,
|
||||
pw_groups = haex.passwords.groups,
|
||||
pw_group_items = haex.passwords.group_items,
|
||||
pw_item_details = haex.passwords.item_details,
|
||||
pw_item_key_values = haex.passwords.item_key_values,
|
||||
pw_item_histories = haex.passwords.item_histories,
|
||||
crdt_logs = haex.crdt.logs,
|
||||
crdt_snapshots = haex.crdt.snapshots,
|
||||
crdt_configs = haex.crdt.configs
|
||||
);
|
||||
|
||||
// --- 4. Den generierten Code in die Zieldatei schreiben ---
|
||||
let mut f = File::create(&dest_path).expect("Konnte die Zieldatei nicht erstellen");
|
||||
f.write_all(code.as_bytes())
|
||||
.expect("Konnte nicht in die Zieldatei schreiben");
|
||||
|
||||
// --- 5. Cargo anweisen, das Skript erneut auszuführen, wenn sich die JSON-Datei ändert ---
|
||||
// Diese Zeile ist extrem wichtig für eine reibungslose Entwicklung! Ohne sie
|
||||
// würde Cargo Änderungen an der JSON-Datei nicht bemerken.
|
||||
println!("cargo:rerun-if-changed=database/tableNames.json");
|
||||
|
||||
tauri_build::build()
|
||||
}
|
||||
25
src-tauri/Cargo.lock
generated
25
src-tauri/Cargo.lock
generated
@ -3646,10 +3646,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.219"
|
||||
version = "1.0.223"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||
checksum = "a505d71960adde88e293da5cb5eda57093379f64e61cf77bf0e6a63af07a7bac"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
@ -3665,10 +3666,19 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.219"
|
||||
name = "serde_core"
|
||||
version = "1.0.223"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||
checksum = "20f57cbd357666aa7b3ac84a90b4ea328f1d4ddb6772b430caa5d9e1309bb9e9"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.223"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d428d07faf17e306e699ec1e91996e5a165ba5d6bce5b5155173e91a8a01a56"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -3688,14 +3698,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.143"
|
||||
version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
|
||||
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@ -15,8 +15,10 @@ name = "haex_hub_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.2", features = [] }
|
||||
serde_json = "1.0.145"
|
||||
|
||||
tauri-build = { version = "2.2", features = [] }
|
||||
serde = { version = "1.0.223", features = ["derive"] }
|
||||
[dependencies]
|
||||
rusqlite = { version = "0.37.0", features = [
|
||||
"load_extension",
|
||||
|
||||
@ -1,3 +1,106 @@
|
||||
use serde::Deserialize;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, Write};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Schema {
|
||||
haex: Haex,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Haex {
|
||||
settings: String,
|
||||
extensions: String,
|
||||
extension_permissions: String,
|
||||
notifications: String,
|
||||
passwords: Passwords,
|
||||
crdt: Crdt,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Passwords {
|
||||
groups: String,
|
||||
group_items: String,
|
||||
item_details: String,
|
||||
item_key_values: String,
|
||||
item_histories: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Crdt {
|
||||
logs: String,
|
||||
snapshots: String,
|
||||
configs: String,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Pfad zur Eingabe-JSON und zur Ausgabe-Rust-Datei festlegen.
|
||||
// `OUT_DIR` ist ein spezielles Verzeichnis, das Cargo für generierte Dateien bereitstellt.
|
||||
let schema_path = Path::new("database/tableNames.json");
|
||||
let out_dir =
|
||||
env::var("OUT_DIR").expect("OUT_DIR ist nicht gesetzt. Führen Sie dies mit Cargo aus.");
|
||||
let dest_path = Path::new(&out_dir).join("tableNames.rs");
|
||||
|
||||
// --- 2. JSON-Datei lesen und mit serde parsen ---
|
||||
let file = File::open(&schema_path).expect("Konnte tableNames.json nicht öffnen");
|
||||
let reader = BufReader::new(file);
|
||||
let schema: Schema =
|
||||
serde_json::from_reader(reader).expect("Konnte tableNames.json nicht parsen");
|
||||
let haex = schema.haex;
|
||||
|
||||
// --- 3. Den zu generierenden Rust-Code als String erstellen ---
|
||||
// Wir verwenden das `format!`-Makro, um die Werte aus den geparsten Structs
|
||||
// in einen vordefinierten Code-Template-String einzufügen.
|
||||
// Das `r#""#`-Format erlaubt uns, mehrzeilige Strings mit Anführungszeichen zu verwenden.
|
||||
let code = format!(
|
||||
r#"
|
||||
// HINWEIS: Diese Datei wurde automatisch von build.rs generiert.
|
||||
// Manuelle Änderungen werden bei der nächsten Kompilierung überschrieben!
|
||||
|
||||
pub const TABLE_SETTINGS: &str = "{settings}";
|
||||
pub const TABLE_EXTENSIONS: &str = "{extensions}";
|
||||
pub const TABLE_EXTENSION_PERMISSIONS: &str = "{extension_permissions}";
|
||||
pub const TABLE_NOTIFICATIONS: &str = "{notifications}";
|
||||
|
||||
// Passwords
|
||||
pub const TABLE_PASSWORDS_GROUPS: &str = "{pw_groups}";
|
||||
pub const TABLE_PASSWORDS_GROUP_ITEMS: &str = "{pw_group_items}";
|
||||
pub const TABLE_PASSWORDS_ITEM_DETAILS: &str = "{pw_item_details}";
|
||||
pub const TABLE_PASSWORDS_ITEM_KEY_VALUES: &str = "{pw_item_key_values}";
|
||||
pub const TABLE_PASSWORDS_ITEM_HISTORIES: &str = "{pw_item_histories}";
|
||||
|
||||
// CRDT
|
||||
pub const TABLE_CRDT_LOGS: &str = "{crdt_logs}";
|
||||
pub const TABLE_CRDT_SNAPSHOTS: &str = "{crdt_snapshots}";
|
||||
pub const TABLE_CRDT_CONFIGS: &str = "{crdt_configs}";
|
||||
|
||||
"#,
|
||||
// Hier werden die Werte aus dem `haex`-Struct in die Platzhalter oben eingesetzt.
|
||||
settings = haex.settings,
|
||||
extensions = haex.extensions,
|
||||
extension_permissions = haex.extension_permissions,
|
||||
notifications = haex.notifications,
|
||||
pw_groups = haex.passwords.groups,
|
||||
pw_group_items = haex.passwords.group_items,
|
||||
pw_item_details = haex.passwords.item_details,
|
||||
pw_item_key_values = haex.passwords.item_key_values,
|
||||
pw_item_histories = haex.passwords.item_histories,
|
||||
crdt_logs = haex.crdt.logs,
|
||||
crdt_snapshots = haex.crdt.snapshots,
|
||||
crdt_configs = haex.crdt.configs
|
||||
);
|
||||
|
||||
// --- 4. Den generierten Code in die Zieldatei schreiben ---
|
||||
let mut f = File::create(&dest_path).expect("Konnte die Zieldatei nicht erstellen");
|
||||
f.write_all(code.as_bytes())
|
||||
.expect("Konnte nicht in die Zieldatei schreiben");
|
||||
|
||||
// --- 5. Cargo anweisen, das Skript erneut auszuführen, wenn sich die JSON-Datei ändert ---
|
||||
// Diese Zeile ist extrem wichtig für eine reibungslose Entwicklung! Ohne sie
|
||||
// würde Cargo Änderungen an der JSON-Datei nicht bemerken.
|
||||
println!("cargo:rerun-if-changed=database/tableNames.json");
|
||||
|
||||
tauri_build::build()
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
||||
import tableNames from '../tableNames.json'
|
||||
|
||||
export const haexCrdtLogs = sqliteTable('haex_crdt_logs', {
|
||||
export const haexCrdtLogs = sqliteTable(tableNames.haex.crdt.logs, {
|
||||
hlc_timestamp: text().primaryKey(),
|
||||
table_name: text(),
|
||||
row_pks: text({ mode: 'json' }),
|
||||
@ -12,7 +13,7 @@ export const haexCrdtLogs = sqliteTable('haex_crdt_logs', {
|
||||
export type InsertHaexCrdtLogs = typeof haexCrdtLogs.$inferInsert
|
||||
export type SelectHaexCrdtLogs = typeof haexCrdtLogs.$inferSelect
|
||||
|
||||
export const haexCrdtSnapshots = sqliteTable('haex_crdt_snapshots', {
|
||||
export const haexCrdtSnapshots = sqliteTable(tableNames.haex.crdt.snapshots, {
|
||||
snapshot_id: text().primaryKey(),
|
||||
created: text(),
|
||||
epoch_hlc: text(),
|
||||
@ -20,7 +21,7 @@ export const haexCrdtSnapshots = sqliteTable('haex_crdt_snapshots', {
|
||||
file_size_bytes: integer(),
|
||||
})
|
||||
|
||||
export const haexCrdtSettings = sqliteTable('haex_crdt_settings', {
|
||||
type: text().primaryKey(),
|
||||
export const haexCrdtConfigs = sqliteTable(tableNames.haex.crdt.configs, {
|
||||
key: text().primaryKey(),
|
||||
value: text(),
|
||||
})
|
||||
|
||||
@ -7,8 +7,9 @@ import {
|
||||
unique,
|
||||
type AnySQLiteColumn,
|
||||
} from 'drizzle-orm/sqlite-core'
|
||||
import tableNames from '../tableNames.json'
|
||||
|
||||
export const haexSettings = sqliteTable('haex_settings', {
|
||||
export const haexSettings = sqliteTable(tableNames.haex.settings, {
|
||||
id: text().primaryKey(),
|
||||
key: text(),
|
||||
type: text(),
|
||||
@ -18,7 +19,7 @@ export const haexSettings = sqliteTable('haex_settings', {
|
||||
export type InsertHaexSettings = typeof haexSettings.$inferInsert
|
||||
export type SelectHaexSettings = typeof haexSettings.$inferSelect
|
||||
|
||||
export const haexExtensions = sqliteTable('haex_extensions', {
|
||||
export const haexExtensions = sqliteTable(tableNames.haex.extensions, {
|
||||
id: text().primaryKey(),
|
||||
author: text(),
|
||||
enabled: integer({ mode: 'boolean' }),
|
||||
@ -31,8 +32,8 @@ export const haexExtensions = sqliteTable('haex_extensions', {
|
||||
export type InsertHaexExtensions = typeof haexExtensions.$inferInsert
|
||||
export type SelectHaexExtensions = typeof haexExtensions.$inferSelect
|
||||
|
||||
export const haexExtensionsPermissions = sqliteTable(
|
||||
'haex_extensions_permissions',
|
||||
export const haexExtensionPermissions = sqliteTable(
|
||||
tableNames.haex.extension_permissions,
|
||||
{
|
||||
id: text().primaryKey(),
|
||||
extensionId: text('extension_id').references(
|
||||
@ -51,12 +52,12 @@ export const haexExtensionsPermissions = sqliteTable(
|
||||
unique().on(table.extensionId, table.resource, table.operation, table.path),
|
||||
],
|
||||
)
|
||||
export type InsertHaexExtensionsPermissions =
|
||||
typeof haexExtensionsPermissions.$inferInsert
|
||||
export type SelectHaexExtensionsPermissions =
|
||||
typeof haexExtensionsPermissions.$inferSelect
|
||||
export type InserthaexExtensionPermissions =
|
||||
typeof haexExtensionPermissions.$inferInsert
|
||||
export type SelecthaexExtensionPermissions =
|
||||
typeof haexExtensionPermissions.$inferSelect
|
||||
|
||||
export const haexNotifications = sqliteTable('haex_notifications', {
|
||||
export const haexNotifications = sqliteTable(tableNames.haex.notifications, {
|
||||
id: text().primaryKey(),
|
||||
alt: text(),
|
||||
date: text(),
|
||||
@ -75,7 +76,7 @@ export type InsertHaexNotifications = typeof haexNotifications.$inferInsert
|
||||
export type SelectHaexNotifications = typeof haexNotifications.$inferSelect
|
||||
|
||||
export const haexPasswordsItemDetails = sqliteTable(
|
||||
'haex_passwords_item_details',
|
||||
tableNames.haex.passwords.item_details,
|
||||
{
|
||||
id: text().primaryKey(),
|
||||
title: text(),
|
||||
@ -98,7 +99,7 @@ export type SelectHaexPasswordsItemDetails =
|
||||
typeof haexPasswordsItemDetails.$inferSelect
|
||||
|
||||
export const haexPasswordsItemKeyValues = sqliteTable(
|
||||
'haex_passwords_item_key_values',
|
||||
tableNames.haex.passwords.item_key_values,
|
||||
{
|
||||
id: text().primaryKey(),
|
||||
itemId: text('item_id').references(
|
||||
@ -118,7 +119,7 @@ export type SelectHaexPasswordsItemKeyValues =
|
||||
typeof haexPasswordsItemKeyValues.$inferSelect
|
||||
|
||||
export const haexPasswordsItemHistory = sqliteTable(
|
||||
'haex_passwords_item_history',
|
||||
tableNames.haex.passwords.item_histories,
|
||||
{
|
||||
id: text().primaryKey(),
|
||||
itemId: text('item_id').references(
|
||||
@ -137,27 +138,30 @@ export type InserthaexPasswordsItemHistory =
|
||||
export type SelectHaexPasswordsItemHistory =
|
||||
typeof haexPasswordsItemHistory.$inferSelect
|
||||
|
||||
export const haexPasswordsGroups = sqliteTable('haex_passwords_groups', {
|
||||
id: text().primaryKey(),
|
||||
name: text(),
|
||||
description: text(),
|
||||
icon: text(),
|
||||
order: integer(),
|
||||
color: text(),
|
||||
parentId: text('parent_id').references(
|
||||
(): AnySQLiteColumn => haexPasswordsGroups.id,
|
||||
),
|
||||
createdAt: text('created_at').default(sql`(CURRENT_TIMESTAMP)`),
|
||||
updateAt: integer('updated_at', { mode: 'timestamp' }).$onUpdate(
|
||||
() => new Date(),
|
||||
),
|
||||
haex_tombstone: integer({ mode: 'boolean' }),
|
||||
})
|
||||
export const haexPasswordsGroups = sqliteTable(
|
||||
tableNames.haex.passwords.groups,
|
||||
{
|
||||
id: text().primaryKey(),
|
||||
name: text(),
|
||||
description: text(),
|
||||
icon: text(),
|
||||
order: integer(),
|
||||
color: text(),
|
||||
parentId: text('parent_id').references(
|
||||
(): AnySQLiteColumn => haexPasswordsGroups.id,
|
||||
),
|
||||
createdAt: text('created_at').default(sql`(CURRENT_TIMESTAMP)`),
|
||||
updateAt: integer('updated_at', { mode: 'timestamp' }).$onUpdate(
|
||||
() => new Date(),
|
||||
),
|
||||
haex_tombstone: integer({ mode: 'boolean' }),
|
||||
},
|
||||
)
|
||||
export type InsertHaexPasswordsGroups = typeof haexPasswordsGroups.$inferInsert
|
||||
export type SelectHaexPasswordsGroups = typeof haexPasswordsGroups.$inferSelect
|
||||
|
||||
export const haexPasswordsGroupItems = sqliteTable(
|
||||
'haex_passwords_group_items',
|
||||
tableNames.haex.passwords.group_items,
|
||||
{
|
||||
groupId: text('group_id').references(
|
||||
(): AnySQLiteColumn => haexPasswordsGroups.id,
|
||||
|
||||
20
src-tauri/database/tableNames.json
Normal file
20
src-tauri/database/tableNames.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"haex": {
|
||||
"settings": "haex_settings",
|
||||
"extensions": "haex_extensions",
|
||||
"extension_permissions": "haex_extension_permissions",
|
||||
"notifications": "haex_notifications",
|
||||
"passwords": {
|
||||
"groups": "haex_passwords_groups",
|
||||
"group_items": "haex_passwords_group_items",
|
||||
"item_details": "haex_passwords_item_details",
|
||||
"item_key_values": "haex_passwords_item_key_values",
|
||||
"item_histories": "haex_passwords_item_history"
|
||||
},
|
||||
"crdt": {
|
||||
"logs": "haex_crdt_logs",
|
||||
"snapshots": "haex_crdt_snapshots",
|
||||
"configs": "haex_crdt_configs"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
src-tauri/gen/android/app/src/main/assets/database/vault.db
Normal file
BIN
src-tauri/gen/android/app/src/main/assets/database/vault.db
Normal file
Binary file not shown.
6707
src-tauri/gen/schemas/android-schema.json
Normal file
6707
src-tauri/gen/schemas/android-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
6707
src-tauri/gen/schemas/mobile-schema.json
Normal file
6707
src-tauri/gen/schemas/mobile-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,32 +0,0 @@
|
||||
use crdt::trigger::TriggerManager;
|
||||
use rusqlite::{Connection, Result};
|
||||
// anpassen an dein Crate-Modul
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// Vault-Datenbank öffnen
|
||||
let conn = Connection::open("vault.db")?;
|
||||
|
||||
println!("🔄 Setup CRDT triggers...");
|
||||
|
||||
// Tabellen aus der DB holen
|
||||
let mut stmt =
|
||||
conn.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'haex_%' AND NOT LIKE 'haex_crdt_%';")?;
|
||||
let table_iter = stmt.query_map([], |row| row.get::<_, String>(0))?;
|
||||
|
||||
for table_name in table_iter {
|
||||
let table_name = table_name?;
|
||||
println!("➡️ Processing table: {}", table_name);
|
||||
|
||||
// Trigger für die Tabelle neu anlegen
|
||||
match TriggerManager::setup_triggers_for_table(&conn, &table_name) {
|
||||
Ok(_) => println!(" ✅ Triggers created for {}", table_name),
|
||||
Err(e) => println!(
|
||||
" ⚠️ Could not create triggers for {}: {:?}",
|
||||
table_name, e
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
println!("✨ Done setting up CRDT triggers.");
|
||||
Ok(())
|
||||
}
|
||||
@ -105,7 +105,7 @@ impl SqlProxy {
|
||||
hlc_timestamp: Option<&Timestamp>,
|
||||
) -> Result<Option<String>, ProxyError> {
|
||||
match stmt {
|
||||
sqlparser::ast::Statement::Query(query) => {
|
||||
Statement::Query(query) => {
|
||||
if let SetExpr::Select(select) = &mut *query.body {
|
||||
let mut tombstone_filters = Vec::new();
|
||||
for twj in &select.from {
|
||||
@ -162,6 +162,7 @@ impl SqlProxy {
|
||||
|
||||
// Hinweis: UNION, EXCEPT etc. werden hier nicht behandelt, was dem bisherigen Code entspricht.
|
||||
}
|
||||
|
||||
Statement::CreateTable(create_table) => {
|
||||
if self.is_audited_table(&create_table.name) {
|
||||
self.add_crdt_columns(&mut create_table.columns);
|
||||
@ -175,6 +176,7 @@ impl SqlProxy {
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Statement::Insert(insert_stmt) => {
|
||||
if let TableObject::TableName(name) = &insert_stmt.table {
|
||||
if self.is_audited_table(name) {
|
||||
@ -184,15 +186,7 @@ impl SqlProxy {
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Statement::Update(update_stmt) => {
|
||||
if let TableFactor::Table { name, .. } = &update_stmt.table.relation {
|
||||
if self.is_audited_table(&name) {
|
||||
if let Some(ts) = hlc_timestamp {
|
||||
update_stmt.assignments.push(self.create_hlc_assignment(ts));
|
||||
}
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
Statement::Update {
|
||||
table,
|
||||
assignments,
|
||||
@ -217,6 +211,7 @@ impl SqlProxy {
|
||||
or: *or,
|
||||
};
|
||||
}
|
||||
|
||||
Statement::Delete(del_stmt) => {
|
||||
let table_name = self.extract_table_name_from_from(&del_stmt.from);
|
||||
if let Some(name) = table_name {
|
||||
@ -232,6 +227,7 @@ impl SqlProxy {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Statement::AlterTable { name, .. } => {
|
||||
if self.is_audited_table(name) {
|
||||
return Ok(Some(
|
||||
|
||||
@ -1,32 +1,129 @@
|
||||
use rusqlite::{Connection, Result, Row};
|
||||
use crate::table_names::{TABLE_CRDT_CONFIGS, TABLE_CRDT_LOGS};
|
||||
use rusqlite::{Connection, Result as RusqliteResult, Row, Transaction};
|
||||
use serde::Serialize;
|
||||
use std::fmt::Write;
|
||||
use std::error::Error;
|
||||
use std::fmt::{self, Display, Formatter, Write};
|
||||
use std::panic::{self, AssertUnwindSafe};
|
||||
use ts_rs::TS;
|
||||
|
||||
// the z_ prefix should make sure that these triggers are executed lasts
|
||||
// Die "z_"-Präfix soll sicherstellen, dass diese Trigger als Letzte ausgeführt werden
|
||||
const INSERT_TRIGGER_TPL: &str = "z_crdt_{TABLE_NAME}_insert";
|
||||
const UPDATE_TRIGGER_TPL: &str = "z_crdt_{TABLE_NAME}_update";
|
||||
|
||||
pub const LOG_TABLE_NAME: &str = "haex_crdt_logs";
|
||||
const SYNC_ACTIVE_KEY: &str = "sync_active";
|
||||
pub const TOMBSTONE_COLUMN: &str = "haex_tombstone";
|
||||
pub const HLC_TIMESTAMP_COLUMN: &str = "haex_hlc_timestamp";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CrdtSetupError {
|
||||
/// Kapselt einen Fehler, der von der rusqlite-Bibliothek kommt.
|
||||
DatabaseError(rusqlite::Error),
|
||||
/// Die Tabelle hat keine Tombstone-Spalte, was eine CRDT-Voraussetzung ist.
|
||||
TombstoneColumnMissing {
|
||||
table_name: String,
|
||||
column_name: String,
|
||||
},
|
||||
/// Die Tabelle hat keinen Primärschlüssel, was eine CRDT-Voraussetzung ist.
|
||||
PrimaryKeyMissing { table_name: String },
|
||||
}
|
||||
|
||||
// Implementierung, damit unser Error-Typ schön formatiert werden kann.
|
||||
impl Display for CrdtSetupError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
CrdtSetupError::DatabaseError(e) => write!(f, "Database error: {}", e),
|
||||
CrdtSetupError::TombstoneColumnMissing {
|
||||
table_name,
|
||||
column_name,
|
||||
} => write!(
|
||||
f,
|
||||
"Table '{}' is missing the required tombstone column '{}'",
|
||||
table_name, column_name
|
||||
),
|
||||
CrdtSetupError::PrimaryKeyMissing { table_name } => {
|
||||
write!(f, "Table '{}' has no primary key", table_name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Implementierung, damit unser Typ als "echter" Error erkannt wird.
|
||||
impl Error for CrdtSetupError {}
|
||||
|
||||
// Wichtige Konvertierung: Erlaubt uns, den `?`-Operator auf Funktionen zu verwenden,
|
||||
// die `rusqlite::Error` zurückgeben. Der Fehler wird automatisch in unseren
|
||||
// `CrdtSetupError::DatabaseError` verpackt.
|
||||
impl From<rusqlite::Error> for CrdtSetupError {
|
||||
fn from(err: rusqlite::Error) -> Self {
|
||||
CrdtSetupError::DatabaseError(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "status", content = "details")]
|
||||
pub enum TriggerSetupResult {
|
||||
Success,
|
||||
TableNotFound,
|
||||
TombstoneColumnMissing { column_name: String },
|
||||
PrimaryKeyMissing,
|
||||
}
|
||||
|
||||
fn set_sync_active(conn: &mut Connection) -> RusqliteResult<()> {
|
||||
let sql = format!(
|
||||
"INSERT OR REPLACE INTO \"{meta_table}\" (key, value) VALUES (?, '1');",
|
||||
meta_table = TABLE_CRDT_CONFIGS
|
||||
);
|
||||
conn.execute(&sql, [SYNC_ACTIVE_KEY])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_sync_active(conn: &mut Connection) -> RusqliteResult<()> {
|
||||
let sql = format!(
|
||||
"DELETE FROM \"{meta_table}\" WHERE key = ?;",
|
||||
meta_table = TABLE_CRDT_CONFIGS
|
||||
);
|
||||
conn.execute(&sql, [SYNC_ACTIVE_KEY])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Führt eine Aktion aus, während die Trigger temporär deaktiviert sind.
|
||||
/// Diese Funktion stellt sicher, dass die Trigger auch bei einem Absturz (Panic)
|
||||
/// wieder aktiviert werden.
|
||||
pub fn with_triggers_paused<F, R>(conn: &mut Connection, action: F) -> RusqliteResult<R>
|
||||
where
|
||||
F: FnOnce(&mut Connection) -> RusqliteResult<R>,
|
||||
{
|
||||
set_sync_active(conn)?;
|
||||
|
||||
// AssertUnwindSafe wird benötigt, um den Mutex über eine Panic-Grenze hinweg zu verwenden.
|
||||
// Wir fangen einen möglichen Panic in `action` ab.
|
||||
let result = panic::catch_unwind(AssertUnwindSafe(|| action(conn)));
|
||||
|
||||
// Diese Aktion MUSS immer ausgeführt werden, egal ob `action` erfolgreich war oder nicht.
|
||||
clear_sync_active(conn)?;
|
||||
|
||||
match result {
|
||||
Ok(res) => res, // Alles gut, gib das Ergebnis von `action` zurück.
|
||||
Err(e) => panic::resume_unwind(e), // Ein Panic ist aufgetreten, wir geben ihn weiter, nachdem wir aufgeräumt haben.
|
||||
}
|
||||
}
|
||||
|
||||
/// Erstellt die benötigte Meta-Tabelle, falls sie nicht existiert.
|
||||
pub fn setup_meta_table(conn: &mut Connection) -> RusqliteResult<()> {
|
||||
let sql = format!(
|
||||
"CREATE TABLE IF NOT EXISTS \"{meta_table}\" (key TEXT PRIMARY KEY, value TEXT) WITHOUT ROWID;",
|
||||
meta_table = TABLE_CRDT_CONFIGS
|
||||
);
|
||||
conn.execute(&sql, [])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ColumnInfo {
|
||||
name: String,
|
||||
is_pk: bool,
|
||||
}
|
||||
|
||||
impl ColumnInfo {
|
||||
fn from_row(row: &Row) -> Result<Self> {
|
||||
fn from_row(row: &Row) -> RusqliteResult<Self> {
|
||||
Ok(ColumnInfo {
|
||||
name: row.get("name")?,
|
||||
is_pk: row.get::<_, i64>("pk")? > 0,
|
||||
@ -34,145 +131,260 @@ impl ColumnInfo {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TriggerManager;
|
||||
fn is_safe_identifier(name: &str) -> bool {
|
||||
!name.is_empty() && name.chars().all(|c| c.is_alphanumeric() || c == '_')
|
||||
}
|
||||
|
||||
impl TriggerManager {
|
||||
pub fn new() -> Self {
|
||||
TriggerManager {}
|
||||
/// Richtet CRDT-Trigger für eine einzelne Tabelle ein.
|
||||
pub fn setup_triggers_for_table(
|
||||
conn: &mut Connection,
|
||||
table_name: &str,
|
||||
) -> Result<TriggerSetupResult, CrdtSetupError> {
|
||||
if !is_safe_identifier(table_name) {
|
||||
return Err(rusqlite::Error::InvalidParameterName(format!(
|
||||
"Invalid or unsafe table name provided: {}",
|
||||
table_name
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
pub fn setup_triggers_for_table(
|
||||
&self,
|
||||
conn: &mut Connection,
|
||||
table_name: &str,
|
||||
) -> Result<TriggerSetupResult, rusqlite::Error> {
|
||||
let columns = self.get_table_schema(conn, table_name)?;
|
||||
let columns = get_table_schema(conn, table_name)?;
|
||||
|
||||
if columns.is_empty() {
|
||||
return Ok(TriggerSetupResult::TableNotFound);
|
||||
}
|
||||
|
||||
if !columns.iter().any(|c| c.name == TOMBSTONE_COLUMN) {
|
||||
return Ok(TriggerSetupResult::TombstoneColumnMissing {
|
||||
column_name: TOMBSTONE_COLUMN.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let pks: Vec<String> = columns
|
||||
.iter()
|
||||
.filter(|c| c.is_pk)
|
||||
.map(|c| c.name.clone())
|
||||
.collect();
|
||||
if pks.is_empty() {
|
||||
return Ok(TriggerSetupResult::PrimaryKeyMissing);
|
||||
}
|
||||
|
||||
let cols_to_track: Vec<String> = columns
|
||||
.iter()
|
||||
.filter(|c| !c.is_pk && c.name != TOMBSTONE_COLUMN)
|
||||
.map(|c| c.name.clone())
|
||||
.collect();
|
||||
|
||||
let insert_trigger_sql = self.generate_insert_trigger_sql(table_name, &pks, &cols_to_track);
|
||||
let update_trigger_sql = self.generate_update_trigger_sql(table_name, &pks, &cols_to_track);
|
||||
let drop_insert_trigger_sql = self.drop_trigger_sql(table_name, "insert");
|
||||
|
||||
let tx = conn.transaction()?;
|
||||
tx.execute_batch(&format!("{}\n{}", insert_trigger_sql, update_trigger_sql))?;
|
||||
tx.commit()?;
|
||||
|
||||
Ok(TriggerSetupResult::Success)
|
||||
if columns.is_empty() {
|
||||
return Ok(TriggerSetupResult::TableNotFound);
|
||||
}
|
||||
|
||||
fn get_table_schema(&self, conn: &Connection, table_name: &str) -> Result<Vec<ColumnInfo>> {
|
||||
let sql = format!("PRAGMA table_info('{}');", table_name);
|
||||
let mut stmt = conn.prepare(&sql)?;
|
||||
let rows = stmt.query_map([], ColumnInfo::from_row)?;
|
||||
rows.collect()
|
||||
}
|
||||
|
||||
fn generate_insert_trigger_sql(
|
||||
&self,
|
||||
table_name: &str,
|
||||
pks: &[String],
|
||||
cols: &[String],
|
||||
) -> String {
|
||||
let pk_json_payload = pks
|
||||
.iter()
|
||||
.map(|pk| format!("'{}', NEW.\"{}\"", pk, pk))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
let column_inserts = cols.iter().fold(String::new(), |mut acc, col| {
|
||||
writeln!(&mut acc, "INSERT INTO {log_table} (hlc_timestamp, op_type, table_name, row_pk, column_name, value) VALUES (NEW.\"{hlc_col}\", 'INSERT', '{table}', json_object({pk_payload}), '{column}', json_object('value', NEW.\"{column}\"));",
|
||||
log_table = LOG_TABLE_NAME,
|
||||
hlc_col = HLC_TIMESTAMP_COLUMN,
|
||||
table = table_name,
|
||||
pk_payload = pk_json_payload,
|
||||
column = col
|
||||
).unwrap();
|
||||
acc
|
||||
if !columns.iter().any(|c| c.name == TOMBSTONE_COLUMN) {
|
||||
return Err(CrdtSetupError::TombstoneColumnMissing {
|
||||
table_name: table_name.to_string(),
|
||||
column_name: TOMBSTONE_COLUMN.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Verwende die neue Konstante für den Trigger-Namen
|
||||
let trigger_name = INSERT_TRIGGER_TPL.replace("{TABLE_NAME}", table_name);
|
||||
let pks: Vec<String> = columns
|
||||
.iter()
|
||||
.filter(|c| c.is_pk)
|
||||
.map(|c| c.name.clone())
|
||||
.collect();
|
||||
|
||||
format!(
|
||||
"CREATE TRIGGER IF NOT EXISTS {trigger_name}
|
||||
if pks.is_empty() {
|
||||
return Err(CrdtSetupError::PrimaryKeyMissing {
|
||||
table_name: table_name.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let cols_to_track: Vec<String> = columns
|
||||
.iter()
|
||||
.filter(|c| !c.is_pk && c.name != TOMBSTONE_COLUMN && c.name != HLC_TIMESTAMP_COLUMN)
|
||||
.map(|c| c.name.clone())
|
||||
.collect();
|
||||
|
||||
let insert_trigger_sql = generate_insert_trigger_sql(table_name, &pks, &cols_to_track);
|
||||
let update_trigger_sql = generate_update_trigger_sql(table_name, &pks, &cols_to_track);
|
||||
|
||||
let sql_batch = format!("{}\n{}", insert_trigger_sql, update_trigger_sql);
|
||||
|
||||
// Führe die Erstellung innerhalb einer Transaktion aus
|
||||
let tx = conn.transaction()?;
|
||||
tx.execute_batch(&sql_batch)?;
|
||||
tx.commit()?;
|
||||
|
||||
Ok(TriggerSetupResult::Success)
|
||||
}
|
||||
/// Holt das Schema für eine gegebene Tabelle.
|
||||
/// WICHTIG: Dies ist eine private Hilfsfunktion. Sie geht davon aus, dass `table_name`
|
||||
/// bereits vom öffentlichen Aufrufer (setup_triggers_for_table) validiert wurde.
|
||||
fn get_table_schema(conn: &Connection, table_name: &str) -> RusqliteResult<Vec<ColumnInfo>> {
|
||||
let sql = format!("PRAGMA table_info(\"{}\");", table_name);
|
||||
let mut stmt = conn.prepare(&sql)?;
|
||||
let rows = stmt.query_map([], ColumnInfo::from_row)?;
|
||||
rows.collect()
|
||||
}
|
||||
|
||||
pub fn drop_triggers_for_table(
|
||||
tx: &Transaction, // Arbeitet direkt auf einer Transaktion
|
||||
table_name: &str,
|
||||
) -> Result<(), CrdtSetupError> {
|
||||
if !is_safe_identifier(table_name) {
|
||||
return Err(rusqlite::Error::InvalidParameterName(format!(
|
||||
"Invalid or unsafe table name provided: {}",
|
||||
table_name
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
let drop_insert_trigger_sql =
|
||||
drop_trigger_sql(INSERT_TRIGGER_TPL.replace("{TABLE_NAME}", table_name));
|
||||
let drop_update_trigger_sql =
|
||||
drop_trigger_sql(UPDATE_TRIGGER_TPL.replace("{TABLE_NAME}", table_name));
|
||||
|
||||
let sql_batch = format!("{}\n{}", drop_insert_trigger_sql, drop_update_trigger_sql);
|
||||
|
||||
tx.execute_batch(&sql_batch)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn recreate_triggers_for_table(
|
||||
conn: &mut Connection,
|
||||
table_name: &str,
|
||||
) -> Result<TriggerSetupResult, CrdtSetupError> {
|
||||
// Starte eine einzige Transaktion für beide Operationen
|
||||
let tx = conn.transaction()?;
|
||||
|
||||
// 1. Rufe die Drop-Funktion auf
|
||||
drop_triggers_for_table(&tx, table_name)?;
|
||||
|
||||
// 2. Erstelle die Trigger neu (vereinfachte Logik ohne Drop)
|
||||
// Wir rufen die `setup_triggers_for_table` Logik hier manuell nach,
|
||||
// um die Transaktion weiterzuverwenden.
|
||||
let columns = get_table_schema(&tx, table_name)?;
|
||||
|
||||
if columns.is_empty() {
|
||||
tx.commit()?; // Wichtig: Transaktion beenden
|
||||
return Ok(TriggerSetupResult::TableNotFound);
|
||||
}
|
||||
// ... (Validierungslogik wiederholen) ...
|
||||
if !columns.iter().any(|c| c.name == TOMBSTONE_COLUMN) {
|
||||
/* ... */
|
||||
return Err(CrdtSetupError::TombstoneColumnMissing {
|
||||
table_name: table_name.to_string(),
|
||||
column_name: TOMBSTONE_COLUMN.to_string(),
|
||||
});
|
||||
}
|
||||
let pks: Vec<String> = columns
|
||||
.iter()
|
||||
.filter(|c| c.is_pk)
|
||||
.map(|c| c.name.clone())
|
||||
.collect();
|
||||
if pks.is_empty() {
|
||||
/* ... */
|
||||
return Err(CrdtSetupError::PrimaryKeyMissing {
|
||||
table_name: table_name.to_string(),
|
||||
});
|
||||
}
|
||||
let cols_to_track: Vec<String> = columns
|
||||
.iter()
|
||||
.filter(|c| !c.is_pk && c.name != TOMBSTONE_COLUMN && c.name != HLC_TIMESTAMP_COLUMN)
|
||||
.map(|c| c.name.clone())
|
||||
.collect();
|
||||
|
||||
let insert_trigger_sql = generate_insert_trigger_sql(table_name, &pks, &cols_to_track);
|
||||
let update_trigger_sql = generate_update_trigger_sql(table_name, &pks, &cols_to_track);
|
||||
let sql_batch = format!("{}\n{}", insert_trigger_sql, update_trigger_sql);
|
||||
tx.execute_batch(&sql_batch)?;
|
||||
|
||||
// Beende die Transaktion
|
||||
tx.commit()?;
|
||||
|
||||
Ok(TriggerSetupResult::Success)
|
||||
}
|
||||
|
||||
/// Generiert das SQL für den INSERT-Trigger.
|
||||
fn generate_insert_trigger_sql(table_name: &str, pks: &[String], cols: &[String]) -> String {
|
||||
let pk_json_payload = pks
|
||||
.iter()
|
||||
.map(|pk| format!("'{}', NEW.\"{}\"", pk, pk))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
let column_inserts = cols.iter().fold(String::new(), |mut acc, col| {
|
||||
writeln!(&mut acc, " INSERT INTO {log_table} (hlc_timestamp, op_type, table_name, row_pk, column_name, value) VALUES (NEW.\"{hlc_col}\", 'INSERT', '{table}', json_object({pk_payload}), '{column}', json_object('value', NEW.\"{column}\"));",
|
||||
log_table = TABLE_CRDT_LOGS,
|
||||
hlc_col = HLC_TIMESTAMP_COLUMN,
|
||||
table = table_name,
|
||||
pk_payload = pk_json_payload,
|
||||
column = col
|
||||
).unwrap();
|
||||
acc
|
||||
});
|
||||
|
||||
let trigger_name = INSERT_TRIGGER_TPL.replace("{TABLE_NAME}", table_name);
|
||||
|
||||
format!(
|
||||
"CREATE TRIGGER IF NOT EXISTS \"{trigger_name}\"
|
||||
AFTER INSERT ON \"{table_name}\"
|
||||
WHEN (SELECT value FROM \"{config_table}\" WHERE key = '{sync_key}') IS NOT '1'
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
{column_inserts}
|
||||
END;"
|
||||
)
|
||||
}
|
||||
END;",
|
||||
config_table = TABLE_CRDT_CONFIGS,
|
||||
sync_key = SYNC_ACTIVE_KEY
|
||||
)
|
||||
}
|
||||
|
||||
fn drop_trigger_sql(&self, table: &str, action: &str) -> String {
|
||||
format!("DROP TRIGGER IF EXISTS z_crdt_{table}_{action};")
|
||||
}
|
||||
/// Generiert das SQL zum Löschen eines Triggers.
|
||||
fn drop_trigger_sql(trigger_name: String) -> String {
|
||||
format!("DROP TRIGGER IF EXISTS \"{}\";", trigger_name)
|
||||
}
|
||||
|
||||
fn generate_update_trigger_sql(
|
||||
&self,
|
||||
table_name: &str,
|
||||
pks: &[String],
|
||||
cols: &[String],
|
||||
) -> String {
|
||||
let pk_json_payload = pks
|
||||
.iter()
|
||||
.map(|pk| format!("'{}', NEW.\"{}\"", pk, pk))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
/// Generiert das SQL für den UPDATE-Trigger.
|
||||
fn generate_update_trigger_sql(table_name: &str, pks: &[String], cols: &[String]) -> String {
|
||||
let pk_json_payload = pks
|
||||
.iter()
|
||||
.map(|pk| format!("'{}', NEW.\"{}\"", pk, pk))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
let column_updates = cols.iter().fold(String::new(), |mut acc, col| {
|
||||
writeln!(&mut acc, "IF NEW.\"{column}\" IS NOT OLD.\"{column}\" THEN INSERT INTO {log_table} (hlc_timestamp, op_type, table_name, row_pk, column_name, value, old_value) VALUES (NEW.\"{hlc_col}\", 'UPDATE', '{table}', json_object({pk_payload}), '{column}', json_object('value', NEW.\"{column}\"), json_object('value', OLD.\"{column}\")); END IF;",
|
||||
log_table = LOG_TABLE_NAME,
|
||||
hlc_col = HLC_TIMESTAMP_COLUMN,
|
||||
table = table_name,
|
||||
pk_payload = pk_json_payload,
|
||||
column = col).unwrap();
|
||||
acc
|
||||
});
|
||||
|
||||
let soft_delete_logic = format!(
|
||||
"IF NEW.{tombstone_col} = 1 AND OLD.{tombstone_col} = 0 THEN INSERT INTO {log_table} (hlc_timestamp, op_type, table_name, row_pk) VALUES (NEW.\"{hlc_col}\", 'DELETE', '{table}', json_object({pk_payload})); END IF;",
|
||||
log_table = LOG_TABLE_NAME,
|
||||
let column_updates = cols.iter().fold(String::new(), |mut acc, col| {
|
||||
writeln!(&mut acc, " IF NEW.\"{column}\" IS NOT OLD.\"{column}\" THEN INSERT INTO {log_table} (hlc_timestamp, op_type, table_name, row_pk, column_name, value, old_value) VALUES (NEW.\"{hlc_col}\", 'UPDATE', '{table}', json_object({pk_payload}), '{column}', json_object('value', NEW.\"{column}\"), json_object('value', OLD.\"{column}\")); END IF;",
|
||||
log_table = TABLE_CRDT_LOGS,
|
||||
hlc_col = HLC_TIMESTAMP_COLUMN,
|
||||
tombstone_col = TOMBSTONE_COLUMN,
|
||||
table = table_name,
|
||||
pk_payload = pk_json_payload
|
||||
);
|
||||
pk_payload = pk_json_payload,
|
||||
column = col
|
||||
).unwrap();
|
||||
acc
|
||||
});
|
||||
|
||||
// Verwende die neue Konstante für den Trigger-Namen
|
||||
let trigger_name = UPDATE_TRIGGER_TPL.replace("{TABLE_NAME}", table_name);
|
||||
let soft_delete_logic = format!(
|
||||
" IF NEW.\"{tombstone_col}\" = 1 AND OLD.\"{tombstone_col}\" = 0 THEN INSERT INTO {log_table} (hlc_timestamp, op_type, table_name, row_pk) VALUES (NEW.\"{hlc_col}\", 'DELETE', '{table}', json_object({pk_payload})); END IF;",
|
||||
log_table = TABLE_CRDT_LOGS,
|
||||
hlc_col = HLC_TIMESTAMP_COLUMN,
|
||||
tombstone_col = TOMBSTONE_COLUMN,
|
||||
table = table_name,
|
||||
pk_payload = pk_json_payload
|
||||
);
|
||||
|
||||
format!(
|
||||
"CREATE TRIGGER IF NOT EXISTS {trigger_name}
|
||||
let trigger_name = UPDATE_TRIGGER_TPL.replace("{TABLE_NAME}", table_name);
|
||||
|
||||
format!(
|
||||
"CREATE TRIGGER IF NOT EXISTS \"{trigger_name}\"
|
||||
AFTER UPDATE ON \"{table_name}\"
|
||||
WHEN (SELECT value FROM \"{config_table}\" WHERE key = '{sync_key}') IS NOT '1'
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
{column_updates}
|
||||
{soft_delete_logic}
|
||||
END;"
|
||||
)
|
||||
}
|
||||
{column_updates}
|
||||
{soft_delete_logic}
|
||||
END;",
|
||||
config_table = TABLE_CRDT_CONFIGS,
|
||||
sync_key = SYNC_ACTIVE_KEY
|
||||
)
|
||||
}
|
||||
|
||||
/// Durchläuft alle `haex_`-Tabellen und richtet die CRDT-Trigger ein.
|
||||
pub fn generate_haex_triggers(conn: &mut Connection) -> Result<(), rusqlite::Error> {
|
||||
println!("🔄 Setup CRDT triggers...");
|
||||
let table_names: Vec<String> = {
|
||||
let mut stmt = conn.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'haex_%' AND name NOT LIKE 'haex_crdt_%';")?;
|
||||
let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
|
||||
rows.collect::<RusqliteResult<Vec<String>>>()?
|
||||
};
|
||||
for table_name in table_names {
|
||||
if table_name == TABLE_CRDT_CONFIGS {
|
||||
continue;
|
||||
}
|
||||
println!("➡️ Processing table: {}", table_name);
|
||||
match setup_triggers_for_table(conn, &table_name) {
|
||||
Ok(TriggerSetupResult::Success) => {
|
||||
println!(" ✅ Triggers created for {}", table_name)
|
||||
}
|
||||
Ok(TriggerSetupResult::TableNotFound) => {
|
||||
println!(" ℹ️ Table {} not found, skipping.", table_name)
|
||||
}
|
||||
Err(e) => println!(" ❌ Could not set up triggers for {}: {}", table_name, e),
|
||||
}
|
||||
}
|
||||
println!("✨ Done setting up CRDT triggers.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// database/core.rs
|
||||
use crate::crdt::hlc;
|
||||
|
||||
use crate::database::DbConnection;
|
||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||
use rusqlite::{
|
||||
@ -166,7 +166,7 @@ pub fn open_and_init_db(path: &str, key: &str, create: bool) -> Result<Connectio
|
||||
|
||||
let conn = Connection::open_with_flags(path, flags).map_err(|e| {
|
||||
format!(
|
||||
"Dateiii gibt es nicht: {}. Habe nach {} gesucht",
|
||||
"Datei gibt es nicht: {}. Habe nach {} gesucht",
|
||||
e.to_string(),
|
||||
path
|
||||
)
|
||||
@ -174,8 +174,8 @@ pub fn open_and_init_db(path: &str, key: &str, create: bool) -> Result<Connectio
|
||||
conn.pragma_update(None, "key", key)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
conn.execute_batch("SELECT count(*) from haex_extensions")
|
||||
.map_err(|e| e.to_string())?;
|
||||
/* conn.execute_batch("SELECT count(*) from haex_extensions")
|
||||
.map_err(|e| e.to_string())?; */
|
||||
|
||||
let journal_mode: String = conn
|
||||
.query_row("PRAGMA journal_mode=WAL;", [], |row| row.get(0))
|
||||
|
||||
@ -5,10 +5,10 @@ use rusqlite::Connection;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tauri::{path::BaseDirectory, AppHandle, Manager, State, Wry};
|
||||
use tauri::{path::BaseDirectory, AppHandle, Manager, State};
|
||||
|
||||
use crate::database::core::open_and_init_db;
|
||||
pub struct HlcService(pub Mutex<uhlc::HLC>);
|
||||
|
||||
@ -1,14 +1,19 @@
|
||||
//mod browser;
|
||||
mod android_storage;
|
||||
//mod android_storage;
|
||||
pub mod crdt;
|
||||
mod database;
|
||||
mod extension;
|
||||
mod models;
|
||||
pub mod table_names {
|
||||
include!(concat!(env!("OUT_DIR"), "/tableNames.rs"));
|
||||
}
|
||||
|
||||
use database::DbConnection;
|
||||
use models::ExtensionState;
|
||||
use rusqlite::{Connection, OpenFlags};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::database::DbConnection;
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let protocol_name = "haex-extension";
|
||||
@ -67,9 +72,9 @@ pub fn run() {
|
||||
extension::copy_directory,
|
||||
extension::database::extension_sql_execute,
|
||||
extension::database::extension_sql_select,
|
||||
android_storage::request_storage_permission,
|
||||
/* android_storage::request_storage_permission,
|
||||
android_storage::has_storage_permission,
|
||||
android_storage::get_external_storage_paths,
|
||||
android_storage::get_external_storage_paths, */
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
<template>
|
||||
<button
|
||||
aria-disabled
|
||||
class="flex gap-4 w-full px-4 py-2"
|
||||
@click="$emit('click', menuItem)"
|
||||
:style="{ color: menuItem.color ?? '' }"
|
||||
@click="$emit('click', menuItem)"
|
||||
>
|
||||
<Icon
|
||||
:name="menuIcon"
|
||||
@ -34,7 +33,7 @@ const menuIcon = computed(() =>
|
||||
menuItem?.icon
|
||||
? menuItem.icon
|
||||
: menuItem.type === 'group'
|
||||
? 'mdi:folder-outline'
|
||||
: 'mdi:key-outline',
|
||||
? 'mdi:folder-outline'
|
||||
: 'mdi:key-outline',
|
||||
)
|
||||
</script>
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
:title
|
||||
:description
|
||||
:fullscreen="isSmallScreen"
|
||||
:ui="{ header: 'pt-10 sm:pt-0', footer: 'mb-10 sm:mb-0' }"
|
||||
>
|
||||
<slot>
|
||||
<!-- <UiButton
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex flex-col">
|
||||
<header
|
||||
class="bg-default/70 backdrop-blur border-b border-accented h-(--ui-header-height) sticky top-0 z-50 flex"
|
||||
class="bg-default/90 backdrop-blur border-b border-accented h-(--ui-header-height) sticky top-0 z-50 flex"
|
||||
>
|
||||
<div class="px-2 bg-primary rounded-br-xs">
|
||||
<UiLogoHaexhub class="p-2 size-12 shrink-0" />
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="bg-default isolate h-dvh">
|
||||
<div class="bg-default isolate h-dvh py-4 sm:py-0">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user