Add frontend, member_state_log, etc.
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
-- Step 1: Create the member_states lookup table
|
||||
CREATE TABLE member_states (
|
||||
state_code CHAR(1) PRIMARY KEY,
|
||||
state_name VARCHAR(20) NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Insert the three states
|
||||
INSERT INTO member_states (state_code, state_name, description) VALUES
|
||||
('A', 'Active', 'Member is currently active'),
|
||||
('I', 'Inactive', 'Member is temporarily inactive'),
|
||||
('D', 'Deactivated', 'Member has been permanently deactivated');
|
||||
|
||||
-- Step 2: Create the member_state_log table
|
||||
CREATE TABLE member_state_log (
|
||||
log_id SERIAL PRIMARY KEY,
|
||||
member_id INTEGER NOT NULL,
|
||||
state_code CHAR(1) NOT NULL,
|
||||
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
changed_by INTEGER, -- Optional: user ID who made the change
|
||||
reason TEXT, -- Optional: reason for state change
|
||||
FOREIGN KEY (member_id) REFERENCES members(member_id),
|
||||
FOREIGN KEY (state_code) REFERENCES member_states(state_code)
|
||||
);
|
||||
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX idx_member_state_log_member_id ON member_state_log(member_id);
|
||||
CREATE INDEX idx_member_state_log_changed_at ON member_state_log(changed_at);
|
||||
CREATE INDEX idx_member_state_log_state_code ON member_state_log(state_code);
|
||||
|
||||
-- Step 3: Migration script to populate the log table from existing data
|
||||
INSERT INTO member_state_log (member_id, state_code, changed_at)
|
||||
SELECT
|
||||
member_id,
|
||||
CASE
|
||||
WHEN member_active = true THEN 'A'
|
||||
ELSE 'I'
|
||||
END,
|
||||
COALESCE(updated_at, created_at, CURRENT_TIMESTAMP)
|
||||
FROM members;
|
||||
|
||||
-- Step 4: Create a view to get current member states (most recent log entry per member)
|
||||
CREATE OR REPLACE VIEW member_current_state AS
|
||||
SELECT DISTINCT ON (msl.member_id)
|
||||
msl.member_id,
|
||||
msl.state_code,
|
||||
ms.state_name,
|
||||
ms.description,
|
||||
msl.changed_at,
|
||||
msl.changed_by,
|
||||
msl.reason
|
||||
FROM member_state_log msl
|
||||
JOIN member_states ms ON msl.state_code = ms.state_code
|
||||
ORDER BY msl.member_id, msl.changed_at DESC, msl.log_id DESC;
|
||||
|
||||
-- Step 5: Function to change member state (ensures proper logging)
|
||||
CREATE OR REPLACE FUNCTION change_member_state(
|
||||
p_member_id INTEGER,
|
||||
p_new_state_code CHAR(1),
|
||||
p_changed_by INTEGER DEFAULT NULL,
|
||||
p_reason TEXT DEFAULT NULL
|
||||
) RETURNS VOID AS $$
|
||||
BEGIN
|
||||
-- Validate that the state exists
|
||||
IF NOT EXISTS (SELECT 1 FROM member_states WHERE state_code = p_new_state_code) THEN
|
||||
RAISE EXCEPTION 'Invalid state code: %', p_new_state_code;
|
||||
END IF;
|
||||
|
||||
-- Validate that the member exists
|
||||
IF NOT EXISTS (SELECT 1 FROM members WHERE member_id = p_member_id) THEN
|
||||
RAISE EXCEPTION 'Member not found: %', p_member_id;
|
||||
END IF;
|
||||
|
||||
-- Insert the new state log entry
|
||||
INSERT INTO member_state_log (member_id, state_code, changed_by, reason)
|
||||
VALUES (p_member_id, p_new_state_code, p_changed_by, p_reason);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Step 6: Common queries you'll need
|
||||
|
||||
-- Get all active members
|
||||
SELECT m.*, mcs.state_name, mcs.changed_at as last_state_change
|
||||
FROM members m
|
||||
JOIN member_current_state mcs ON m.member_id = mcs.member_id
|
||||
WHERE mcs.state_code = 'A';
|
||||
|
||||
-- Get member state history
|
||||
SELECT
|
||||
msl.changed_at,
|
||||
ms.state_name,
|
||||
msl.reason,
|
||||
msl.changed_by
|
||||
FROM member_state_log msl
|
||||
JOIN member_states ms ON msl.state_code = ms.state_code
|
||||
WHERE msl.member_id = 123 -- Replace with actual member_id
|
||||
ORDER BY msl.changed_at DESC;
|
||||
|
||||
-- Count members by state
|
||||
SELECT
|
||||
ms.state_name,
|
||||
COUNT(*) as member_count
|
||||
FROM member_current_state mcs
|
||||
JOIN member_states ms ON mcs.state_code = ms.state_code
|
||||
GROUP BY ms.state_code, ms.state_name
|
||||
ORDER BY ms.state_code;
|
||||
|
||||
-- Step 7: Example usage of the change_member_state function
|
||||
-- Change member 123 to inactive
|
||||
SELECT change_member_state(123, 'I', 1, 'Member requested temporary suspension');
|
||||
|
||||
-- Reactivate member 123
|
||||
SELECT change_member_state(123, 'A', 1, 'Member reactivation approved');
|
||||
|
||||
-- Step 8: After testing, remove the old column (CAREFUL!)
|
||||
-- ALTER TABLE members DROP COLUMN member_active;
|
||||
@@ -0,0 +1,252 @@
|
||||
-- Step 1: Create the member_states lookup table
|
||||
CREATE TABLE member_states (
|
||||
state_code CHAR(1) PRIMARY KEY,
|
||||
state_name VARCHAR(20) NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Insert the three states
|
||||
INSERT INTO member_states (state_code, state_name, description) VALUES
|
||||
('A', 'Active', 'Member is currently active'),
|
||||
('I', 'Inactive', 'Member is temporarily inactive'),
|
||||
('D', 'Deactivated', 'Member has been permanently deactivated');
|
||||
|
||||
-- Step 2: Create the member_state_log table
|
||||
drop table member_state_log;
|
||||
CREATE TABLE member_state_log (
|
||||
log_id SERIAL PRIMARY KEY,
|
||||
member_id INTEGER NOT NULL,
|
||||
state_code CHAR(2) NOT NULL,
|
||||
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
changed_by INTEGER, -- Optional: user ID who made the change
|
||||
reason TEXT, -- Optional: reason for state change
|
||||
FOREIGN KEY (member_id) REFERENCES members(id),
|
||||
FOREIGN KEY (state_code) REFERENCES member_states(state_code)
|
||||
);
|
||||
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX idx_member_state_log_member_id ON member_state_log(member_id);
|
||||
CREATE INDEX idx_member_state_log_changed_at ON member_state_log(changed_at);
|
||||
CREATE INDEX idx_member_state_log_state_code ON member_state_log(state_code);
|
||||
|
||||
-- Step 3: Migration script to populate the log table from existing data
|
||||
INSERT INTO member_state_log (member_id, state_code, changed_at)
|
||||
SELECT
|
||||
id,
|
||||
case
|
||||
when member_died is not null then 'D'
|
||||
when member_active = true THEN 'A'
|
||||
ELSE 'I'
|
||||
END,
|
||||
COALESCE(member_died, updated_date, created_date, CURRENT_TIMESTAMP)
|
||||
FROM members;
|
||||
|
||||
-- Step 4: Create a view to get current member states (most recent log entry per member)
|
||||
CREATE OR REPLACE VIEW member_current_state AS
|
||||
SELECT DISTINCT ON (msl.member_id)
|
||||
msl.member_id,
|
||||
msl.state_code,
|
||||
ms.state_name,
|
||||
ms.description,
|
||||
msl.changed_at,
|
||||
msl.changed_by,
|
||||
msl.reason
|
||||
FROM member_state_log msl
|
||||
JOIN member_states ms ON msl.state_code = ms.state_code
|
||||
ORDER BY msl.member_id, msl.changed_at DESC, msl.log_id DESC;
|
||||
|
||||
-- Step 5: Function to change member state (ensures proper logging)
|
||||
CREATE OR REPLACE FUNCTION change_member_state(
|
||||
p_member_id INTEGER,
|
||||
p_new_state_code CHAR(1),
|
||||
p_changed_by INTEGER DEFAULT NULL,
|
||||
p_reason TEXT DEFAULT NULL
|
||||
) RETURNS VOID AS $$
|
||||
BEGIN
|
||||
-- Validate that the state exists
|
||||
IF NOT EXISTS (SELECT 1 FROM member_states WHERE state_code = p_new_state_code) THEN
|
||||
RAISE EXCEPTION 'Invalid state code: %', p_new_state_code;
|
||||
END IF;
|
||||
|
||||
-- Validate that the member exists
|
||||
IF NOT EXISTS (SELECT 1 FROM members WHERE member_id = p_member_id) THEN
|
||||
RAISE EXCEPTION 'Member not found: %', p_member_id;
|
||||
END IF;
|
||||
|
||||
-- Insert the new state log entry
|
||||
INSERT INTO member_state_log (member_id, state_code, changed_by, reason)
|
||||
VALUES (p_member_id, p_new_state_code, p_changed_by, p_reason);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Step 6: Common queries you'll need
|
||||
|
||||
-- Get all active members
|
||||
SELECT m.*, mcs.state_name, mcs.changed_at as last_state_change
|
||||
FROM members m
|
||||
JOIN member_current_state mcs ON m.member_id = mcs.member_id
|
||||
WHERE mcs.state_code = 'A';
|
||||
|
||||
-- Get member state history
|
||||
SELECT
|
||||
msl.changed_at,
|
||||
ms.state_name,
|
||||
msl.reason,
|
||||
msl.changed_by
|
||||
FROM member_state_log msl
|
||||
JOIN member_states ms ON msl.state_code = ms.state_code
|
||||
WHERE msl.member_id = 123 -- Replace with actual member_id
|
||||
ORDER BY msl.changed_at DESC;
|
||||
|
||||
-- Count members by state
|
||||
SELECT
|
||||
ms.state_name,
|
||||
COUNT(*) as member_count
|
||||
FROM member_current_state mcs
|
||||
JOIN member_states ms ON mcs.state_code = ms.state_code
|
||||
GROUP BY ms.state_code, ms.state_name
|
||||
ORDER BY ms.state_code;
|
||||
|
||||
-- Step 7: Example usage of the change_member_state function
|
||||
-- Change member 123 to inactive
|
||||
SELECT change_member_state(123, 'I', 1, 'Member requested temporary suspension');
|
||||
|
||||
-- Reactivate member 123
|
||||
SELECT change_member_state(123, 'A', 1, 'Member reactivation approved');
|
||||
|
||||
-- Step 8: Options for "calculated column" - current member state
|
||||
|
||||
-- OPTION 1: Add a cached state column with triggers (RECOMMENDED)
|
||||
ALTER TABLE members ADD COLUMN current_state_code CHAR(1);
|
||||
ALTER TABLE members ADD COLUMN current_state_changed_at TIMESTAMP;
|
||||
ALTER TABLE members ADD CONSTRAINT fk_members_current_state
|
||||
FOREIGN KEY (current_state_code) REFERENCES member_states(state_code);
|
||||
|
||||
-- Function to update member's current state cache
|
||||
CREATE OR REPLACE FUNCTION update_member_current_state_cache(p_member_id INTEGER)
|
||||
RETURNS VOID AS $
|
||||
DECLARE
|
||||
current_state RECORD;
|
||||
BEGIN
|
||||
-- Get the most recent state for this member
|
||||
SELECT state_code, changed_at
|
||||
INTO current_state
|
||||
FROM member_state_log
|
||||
WHERE member_id = p_member_id
|
||||
ORDER BY changed_at DESC, log_id DESC
|
||||
LIMIT 1;
|
||||
|
||||
-- Update the cached values in members table
|
||||
IF FOUND THEN
|
||||
UPDATE members
|
||||
SET current_state_code = current_state.state_code,
|
||||
current_state_changed_at = current_state.changed_at
|
||||
WHERE member_id = p_member_id;
|
||||
END IF;
|
||||
END;
|
||||
$ LANGUAGE plpgsql;
|
||||
|
||||
-- Trigger to automatically update cache when state changes
|
||||
CREATE OR REPLACE FUNCTION trigger_update_member_state_cache()
|
||||
RETURNS TRIGGER AS $
|
||||
BEGIN
|
||||
PERFORM update_member_current_state_cache(NEW.member_id);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER tr_member_state_log_update_cache
|
||||
AFTER INSERT ON member_state_log
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trigger_update_member_state_cache();
|
||||
|
||||
-- Initialize the cache for existing members
|
||||
UPDATE members
|
||||
SET (current_state_code, current_state_changed_at) = (
|
||||
SELECT mcs.state_code, mcs.changed_at
|
||||
FROM member_current_state mcs
|
||||
WHERE mcs.member_id = members.member_id
|
||||
);
|
||||
|
||||
-- Update the change_member_state function to work with cache
|
||||
CREATE OR REPLACE FUNCTION change_member_state(
|
||||
p_member_id INTEGER,
|
||||
p_new_state_code CHAR(1),
|
||||
p_changed_by INTEGER DEFAULT NULL,
|
||||
p_reason TEXT DEFAULT NULL
|
||||
) RETURNS VOID AS $
|
||||
BEGIN
|
||||
-- Validate that the state exists
|
||||
IF NOT EXISTS (SELECT 1 FROM member_states WHERE state_code = p_new_state_code) THEN
|
||||
RAISE EXCEPTION 'Invalid state code: %', p_new_state_code;
|
||||
END IF;
|
||||
|
||||
-- Validate that the member exists
|
||||
IF NOT EXISTS (SELECT 1 FROM members WHERE member_id = p_member_id) THEN
|
||||
RAISE EXCEPTION 'Member not found: %', p_member_id;
|
||||
END IF;
|
||||
|
||||
-- Insert the new state log entry (trigger will update cache automatically)
|
||||
INSERT INTO member_state_log (member_id, state_code, changed_by, reason)
|
||||
VALUES (p_member_id, p_new_state_code, p_changed_by, p_reason);
|
||||
END;
|
||||
$ LANGUAGE plpgsql;
|
||||
|
||||
-- OPTION 2: Generated column using a function (PostgreSQL 12+)
|
||||
-- Note: This approach has performance implications for large tables
|
||||
ALTER TABLE members ADD COLUMN member_state CHAR(2)
|
||||
GENERATED ALWAYS AS (
|
||||
(SELECT state_code
|
||||
FROM member_state_log msl
|
||||
WHERE msl.member_id = members.id
|
||||
ORDER BY changed_at DESC, log_id DESC
|
||||
LIMIT 1)
|
||||
) STORED;
|
||||
|
||||
-- OPTION 3: Materialized view (refresh manually or on schedule)
|
||||
/*
|
||||
CREATE MATERIALIZED VIEW members_with_current_state AS
|
||||
SELECT
|
||||
m.*,
|
||||
mcs.state_code as current_state_code,
|
||||
mcs.state_name as current_state_name,
|
||||
mcs.changed_at as current_state_changed_at
|
||||
FROM members m
|
||||
LEFT JOIN member_current_state mcs ON m.member_id = mcs.member_id;
|
||||
|
||||
CREATE UNIQUE INDEX idx_members_with_current_state_id
|
||||
ON members_with_current_state(member_id);
|
||||
|
||||
-- Refresh the materialized view (call this after state changes)
|
||||
-- REFRESH MATERIALIZED VIEW members_with_current_state;
|
||||
*/
|
||||
|
||||
-- Updated common queries using the cached column (OPTION 1)
|
||||
|
||||
-- Get all active members (much faster now!)
|
||||
SELECT m.*, ms.state_name
|
||||
FROM members m
|
||||
JOIN member_states ms ON m.current_state_code = ms.state_code
|
||||
WHERE m.current_state_code = 'A';
|
||||
|
||||
-- Get member with current state info
|
||||
SELECT
|
||||
m.*,
|
||||
ms.state_name as current_state_name,
|
||||
m.current_state_changed_at
|
||||
FROM members m
|
||||
LEFT JOIN member_states ms ON m.current_state_code = ms.state_code
|
||||
WHERE m.member_id = 123;
|
||||
|
||||
-- Count members by current state (very fast)
|
||||
SELECT
|
||||
ms.state_name,
|
||||
COUNT(*) as member_count
|
||||
FROM members m
|
||||
JOIN member_states ms ON m.current_state_code = ms.state_code
|
||||
GROUP BY ms.state_code, ms.state_name
|
||||
ORDER BY ms.state_code;
|
||||
|
||||
-- Step 9: After testing, remove the old column (CAREFUL!)
|
||||
-- ALTER TABLE members DROP COLUMN member_active;
|
||||
@@ -0,0 +1,255 @@
|
||||
-- Step 1: Create the member_states lookup table
|
||||
CREATE TABLE member_states (
|
||||
state_code CHAR(1) PRIMARY KEY,
|
||||
state_name VARCHAR(20) NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Insert the three states
|
||||
INSERT INTO member_states (state_code, state_name, description) VALUES
|
||||
('A', 'Active', 'Member is currently active'),
|
||||
('I', 'Inactive', 'Member is temporarily inactive'),
|
||||
('D', 'Deactivated', 'Member has been permanently deactivated');
|
||||
|
||||
-- Step 2: Create the member_state_log table
|
||||
CREATE TABLE member_state_log (
|
||||
log_id SERIAL PRIMARY KEY,
|
||||
member_id INTEGER NOT NULL,
|
||||
state_code CHAR(1) NOT NULL,
|
||||
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
changed_by INTEGER, -- Optional: user ID who made the change
|
||||
reason TEXT, -- Optional: reason for state change
|
||||
FOREIGN KEY (member_id) REFERENCES members(member_id),
|
||||
FOREIGN KEY (state_code) REFERENCES member_states(state_code)
|
||||
);
|
||||
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX idx_member_state_log_member_id ON member_state_log(member_id);
|
||||
CREATE INDEX idx_member_state_log_changed_at ON member_state_log(changed_at);
|
||||
CREATE INDEX idx_member_state_log_state_code ON member_state_log(state_code);
|
||||
|
||||
-- Step 3: Migration script to populate the log table from existing data
|
||||
INSERT INTO member_state_log (member_id, state_code, changed_at)
|
||||
SELECT
|
||||
member_id,
|
||||
CASE
|
||||
WHEN member_died IS NOT NULL THEN 'D'
|
||||
WHEN member_active = true THEN 'A'
|
||||
ELSE 'I'
|
||||
END,
|
||||
COALESCE(updated_at, created_at, CURRENT_TIMESTAMP)
|
||||
FROM members;
|
||||
|
||||
-- Step 4: Create a view to get current member states (most recent log entry per member)
|
||||
CREATE OR REPLACE VIEW member_current_state AS
|
||||
SELECT DISTINCT ON (msl.member_id)
|
||||
msl.member_id,
|
||||
msl.state_code,
|
||||
ms.state_name,
|
||||
ms.description,
|
||||
msl.changed_at,
|
||||
msl.changed_by,
|
||||
msl.reason
|
||||
FROM member_state_log msl
|
||||
JOIN member_states ms ON msl.state_code = ms.state_code
|
||||
ORDER BY msl.member_id, msl.changed_at DESC, msl.log_id DESC;
|
||||
|
||||
-- Step 5: Function to change member state (ensures proper logging)
|
||||
CREATE OR REPLACE FUNCTION change_member_state(
|
||||
p_member_id INTEGER,
|
||||
p_new_state_code CHAR(1),
|
||||
p_changed_by INTEGER DEFAULT NULL,
|
||||
p_reason TEXT DEFAULT NULL
|
||||
) RETURNS VOID AS $$
|
||||
BEGIN
|
||||
-- Validate that the state exists
|
||||
IF NOT EXISTS (SELECT 1 FROM member_states WHERE state_code = p_new_state_code) THEN
|
||||
RAISE EXCEPTION 'Invalid state code: %', p_new_state_code;
|
||||
END IF;
|
||||
|
||||
-- Validate that the member exists
|
||||
IF NOT EXISTS (SELECT 1 FROM members WHERE member_id = p_member_id) THEN
|
||||
RAISE EXCEPTION 'Member not found: %', p_member_id;
|
||||
END IF;
|
||||
|
||||
-- Insert the new state log entry
|
||||
INSERT INTO member_state_log (member_id, state_code, changed_by, reason)
|
||||
VALUES (p_member_id, p_new_state_code, p_changed_by, p_reason);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Step 6: Common queries you'll need
|
||||
|
||||
-- Get all active members
|
||||
SELECT m.*, mcs.state_name, mcs.changed_at as last_state_change
|
||||
FROM members m
|
||||
JOIN member_current_state mcs ON m.member_id = mcs.member_id
|
||||
WHERE mcs.state_code = 'A';
|
||||
|
||||
-- Get member state history
|
||||
SELECT
|
||||
msl.changed_at,
|
||||
ms.state_name,
|
||||
msl.reason,
|
||||
msl.changed_by
|
||||
FROM member_state_log msl
|
||||
JOIN member_states ms ON msl.state_code = ms.state_code
|
||||
WHERE msl.member_id = 123 -- Replace with actual member_id
|
||||
ORDER BY msl.changed_at DESC;
|
||||
|
||||
-- Count members by state
|
||||
SELECT
|
||||
ms.state_name,
|
||||
COUNT(*) as member_count
|
||||
FROM member_current_state mcs
|
||||
JOIN member_states ms ON mcs.state_code = ms.state_code
|
||||
GROUP BY ms.state_code, ms.state_name
|
||||
ORDER BY ms.state_code;
|
||||
|
||||
-- Step 7: Example usage of the change_member_state function
|
||||
-- Change member 123 to inactive
|
||||
SELECT change_member_state(123, 'I', 1, 'Member requested temporary suspension');
|
||||
|
||||
-- Reactivate member 123
|
||||
SELECT change_member_state(123, 'A', 1, 'Member reactivation approved');
|
||||
|
||||
-- Step 8: Options for "calculated column" - current member state
|
||||
|
||||
-- OPTION 1: Add a cached state column with triggers (RECOMMENDED)
|
||||
ALTER TABLE members ADD COLUMN current_state_code CHAR(1);
|
||||
ALTER TABLE members ADD COLUMN current_state_changed_at TIMESTAMP;
|
||||
ALTER TABLE members ADD CONSTRAINT fk_members_current_state
|
||||
FOREIGN KEY (current_state_code) REFERENCES member_states(state_code);
|
||||
|
||||
-- Function to update member's current state cache
|
||||
CREATE OR REPLACE FUNCTION update_member_current_state_cache(p_member_id INTEGER)
|
||||
RETURNS VOID AS $
|
||||
DECLARE
|
||||
current_state RECORD;
|
||||
BEGIN
|
||||
-- Get the most recent state for this member
|
||||
SELECT state_code, changed_at
|
||||
INTO current_state
|
||||
FROM member_state_log
|
||||
WHERE member_id = p_member_id
|
||||
ORDER BY changed_at DESC, log_id DESC
|
||||
LIMIT 1;
|
||||
|
||||
-- Update the cached values in members table
|
||||
IF FOUND THEN
|
||||
UPDATE members
|
||||
SET current_state_code = current_state.state_code,
|
||||
current_state_changed_at = current_state.changed_at
|
||||
WHERE member_id = p_member_id;
|
||||
END IF;
|
||||
END;
|
||||
$ LANGUAGE plpgsql;
|
||||
|
||||
-- Trigger to automatically update cache when state changes
|
||||
CREATE OR REPLACE FUNCTION trigger_update_member_state_cache()
|
||||
RETURNS TRIGGER AS $
|
||||
BEGIN
|
||||
PERFORM update_member_current_state_cache(NEW.member_id);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER tr_member_state_log_update_cache
|
||||
AFTER INSERT ON member_state_log
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trigger_update_member_state_cache();
|
||||
|
||||
-- Initialize the cache for existing members
|
||||
UPDATE members
|
||||
SET (current_state_code, current_state_changed_at) = (
|
||||
SELECT mcs.state_code, mcs.changed_at
|
||||
FROM member_current_state mcs
|
||||
WHERE mcs.member_id = members.member_id
|
||||
);
|
||||
|
||||
-- Update the change_member_state function to work with cache
|
||||
CREATE OR REPLACE FUNCTION change_member_state(
|
||||
p_member_id INTEGER,
|
||||
p_new_state_code CHAR(1),
|
||||
p_changed_by INTEGER DEFAULT NULL,
|
||||
p_reason TEXT DEFAULT NULL
|
||||
) RETURNS VOID AS $
|
||||
BEGIN
|
||||
-- Validate that the state exists
|
||||
IF NOT EXISTS (SELECT 1 FROM member_states WHERE state_code = p_new_state_code) THEN
|
||||
RAISE EXCEPTION 'Invalid state code: %', p_new_state_code;
|
||||
END IF;
|
||||
|
||||
-- Validate that the member exists
|
||||
IF NOT EXISTS (SELECT 1 FROM members WHERE member_id = p_member_id) THEN
|
||||
RAISE EXCEPTION 'Member not found: %', p_member_id;
|
||||
END IF;
|
||||
|
||||
-- Insert the new state log entry (trigger will update cache automatically)
|
||||
INSERT INTO member_state_log (member_id, state_code, changed_by, reason)
|
||||
VALUES (p_member_id, p_new_state_code, p_changed_by, p_reason);
|
||||
END;
|
||||
$ LANGUAGE plpgsql;
|
||||
|
||||
-- OPTION 2: Generated column using a function (NOT POSSIBLE)
|
||||
-- PostgreSQL doesn't allow subqueries in generated column expressions
|
||||
-- This approach won't work:
|
||||
/*
|
||||
ALTER TABLE members ADD COLUMN current_state_code_generated CHAR(1)
|
||||
GENERATED ALWAYS AS (
|
||||
(SELECT state_code
|
||||
FROM member_state_log msl
|
||||
WHERE msl.member_id = members.member_id
|
||||
ORDER BY changed_at DESC, log_id DESC
|
||||
LIMIT 1)
|
||||
) STORED;
|
||||
-- ERROR: cannot use subquery in column generation expression
|
||||
*/
|
||||
|
||||
-- OPTION 3: Materialized view (refresh manually or on schedule)
|
||||
/*
|
||||
CREATE MATERIALIZED VIEW members_with_current_state AS
|
||||
SELECT
|
||||
m.*,
|
||||
mcs.state_code as current_state_code,
|
||||
mcs.state_name as current_state_name,
|
||||
mcs.changed_at as current_state_changed_at
|
||||
FROM members m
|
||||
LEFT JOIN member_current_state mcs ON m.member_id = mcs.member_id;
|
||||
|
||||
CREATE UNIQUE INDEX idx_members_with_current_state_id
|
||||
ON members_with_current_state(member_id);
|
||||
|
||||
-- Refresh the materialized view (call this after state changes)
|
||||
-- REFRESH MATERIALIZED VIEW members_with_current_state;
|
||||
*/
|
||||
|
||||
-- Updated common queries using the cached column (OPTION 1)
|
||||
|
||||
-- Get all active members (much faster now!)
|
||||
SELECT m.*, ms.state_name
|
||||
FROM members m
|
||||
JOIN member_states ms ON m.current_state_code = ms.state_code
|
||||
WHERE m.current_state_code = 'A';
|
||||
|
||||
-- Get member with current state info
|
||||
SELECT
|
||||
m.*,
|
||||
ms.state_name as current_state_name,
|
||||
m.current_state_changed_at
|
||||
FROM members m
|
||||
LEFT JOIN member_states ms ON m.current_state_code = ms.state_code
|
||||
WHERE m.member_id = 123;
|
||||
|
||||
-- Count members by current state (very fast)
|
||||
SELECT
|
||||
ms.state_name,
|
||||
COUNT(*) as member_count
|
||||
FROM members m
|
||||
JOIN member_states ms ON m.current_state_code = ms.state_code
|
||||
GROUP BY ms.state_code, ms.state_name
|
||||
ORDER BY ms.state_code;
|
||||
|
||||
-- Step 9: After testing, remove the old column (CAREFUL!)
|
||||
-- ALTER TABLE members DROP COLUMN member_active;
|
||||
@@ -0,0 +1,197 @@
|
||||
from sqlalchemy import Column, String, Integer, DateTime, Text, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from datetime import datetime
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
# OPTION 1: Correct relationship setup
|
||||
class MemberStates(Base):
|
||||
__tablename__ = 'member_states'
|
||||
|
||||
state_code = Column(String(1), primary_key=True)
|
||||
state_name = Column(String(20), nullable=False)
|
||||
description = Column(Text)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# Backref will create 'members' attribute on MemberStates
|
||||
# members = relationship('Members', back_populates='current_state')
|
||||
|
||||
class Members(Base):
|
||||
__tablename__ = 'members'
|
||||
|
||||
id = Column(Integer, primary_key=True) # Assuming 'id' is your primary key
|
||||
# ... other member fields ...
|
||||
|
||||
# Add foreign key constraint
|
||||
current_state_code = Column(String(1), ForeignKey('member_states.state_code'), index=True)
|
||||
current_state_changed_at = Column(DateTime)
|
||||
|
||||
# Correct relationship definition
|
||||
current_state = relationship('MemberStates', back_populates='members')
|
||||
|
||||
# Update MemberStates to complete the bidirectional relationship
|
||||
MemberStates.members = relationship('Members', back_populates='current_state')
|
||||
|
||||
class MemberStateLog(Base):
|
||||
__tablename__ = 'member_state_log'
|
||||
|
||||
log_id = Column(Integer, primary_key=True)
|
||||
member_id = Column(Integer, ForeignKey('members.id'), nullable=False)
|
||||
state_code = Column(String(1), ForeignKey('member_states.state_code'), nullable=False)
|
||||
changed_at = Column(DateTime, default=datetime.utcnow)
|
||||
changed_by = Column(Integer) # Optional: user ID who made the change
|
||||
reason = Column(Text)
|
||||
|
||||
# Relationships
|
||||
member = relationship('Members', backref='state_history')
|
||||
state = relationship('MemberStates')
|
||||
|
||||
# OPTION 2: Alternative approach with primaryjoin (if you can't add foreign key)
|
||||
class MembersAlternative(Base):
|
||||
__tablename__ = 'members'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
current_state_code = Column(String(1), index=True) # No foreign key
|
||||
current_state_changed_at = Column(DateTime)
|
||||
|
||||
# Use primaryjoin to specify the join condition
|
||||
current_state = relationship(
|
||||
'MemberStates',
|
||||
primaryjoin='Members.current_state_code == MemberStates.state_code',
|
||||
foreign_keys=[current_state_code]
|
||||
)
|
||||
|
||||
# USAGE EXAMPLES:
|
||||
|
||||
# Example 1: Query members with their current state
|
||||
def get_members_with_state():
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
Session = sessionmaker()
|
||||
session = Session()
|
||||
|
||||
members = session.query(Members).join(Members.current_state).all()
|
||||
|
||||
for member in members:
|
||||
print(f"Member: {member.name}")
|
||||
print(f"State: {member.current_state.state_name}")
|
||||
print(f"Description: {member.current_state.description}")
|
||||
|
||||
session.close()
|
||||
|
||||
# Example 2: Get member state name directly
|
||||
def get_member_state_name(member):
|
||||
if member.current_state:
|
||||
return member.current_state.state_name
|
||||
return "Unknown"
|
||||
|
||||
# Example 3: Filter members by state
|
||||
def get_active_members():
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
Session = sessionmaker()
|
||||
session = Session()
|
||||
|
||||
active_members = session.query(Members).filter(
|
||||
Members.current_state_code == 'A'
|
||||
).all()
|
||||
|
||||
session.close()
|
||||
return active_members
|
||||
|
||||
# Example 4: Count members by state
|
||||
def count_members_by_state():
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import func
|
||||
Session = sessionmaker()
|
||||
session = Session()
|
||||
|
||||
counts = session.query(
|
||||
MemberStates.state_name,
|
||||
func.count(Members.id).label('count')
|
||||
).outerjoin(Members).group_by(
|
||||
MemberStates.state_code,
|
||||
MemberStates.state_name
|
||||
).all()
|
||||
|
||||
session.close()
|
||||
return counts
|
||||
|
||||
# Example 5: Update member state (with logging)
|
||||
def change_member_state(session, member_id, new_state_code, changed_by=None, reason=None):
|
||||
# Get the member
|
||||
member = session.query(Members).get(member_id)
|
||||
if not member:
|
||||
raise ValueError(f"Member {member_id} not found")
|
||||
|
||||
# Validate state exists
|
||||
state = session.query(MemberStates).get(new_state_code)
|
||||
if not state:
|
||||
raise ValueError(f"Invalid state code: {new_state_code}")
|
||||
|
||||
# Log the state change
|
||||
log_entry = MemberStateLog(
|
||||
member_id=member_id,
|
||||
state_code=new_state_code,
|
||||
changed_by=changed_by,
|
||||
reason=reason
|
||||
)
|
||||
session.add(log_entry)
|
||||
|
||||
# Update member's current state
|
||||
member.current_state_code = new_state_code
|
||||
member.current_state_changed_at = datetime.utcnow()
|
||||
|
||||
session.commit()
|
||||
|
||||
# Example 6: Serialization for API responses
|
||||
def serialize_member_with_state(member):
|
||||
return {
|
||||
'id': member.id,
|
||||
'name': member.name, # Assuming you have a name field
|
||||
'current_state_code': member.current_state_code,
|
||||
'current_state_name': member.current_state.state_name if member.current_state else None,
|
||||
'current_state_description': member.current_state.description if member.current_state else None,
|
||||
'current_state_changed_at': member.current_state_changed_at.isoformat() if member.current_state_changed_at else None
|
||||
}
|
||||
|
||||
# Example 7: Query with eager loading to avoid N+1 queries
|
||||
def get_all_members_with_states_optimized():
|
||||
from sqlalchemy.orm import sessionmaker, joinedload
|
||||
Session = sessionmaker()
|
||||
session = Session()
|
||||
|
||||
members = session.query(Members).options(
|
||||
joinedload(Members.current_state)
|
||||
).all()
|
||||
|
||||
# Now you can access member.current_state.state_name without additional queries
|
||||
result = [serialize_member_with_state(member) for member in members]
|
||||
|
||||
session.close()
|
||||
return result
|
||||
|
||||
# Example 8: Custom property for easier access
|
||||
class MembersWithProperty(Base):
|
||||
__tablename__ = 'members'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
current_state_code = Column(String(1), ForeignKey('member_states.state_code'), index=True)
|
||||
current_state_changed_at = Column(DateTime)
|
||||
|
||||
current_state = relationship('MemberStates')
|
||||
|
||||
@property
|
||||
def state_name(self):
|
||||
return self.current_state.state_name if self.current_state else "Unknown"
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
return self.current_state_code == 'A'
|
||||
|
||||
@property
|
||||
def is_inactive(self):
|
||||
return self.current_state_code == 'I'
|
||||
|
||||
@property
|
||||
def is_deactivated(self):
|
||||
return self.current_state_code == 'D'
|
||||
Reference in New Issue
Block a user