* feat: init database triggers * feat: ✨ wip: database_triggers * feat: ✨ add database triggers front view * feat: 🚧 database_triggers * feat: 🚧 add definition in yaml, updated backend code and added migration * feat: 🚧 updated migration file, update openapi.yml, updated database_triggers page and backend function * fix: struct rust * feat: 🚧 update migrate, database trigger backend function fixed * feat: 🚧 add resource picker front, update backend function * feat: 🚧 edit inner database inner, update triggers * feat: 🚧 database_triggers * feat: 🚧 update openapi yaml, prettied websocker trigger * feat: 🚧 database_triggers * feat: 🚧 add resource module, update variable file, working on main loop for database_trigger * feat: 🐛 working sqlx query * feat: 🚧 fix query with sqlx, added main loop * feat: 🚧 add new column database_trigg * feat: 🛂 run jobs works * feat: 🚧 handling slot name and replication * feat: restructring triggers, decoding trigger message on work * feat: 🚧 database_trigger * feat: 🚧 * feat: 🚧 converter done, work on custom script * feat: 🚧 multiple trigger * feat: 🚧 adding new argument function * feat: 🚧 database_trigger * feat: 🚧 add generate template for front, update script picker * feat: 🚧 template script fix bug, work on restructing backend logic * feat: 🚧 update autogenerated script, add persistence state for template script * feat: 🚧 update structure client * feat: 🚧 rewrited crud function * feat: 🚧 added publication handler * feat: 🚧 new ui finished * feat: 🚧 added slot function hanlder finish front ux * feat: ✨ ux improvement done, backend logic done * feat: 🐛 fix where clause * feat: * feat: * chore: update .sqlx and remove empty package * fix: update publication and remove unneccessary print * feat: finish converter to json, remove save button, remove unused crate * feat: update migration for database trigger, fixed query, fixed frontend error with missing feature on back * chore: update .sqlx * chore: update sqlx * chore: .sqlx * chore: add unused * fix: use right database resource in back and front * fix build * refactor: * fix sqlx * nits: retrieve all script from database trigger editor * fix: template script object, fix route name * merge main * feat: rework ux * feat/finish ux and update backend logic * feat: database_trigger * feat: database_trigger * chore: update sqlx * chore: fix ci * nits: fix worker.rs * feat: database_trigger * Update frontend/src/lib/components/triggers/database/DatabaseTriggersPanel.svelte Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * feat: * fix ci * nits: database to postgres * nits: fix .sqlx * feat: add link to original code and open tab functionallity * nits: update .sqlx * nits: fix request type and yaml file * nits: add abel suggestion * fix: update .sqlx * fix: update .sqlx * nits: add comment to hex.rs * nits: remove features, rename database to postgres name * nits: replace database name by postgres * nits: database name to postgres * nits: update .sqlx * nits: update .sqlx * nit * nit * nits * nit * nits * nits * nits * nits * fix sqlx * nits: update .vscode --------- Co-authored-by: HugoCasa <hugo@casademont.ch> Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
511 lines
16 KiB
Rust
511 lines
16 KiB
Rust
#![allow(unused)]
|
|
|
|
use core::str;
|
|
use std::{
|
|
cmp,
|
|
io::{self, Cursor, Read},
|
|
str::Utf8Error,
|
|
};
|
|
|
|
use byteorder::{BigEndian, ReadBytesExt};
|
|
use bytes::Bytes;
|
|
use rust_postgres::types::{Oid, Type};
|
|
use thiserror::Error;
|
|
|
|
use super::trigger::LogicalReplicationSettings;
|
|
const PRIMARY_KEEPALIVE_BYTE: u8 = b'k';
|
|
const X_LOG_DATA_BYTE: u8 = b'w';
|
|
|
|
/**
|
|
* This implementation is inspired by Postgres replication functionality
|
|
* from https://github.com/supabase/pg_replicate
|
|
*
|
|
* Original implementation:
|
|
* - https://github.com/supabase/pg_replicate/blob/main/pg_replicate/src/conversions/cdc_event.rs
|
|
*
|
|
*/
|
|
|
|
#[derive(Debug)]
|
|
pub struct PrimaryKeepAliveBody {
|
|
pub wal_end: u64,
|
|
pub timestamp: i64,
|
|
pub reply: bool,
|
|
}
|
|
|
|
impl PrimaryKeepAliveBody {
|
|
pub fn new(wal_end: u64, timestamp: i64, reply: bool) -> PrimaryKeepAliveBody {
|
|
PrimaryKeepAliveBody { wal_end, timestamp, reply }
|
|
}
|
|
}
|
|
|
|
const BEGIN_BYTE: u8 = b'B';
|
|
const COMMIT_BYTE: u8 = b'C';
|
|
const ORIGIN_BYTE: u8 = b'O';
|
|
const RELATION_BYTE: u8 = b'R';
|
|
const TYPE_BYTE: u8 = b'Y';
|
|
const INSERT_BYTE: u8 = b'I';
|
|
const UPDATE_BYTE: u8 = b'U';
|
|
const DELETE_BYTE: u8 = b'D';
|
|
const TUPLE_NEW_BYTE: u8 = b'N';
|
|
const TUPLE_KEY_BYTE: u8 = b'K';
|
|
const TUPLE_OLD_BYTE: u8 = b'O';
|
|
const TUPLE_DATA_NULL_BYTE: u8 = b'n';
|
|
const TUPLE_DATA_TOAST_BYTE: u8 = b'u';
|
|
const TUPLE_DATA_TEXT_BYTE: u8 = b't';
|
|
const TUPLE_DATA_BINARY_BYTE: u8 = b'b';
|
|
|
|
const REPLICA_IDENTITY_DEFAULT_BYTE: i8 = 0x64;
|
|
const REPLICA_IDENTITY_NOTHING_BYTE: i8 = 0x6E;
|
|
const REPLICA_IDENTITY_FULL_BYTE: i8 = 0x66;
|
|
const REPLICA_IDENTITY_INDEX_BYTE: i8 = 0x69;
|
|
|
|
#[derive(Debug)]
|
|
pub enum ReplicaIdentity {
|
|
Default,
|
|
Nothing,
|
|
Full,
|
|
Index,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct Column {
|
|
pub flags: i8,
|
|
pub name: String,
|
|
pub type_o_id: Option<Type>,
|
|
pub type_modifier: i32,
|
|
}
|
|
|
|
impl Column {
|
|
pub fn new(flags: i8, name: String, type_o_id: Option<Type>, type_modifier: i32) -> Self {
|
|
Self { flags, name, type_o_id, type_modifier }
|
|
}
|
|
}
|
|
|
|
pub type Columns = Vec<Column>;
|
|
|
|
#[derive(Debug)]
|
|
pub struct RelationBody {
|
|
pub transaction_id: Option<i32>,
|
|
pub o_id: Oid,
|
|
pub namespace: String,
|
|
pub name: String,
|
|
pub replica_identity: ReplicaIdentity,
|
|
pub columns: Columns,
|
|
}
|
|
|
|
impl RelationBody {
|
|
pub fn new(
|
|
transaction_id: Option<i32>,
|
|
o_id: Oid,
|
|
namespace: String,
|
|
name: String,
|
|
replica_identity: ReplicaIdentity,
|
|
columns: Columns,
|
|
) -> Self {
|
|
Self { transaction_id, o_id, namespace, name, replica_identity, columns }
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct InsertBody {
|
|
pub transaction_id: Option<i32>,
|
|
pub o_id: Oid,
|
|
pub tuple: Vec<TupleData>,
|
|
}
|
|
|
|
impl InsertBody {
|
|
pub fn new(transaction_id: Option<i32>, o_id: Oid, tuple: Vec<TupleData>) -> Self {
|
|
Self { transaction_id, o_id, tuple }
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct UpdateBody {
|
|
transaction_id: Option<i32>,
|
|
pub o_id: Oid,
|
|
pub old_tuple: Option<Vec<TupleData>>,
|
|
pub key_tuple: Option<Vec<TupleData>>,
|
|
pub new_tuple: Vec<TupleData>,
|
|
}
|
|
|
|
impl UpdateBody {
|
|
pub fn new(
|
|
transaction_id: Option<i32>,
|
|
o_id: Oid,
|
|
old_tuple: Option<Vec<TupleData>>,
|
|
key_tuple: Option<Vec<TupleData>>,
|
|
new_tuple: Vec<TupleData>,
|
|
) -> Self {
|
|
Self { transaction_id, o_id, old_tuple, key_tuple, new_tuple }
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct DeleteBody {
|
|
transaction_id: Option<i32>,
|
|
pub o_id: Oid,
|
|
pub old_tuple: Option<Vec<TupleData>>,
|
|
pub key_tuple: Option<Vec<TupleData>>,
|
|
}
|
|
|
|
impl DeleteBody {
|
|
pub fn new(
|
|
transaction_id: Option<i32>,
|
|
o_id: Oid,
|
|
old_tuple: Option<Vec<TupleData>>,
|
|
key_tuple: Option<Vec<TupleData>>,
|
|
) -> Self {
|
|
Self { transaction_id, o_id, old_tuple, key_tuple }
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum TupleData {
|
|
Null,
|
|
UnchangedToast,
|
|
Text(Bytes),
|
|
Binary(Bytes),
|
|
}
|
|
|
|
impl TupleData {
|
|
fn parse(buf: &mut Buffer) -> Result<Vec<TupleData>, ConversionError> {
|
|
let number_of_columns = buf.read_i16::<BigEndian>()?;
|
|
let mut tuples = Vec::with_capacity(number_of_columns as usize);
|
|
for _ in 0..number_of_columns {
|
|
let byte = buf.read_u8()?;
|
|
let tuple_data = match byte {
|
|
TUPLE_DATA_NULL_BYTE => TupleData::Null,
|
|
TUPLE_DATA_TOAST_BYTE => TupleData::UnchangedToast,
|
|
TUPLE_DATA_TEXT_BYTE => {
|
|
let len = buf.read_i32::<BigEndian>()?;
|
|
let mut data = vec![0; len as usize];
|
|
buf.read_exact(&mut data)?;
|
|
TupleData::Text(data.into())
|
|
}
|
|
TUPLE_DATA_BINARY_BYTE => {
|
|
let len = buf.read_i32::<BigEndian>()?;
|
|
let mut data = vec![0; len as usize];
|
|
buf.read_exact(&mut data)?;
|
|
TupleData::Binary(data.into())
|
|
}
|
|
byte => {
|
|
return Err(ConversionError::Io(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
format!("unknown replication message byte `{}`", byte),
|
|
)));
|
|
}
|
|
};
|
|
|
|
tuples.push(tuple_data);
|
|
}
|
|
|
|
Ok(tuples)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum TransactionBody {
|
|
Insert(InsertBody),
|
|
Update(UpdateBody),
|
|
Delete(DeleteBody),
|
|
}
|
|
|
|
#[non_exhaustive]
|
|
#[derive(Debug)]
|
|
pub enum LogicalReplicationMessage {
|
|
Begin,
|
|
Commit,
|
|
Relation(RelationBody),
|
|
Type,
|
|
Insert(InsertBody),
|
|
Update(UpdateBody),
|
|
Delete(DeleteBody),
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct XLogDataBody {
|
|
pub wal_start: u64,
|
|
pub wal_end: u64,
|
|
pub timestamp: i64,
|
|
pub data: Bytes,
|
|
}
|
|
|
|
#[derive(Error, Debug)]
|
|
pub enum ConversionError {
|
|
#[error("Error: {0}")]
|
|
Io(#[from] io::Error),
|
|
#[error("Utf8Error conversion: {0}")]
|
|
Utf8(#[from] Utf8Error),
|
|
}
|
|
|
|
struct Buffer {
|
|
bytes: Bytes,
|
|
idx: usize,
|
|
}
|
|
|
|
impl Buffer {
|
|
pub fn new(bytes: Bytes, idx: usize) -> Buffer {
|
|
Buffer { bytes, idx }
|
|
}
|
|
|
|
fn slice(&self) -> &[u8] {
|
|
&self.bytes[self.idx..]
|
|
}
|
|
|
|
fn read_cstr(&mut self) -> Result<String, ConversionError> {
|
|
match self.slice().iter().position(|&x| x == 0) {
|
|
Some(pos) => {
|
|
let start = self.idx;
|
|
let end = start + pos;
|
|
let cstr = str::from_utf8(&self.bytes[start..end])?.to_owned();
|
|
self.idx = end + 1;
|
|
Ok(cstr)
|
|
}
|
|
None => Err(ConversionError::Io(io::Error::new(
|
|
io::ErrorKind::UnexpectedEof,
|
|
"unexpected EOF",
|
|
))),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Read for Buffer {
|
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
|
let len = {
|
|
let slice = self.slice();
|
|
let len = cmp::min(slice.len(), buf.len());
|
|
buf[..len].copy_from_slice(&slice[..len]);
|
|
len
|
|
};
|
|
self.idx += len;
|
|
Ok(len)
|
|
}
|
|
}
|
|
|
|
impl XLogDataBody {
|
|
pub fn new(wal_start: u64, wal_end: u64, timestamp: i64, data: Bytes) -> XLogDataBody {
|
|
XLogDataBody { wal_start, wal_end, timestamp, data }
|
|
}
|
|
|
|
pub fn parse(
|
|
self,
|
|
logical_replication_settings: &LogicalReplicationSettings,
|
|
) -> Result<LogicalReplicationMessage, ConversionError> {
|
|
let mut buf = Buffer::new(self.data.clone(), 0);
|
|
let byte = buf.read_u8()?;
|
|
|
|
let logical_replication_message = match byte {
|
|
BEGIN_BYTE => {
|
|
buf.read_i64::<BigEndian>()?;
|
|
buf.read_i64::<BigEndian>()?;
|
|
buf.read_i32::<BigEndian>()?;
|
|
|
|
LogicalReplicationMessage::Begin
|
|
}
|
|
COMMIT_BYTE => {
|
|
buf.read_i8()?;
|
|
buf.read_u64::<BigEndian>()?;
|
|
buf.read_u64::<BigEndian>()?;
|
|
buf.read_i64::<BigEndian>()?;
|
|
LogicalReplicationMessage::Commit
|
|
}
|
|
RELATION_BYTE => {
|
|
let transaction_id = match logical_replication_settings.streaming {
|
|
true => Some(buf.read_i32::<BigEndian>()?),
|
|
false => None,
|
|
};
|
|
|
|
let o_id = buf.read_u32::<BigEndian>()?;
|
|
let namespace = buf.read_cstr()?;
|
|
let name = buf.read_cstr()?;
|
|
let replica_identity = match buf.read_i8()? {
|
|
REPLICA_IDENTITY_DEFAULT_BYTE => ReplicaIdentity::Default,
|
|
REPLICA_IDENTITY_NOTHING_BYTE => ReplicaIdentity::Nothing,
|
|
REPLICA_IDENTITY_FULL_BYTE => ReplicaIdentity::Full,
|
|
REPLICA_IDENTITY_INDEX_BYTE => ReplicaIdentity::Index,
|
|
byte => {
|
|
return Err(ConversionError::Io(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
format!("unknown replica identity byte `{}`", byte),
|
|
)));
|
|
}
|
|
};
|
|
|
|
let num_of_column = buf.read_i16::<BigEndian>()?;
|
|
|
|
let mut columns = Vec::with_capacity(num_of_column as usize);
|
|
for _ in 0..num_of_column {
|
|
let flags = buf.read_i8()?;
|
|
let name = buf.read_cstr()?;
|
|
let o_id = buf.read_u32::<BigEndian>()?;
|
|
let type_modifier = buf.read_i32::<BigEndian>()?;
|
|
let type_o_id = Type::from_oid(o_id);
|
|
let column = Column::new(flags, name, type_o_id, type_modifier);
|
|
|
|
columns.push(column);
|
|
}
|
|
|
|
LogicalReplicationMessage::Relation(RelationBody::new(
|
|
transaction_id,
|
|
o_id,
|
|
namespace,
|
|
name,
|
|
replica_identity,
|
|
columns,
|
|
))
|
|
}
|
|
TYPE_BYTE => {
|
|
buf.read_u32::<BigEndian>()?;
|
|
buf.read_cstr()?;
|
|
buf.read_cstr()?;
|
|
|
|
LogicalReplicationMessage::Type
|
|
}
|
|
INSERT_BYTE => {
|
|
let transaction_id = match logical_replication_settings.streaming {
|
|
true => Some(buf.read_i32::<BigEndian>()?),
|
|
false => None,
|
|
};
|
|
let o_id = buf.read_u32::<BigEndian>()?;
|
|
let byte = buf.read_u8()?;
|
|
|
|
let tuple = match byte {
|
|
TUPLE_NEW_BYTE => TupleData::parse(&mut buf)?,
|
|
byte => {
|
|
return Err(ConversionError::Io(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
format!("unexpected tuple byte `{}`", byte),
|
|
)));
|
|
}
|
|
};
|
|
|
|
LogicalReplicationMessage::Insert(InsertBody::new(transaction_id, o_id, tuple))
|
|
}
|
|
UPDATE_BYTE => {
|
|
let transaction_id = match logical_replication_settings.streaming {
|
|
true => Some(buf.read_i32::<BigEndian>()?),
|
|
false => None,
|
|
};
|
|
let o_id = buf.read_u32::<BigEndian>()?;
|
|
let byte = buf.read_u8()?;
|
|
let mut key_tuple = None;
|
|
let mut old_tuple = None;
|
|
|
|
let new_tuple = match byte {
|
|
TUPLE_NEW_BYTE => TupleData::parse(&mut buf)?,
|
|
TUPLE_OLD_BYTE | TUPLE_KEY_BYTE => {
|
|
if byte == TUPLE_OLD_BYTE {
|
|
old_tuple = Some(TupleData::parse(&mut buf)?);
|
|
} else {
|
|
key_tuple = Some(TupleData::parse(&mut buf)?);
|
|
}
|
|
match buf.read_u8()? {
|
|
TUPLE_NEW_BYTE => TupleData::parse(&mut buf)?,
|
|
byte => {
|
|
return Err(ConversionError::Io(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
format!("unexpected tuple byte `{}`", byte),
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
byte => {
|
|
return Err(ConversionError::Io(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
format!("unknown tuple byte `{}`", byte),
|
|
)));
|
|
}
|
|
};
|
|
|
|
LogicalReplicationMessage::Update(UpdateBody::new(
|
|
transaction_id,
|
|
o_id,
|
|
old_tuple,
|
|
key_tuple,
|
|
new_tuple,
|
|
))
|
|
}
|
|
DELETE_BYTE => {
|
|
let transaction_id = match logical_replication_settings.streaming {
|
|
true => Some(buf.read_i32::<BigEndian>()?),
|
|
false => None,
|
|
};
|
|
let o_id = buf.read_u32::<BigEndian>()?;
|
|
let tag = buf.read_u8()?;
|
|
|
|
let mut key_tuple = None;
|
|
let mut old_tuple = None;
|
|
|
|
match tag {
|
|
TUPLE_OLD_BYTE => old_tuple = Some(TupleData::parse(&mut buf)?),
|
|
TUPLE_KEY_BYTE => key_tuple = Some(TupleData::parse(&mut buf)?),
|
|
tag => {
|
|
return Err(ConversionError::Io(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
format!("unknown tuple tag `{}`", tag),
|
|
)));
|
|
}
|
|
}
|
|
|
|
LogicalReplicationMessage::Delete(DeleteBody::new(
|
|
transaction_id,
|
|
o_id,
|
|
old_tuple,
|
|
key_tuple,
|
|
))
|
|
}
|
|
byte => {
|
|
return Err(ConversionError::Io(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
format!("unknown replication message tag `{}`", byte),
|
|
)));
|
|
}
|
|
};
|
|
|
|
Ok(logical_replication_message)
|
|
}
|
|
}
|
|
|
|
#[non_exhaustive]
|
|
#[derive(Debug)]
|
|
pub enum ReplicationMessage {
|
|
XLogData(XLogDataBody),
|
|
PrimaryKeepAlive(PrimaryKeepAliveBody),
|
|
}
|
|
|
|
impl ReplicationMessage {
|
|
pub fn parse(buf: Bytes) -> io::Result<Self> {
|
|
let (byte, mut message) = buf.split_first().unwrap();
|
|
|
|
let replication_message = match *byte {
|
|
X_LOG_DATA_BYTE => {
|
|
let len = buf.len();
|
|
let wal_start = message.read_u64::<BigEndian>()?;
|
|
let wal_end = message.read_u64::<BigEndian>()?;
|
|
let timestamp = message.read_i64::<BigEndian>()?;
|
|
let len = len - message.len();
|
|
let data = buf.slice(len..);
|
|
ReplicationMessage::XLogData(XLogDataBody::new(wal_start, wal_end, timestamp, data))
|
|
}
|
|
PRIMARY_KEEPALIVE_BYTE => {
|
|
let wal_end = message.read_u64::<BigEndian>()?;
|
|
let timestamp = message.read_i64::<BigEndian>()?;
|
|
let reply = message.read_u8()?;
|
|
ReplicationMessage::PrimaryKeepAlive(PrimaryKeepAliveBody::new(
|
|
wal_end,
|
|
timestamp,
|
|
reply == 1,
|
|
))
|
|
}
|
|
byte => {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
format!("unknown replication message byte `{}`", byte),
|
|
));
|
|
}
|
|
};
|
|
|
|
Ok(replication_message)
|
|
}
|
|
}
|