diff --git a/sql_event_sourcing/README.md b/sql_event_sourcing/README.md new file mode 100644 index 0000000..ffdb9c7 --- /dev/null +++ b/sql_event_sourcing/README.md @@ -0,0 +1,88 @@ +# SQL Event Sourcing Engine + +This module demonstrates how a SQL database is fundamentally an event sourcing system. + +## Core Insight + +Every SQL operation is an event: +- `INSERT` → `row_inserted(Table, RowId, Values)` +- `UPDATE` → `row_updated(Table, RowId, OldValues, NewValues)` +- `DELETE` → `row_deleted(Table, RowId, OldValues)` +- `CREATE TABLE` → `table_created(Name, Schema)` + +The current state is always derived by replaying all events. Traditional databases optimize this with snapshots and indexes, but the fundamental model is event sourcing. + +## Usage + +```prolog +?- use_module(sql_event_sourcing/sql_es). + +% Create a table (emits table_created event) +?- create_table(users, [col(id, integer), col(name, varchar)]). + +% Insert rows (emits row_inserted events) +?- sql_insert(users, [id-1, name-'Alice']). +?- sql_insert(users, [id-2, name-'Bob']). + +% Query using CQL-style syntax +?- users :: [id-Id, name-Name]. +Id = 1, Name = 'Alice' ; +Id = 2, Name = 'Bob'. + +% Update (emits row_updated event with old/new values) +?- sql_update(users, [name-'Robert'], [id-2]). + +% Delete (emits row_deleted event preserving old values) +?- sql_delete(users, [id-1]). + +% View complete event log +?- print_events. + +% View current state (derived from events) +?- print_state. +``` + +## CQL-Style Term Format + +Following [SWI-Prolog's CQL](https://www.swi-prolog.org/pldoc/man?section=cql): + +| Operation | Syntax | +|-----------|--------| +| SELECT | `table :: [col-Var, ...]` | +| SELECT with WHERE | `sql_select(table, [col-Var], [where_col-val])` | +| INSERT | `sql_insert(table, [col-val, ...])` | +| UPDATE | `sql_update(table, [col-newval], [where_col-val])` | +| DELETE | `sql_delete(table, [pk_col-val])` | + +## Transaction Support + +```prolog +?- begin_transaction. +?- sql_insert(orders, [id-1, total-100]). +?- sql_update(inventory, [qty-99], [id-5]). +?- commit_transaction. % or rollback_transaction +``` + +## Event Sourcing Benefits + +1. **Complete Audit Trail**: Every change is recorded with timestamps +2. **Time Travel**: Reconstruct state at any point by replaying events up to that time +3. **Debugging**: See exactly what happened and when +4. **Undo/Redo**: Events contain enough info to reverse operations + +## Running the Demo + +```bash +swipl -s sql_event_sourcing/sql_es_demo.pl -g run_demo +``` + +## Running Tests + +```bash +swipl -s sql_event_sourcing/sql_es_demo.pl -g run_tests_sql -g halt +``` + +## Files + +- `sql_es.pl` - Core event sourcing SQL engine +- `sql_es_demo.pl` - Demonstrations and unit tests diff --git a/sql_event_sourcing/sql_es.pl b/sql_event_sourcing/sql_es.pl new file mode 100644 index 0000000..206dd38 --- /dev/null +++ b/sql_event_sourcing/sql_es.pl @@ -0,0 +1,418 @@ +/* + SQL Event Sourcing Engine + + Demonstrates how a SQL database is fundamentally an event sourcing system. + Every SQL operation (INSERT, UPDATE, DELETE) is an event that modifies state. + The current state is the projection of all events applied in sequence. + + Term Format (CQL-style): + - SELECT: table :: [col-Var, ...] + - INSERT: insert(table, [col-val, ...]) + - UPDATE: update(table, [col-val, ...]), @ :: [where_conditions] + - DELETE: delete(table, [pk-val]) + + Author: Claude Code + Based on event sourcing patterns in ALGT codebase +*/ + +:- module(sql_es, [ + % Event management + emit_event/1, + get_events/1, + clear_events/0, + replay_events/1, + + % DDL operations (emit events) + create_table/2, + drop_table/1, + + % DML operations (emit events) + sql_insert/2, + sql_update/3, + sql_delete/2, + + % Query operations (project current state) + sql_select/2, + sql_select/3, + + % Transaction support + begin_transaction/0, + commit_transaction/0, + rollback_transaction/0, + + % State inspection + current_state/1, + table_rows/2, + table_schema/2, + + % Utilities + print_events/0, + print_state/0 +]). + +:- use_module(library(lists)). +:- use_module(library(apply)). + +%% --------------------------------------------------------------------------- +%% Event Store +%% +%% Events are stored as dynamic facts. Each event has a sequence number +%% and timestamp for ordering and auditing. +%% --------------------------------------------------------------------------- + +:- dynamic event/3. % event(SeqNum, Timestamp, EventTerm) +:- dynamic seq_counter/1. % Sequence number counter +:- dynamic transaction/1. % transaction(pending_events) +:- dynamic in_transaction/0. % Flag for active transaction + +seq_counter(0). + +%% next_seq(-SeqNum) is det. +% Get next sequence number +next_seq(SeqNum) :- + retract(seq_counter(Current)), + SeqNum is Current + 1, + assertz(seq_counter(SeqNum)). + +%% emit_event(+EventTerm) is det. +% Record an event to the event store (or pending transaction) +emit_event(EventTerm) :- + get_time(Timestamp), + next_seq(SeqNum), + Event = event(SeqNum, Timestamp, EventTerm), + ( in_transaction + -> % Add to pending transaction events + retract(transaction(Pending)), + assertz(transaction([Event|Pending])) + ; % Commit directly + assertz(Event) + ). + +%% get_events(-Events) is det. +% Retrieve all events in sequence order +get_events(Events) :- + findall(event(Seq, TS, Term), event(Seq, TS, Term), Unsorted), + sort(1, @<, Unsorted, Events). + +%% clear_events is det. +% Clear all events (reset database) +clear_events :- + retractall(event(_, _, _)), + retractall(seq_counter(_)), + retractall(transaction(_)), + retractall(in_transaction), + assertz(seq_counter(0)). + +%% replay_events(+Events) is det. +% Replay a list of events (for restoring state) +replay_events([]). +replay_events([event(_, _, Term)|Rest]) :- + assertz_event_direct(Term), + replay_events(Rest). + +assertz_event_direct(Term) :- + get_time(Timestamp), + next_seq(SeqNum), + assertz(event(SeqNum, Timestamp, Term)). + +%% --------------------------------------------------------------------------- +%% Event Types +%% +%% DDL Events: +%% - table_created(TableName, Schema) +%% - table_dropped(TableName) +%% +%% DML Events: +%% - row_inserted(TableName, RowId, ColumnValues) +%% - row_updated(TableName, RowId, OldValues, NewValues) +%% - row_deleted(TableName, RowId, OldValues) +%% +%% Transaction Events: +%% - transaction_started(TxId) +%% - transaction_committed(TxId) +%% - transaction_rolled_back(TxId) +%% --------------------------------------------------------------------------- + +%% --------------------------------------------------------------------------- +%% DDL Operations +%% --------------------------------------------------------------------------- + +%% create_table(+TableName, +Schema) is det. +% Schema is a list of column definitions: [col(name, type), ...] +% Example: create_table(users, [col(id, integer), col(name, varchar), col(email, varchar)]) +create_table(TableName, Schema) :- + emit_event(table_created(TableName, Schema)). + +%% drop_table(+TableName) is det. +drop_table(TableName) :- + emit_event(table_dropped(TableName)). + +%% --------------------------------------------------------------------------- +%% DML Operations (CQL-style syntax) +%% --------------------------------------------------------------------------- + +%% sql_insert(+TableName, +ColumnValues) is det. +% Insert a row. ColumnValues is [col-val, ...] (CQL style) +% Example: sql_insert(users, [id-1, name-'Alice', email-'alice@example.com']) +sql_insert(TableName, ColumnValues) :- + generate_row_id(RowId), + emit_event(row_inserted(TableName, RowId, ColumnValues)). + +%% sql_update(+TableName, +NewValues, +WhereClause) is det. +% Update rows matching WhereClause +% Example: sql_update(users, [name-'Bob'], [id-1]) +sql_update(TableName, NewValues, WhereClause) :- + % Find all matching rows + current_table_rows(TableName, AllRows), + include(row_matches(WhereClause), AllRows, MatchingRows), + % Emit update events for each matching row + forall( + member(row(RowId, OldValues), MatchingRows), + ( merge_values(OldValues, NewValues, MergedValues), + emit_event(row_updated(TableName, RowId, OldValues, MergedValues)) + ) + ). + +%% sql_delete(+TableName, +WhereClause) is det. +% Delete rows matching WhereClause +% Example: sql_delete(users, [id-1]) +sql_delete(TableName, WhereClause) :- + current_table_rows(TableName, AllRows), + include(row_matches(WhereClause), AllRows, MatchingRows), + forall( + member(row(RowId, OldValues), MatchingRows), + emit_event(row_deleted(TableName, RowId, OldValues)) + ). + +%% --------------------------------------------------------------------------- +%% Query Operations (CQL-style syntax) +%% --------------------------------------------------------------------------- + +%% sql_select(+TableName, ?ColumnBindings) is nondet. +% Select rows from table. ColumnBindings is [col-Var, ...] +% Unifies variables with values from matching rows +% Example: sql_select(users, [id-Id, name-Name]) +sql_select(TableName, ColumnBindings) :- + sql_select(TableName, ColumnBindings, []). + +%% sql_select(+TableName, ?ColumnBindings, +WhereClause) is nondet. +% Select with WHERE clause +% Example: sql_select(users, [name-Name], [id-1]) +sql_select(TableName, ColumnBindings, WhereClause) :- + current_table_rows(TableName, AllRows), + member(row(_RowId, Values), AllRows), + row_matches(WhereClause, row(_RowId, Values)), + bind_columns(ColumnBindings, Values). + +%% Operator syntax for SELECT (CQL-style) +% users :: [id-Id, name-Name] equivalent to sql_select(users, [id-Id, name-Name]) +:- op(700, xfx, ::). + +TableName :: ColumnBindings :- + sql_select(TableName, ColumnBindings). + +%% --------------------------------------------------------------------------- +%% Transaction Support +%% --------------------------------------------------------------------------- + +%% begin_transaction is det. +begin_transaction :- + ( in_transaction + -> throw(error(nested_transaction, 'Nested transactions not supported')) + ; assertz(in_transaction), + assertz(transaction([])) + ). + +%% commit_transaction is det. +commit_transaction :- + ( in_transaction + -> retract(transaction(PendingReversed)), + reverse(PendingReversed, Pending), + retract(in_transaction), + forall(member(E, Pending), assertz(E)) + ; throw(error(no_transaction, 'No active transaction')) + ). + +%% rollback_transaction is det. +rollback_transaction :- + ( in_transaction + -> retract(transaction(_)), + retract(in_transaction) + ; throw(error(no_transaction, 'No active transaction')) + ). + +%% --------------------------------------------------------------------------- +%% State Projection +%% +%% The "current state" is computed by replaying all events. +%% This is the essence of event sourcing: state is derived, not stored. +%% --------------------------------------------------------------------------- + +%% current_state(-State) is det. +% Compute current database state from events +% State = state(Tables, Rows) where: +% Tables = [table(name, schema), ...] +% Rows = [table_rows(name, [row(id, values), ...]), ...] +current_state(state(Tables, RowsByTable)) :- + get_events(Events), + foldl(apply_event, Events, state([], []), state(Tables, RowsByTable)). + +%% apply_event(+Event, +StateIn, -StateOut) is det. +% Apply a single event to state +apply_event(event(_, _, EventTerm), StateIn, StateOut) :- + apply_event_term(EventTerm, StateIn, StateOut). + +% DDL events +apply_event_term(table_created(Name, Schema), state(Tables, Rows), state(NewTables, NewRows)) :- + NewTables = [table(Name, Schema)|Tables], + NewRows = [table_rows(Name, [])|Rows]. + +apply_event_term(table_dropped(Name), state(Tables, Rows), state(NewTables, NewRows)) :- + exclude(is_table(Name), Tables, NewTables), + exclude(is_table_rows(Name), Rows, NewRows). + +% DML events +apply_event_term(row_inserted(Table, RowId, Values), state(Tables, Rows), state(Tables, NewRows)) :- + update_table_rows(Table, Rows, + add_row(row(RowId, Values)), + NewRows). + +apply_event_term(row_updated(Table, RowId, _OldValues, NewValues), state(Tables, Rows), state(Tables, NewRows)) :- + update_table_rows(Table, Rows, + replace_row(RowId, NewValues), + NewRows). + +apply_event_term(row_deleted(Table, RowId, _OldValues), state(Tables, Rows), state(Tables, NewRows)) :- + update_table_rows(Table, Rows, + remove_row(RowId), + NewRows). + +% Transaction events (no-op for state projection - transactions are already applied or rolled back) +apply_event_term(transaction_started(_), State, State). +apply_event_term(transaction_committed(_), State, State). +apply_event_term(transaction_rolled_back(_), State, State). + +%% Helper predicates for state projection +is_table(Name, table(Name, _)). +is_table_rows(Name, table_rows(Name, _)). + +update_table_rows(Table, Rows, Operation, NewRows) :- + select(table_rows(Table, TableRows), Rows, RestRows), + call(Operation, TableRows, UpdatedTableRows), + NewRows = [table_rows(Table, UpdatedTableRows)|RestRows]. + +add_row(Row, Rows, [Row|Rows]). + +replace_row(RowId, NewValues, Rows, NewRows) :- + select(row(RowId, _), Rows, RestRows), + NewRows = [row(RowId, NewValues)|RestRows]. + +remove_row(RowId, Rows, NewRows) :- + select(row(RowId, _), Rows, NewRows). + +%% --------------------------------------------------------------------------- +%% Convenience Predicates for State Inspection +%% --------------------------------------------------------------------------- + +%% table_rows(+TableName, -Rows) is det. +% Get current rows for a table +table_rows(TableName, Rows) :- + current_state(state(_, RowsByTable)), + member(table_rows(TableName, Rows), RowsByTable). + +%% current_table_rows(+TableName, -Rows) is det. +% Internal: get rows for update/delete operations +current_table_rows(TableName, Rows) :- + ( table_rows(TableName, Rows) + -> true + ; Rows = [] + ). + +%% table_schema(+TableName, -Schema) is det. +% Get schema for a table +table_schema(TableName, Schema) :- + current_state(state(Tables, _)), + member(table(TableName, Schema), Tables). + +%% --------------------------------------------------------------------------- +%% Helper Predicates +%% --------------------------------------------------------------------------- + +%% generate_row_id(-RowId) is det. +% Generate a unique row identifier +:- dynamic row_id_counter/1. +row_id_counter(0). + +generate_row_id(RowId) :- + retract(row_id_counter(Current)), + RowId is Current + 1, + assertz(row_id_counter(RowId)). + +%% row_matches(+Conditions, +Row) is semidet. +% Check if a row matches WHERE conditions +row_matches([], _). +row_matches([Col-Val|Rest], row(_RowId, Values)) :- + member(Col-Val, Values), + row_matches(Rest, row(_RowId, Values)). + +%% bind_columns(+ColumnBindings, +Values) is semidet. +% Bind column variables to row values +bind_columns([], _). +bind_columns([Col-Var|Rest], Values) :- + member(Col-Var, Values), + bind_columns(Rest, Values). + +%% merge_values(+OldValues, +NewValues, -MergedValues) is det. +% Merge new values into old (for updates) +merge_values(OldValues, [], OldValues). +merge_values(OldValues, [Col-NewVal|RestNew], MergedValues) :- + ( select(Col-_, OldValues, TempOld) + -> true + ; TempOld = OldValues + ), + merge_values([Col-NewVal|TempOld], RestNew, MergedValues). + +%% --------------------------------------------------------------------------- +%% Debug/Display Utilities +%% --------------------------------------------------------------------------- + +%% print_events is det. +% Print all events in order +print_events :- + format('~n=== Event Log ===~n', []), + get_events(Events), + ( Events = [] + -> format(' (no events)~n', []) + ; forall(member(event(Seq, TS, Term), Events), + format(' [~w] ~w: ~w~n', [Seq, TS, Term])) + ), + format('=================~n', []). + +%% print_state is det. +% Print current database state +print_state :- + format('~n=== Current State ===~n', []), + current_state(state(Tables, RowsByTable)), + ( Tables = [] + -> format(' (no tables)~n', []) + ; forall(member(table(Name, Schema), Tables), ( + format('~nTable: ~w~n', [Name]), + format(' Schema: ~w~n', [Schema]), + ( member(table_rows(Name, Rows), RowsByTable) + -> format(' Rows:~n', []), + ( Rows = [] + -> format(' (empty)~n', []) + ; forall(member(row(Id, Values), Rows), + format(' [~w] ~w~n', [Id, Values])) + ) + ; format(' Rows: (none)~n', []) + ) + )) + ), + format('=====================~n', []). + +%% --------------------------------------------------------------------------- +%% Module Initialization +%% --------------------------------------------------------------------------- + +:- initialization(clear_events). diff --git a/sql_event_sourcing/sql_es_demo.pl b/sql_event_sourcing/sql_es_demo.pl new file mode 100644 index 0000000..75a330e --- /dev/null +++ b/sql_event_sourcing/sql_es_demo.pl @@ -0,0 +1,349 @@ +/* + SQL Event Sourcing Demonstration + + This file demonstrates how a SQL database is fundamentally an event + sourcing system. Each operation produces an event, and the current + state is always derivable by replaying all events. + + Run with: swipl -s sql_es_demo.pl -g run_demo +*/ + +:- use_module(sql_es). + +%% --------------------------------------------------------------------------- +%% Demo 1: Basic CRUD Operations as Events +%% --------------------------------------------------------------------------- + +demo_basic_crud :- + format('~n=== Demo 1: Basic CRUD Operations as Events ===~n', []), + + % Clear any previous state + clear_events, + + % CREATE TABLE - This emits a table_created event + format('~n1. Creating users table...~n', []), + create_table(users, [col(id, integer), col(name, varchar), col(email, varchar)]), + + % INSERT - Each insert emits a row_inserted event + format('2. Inserting rows...~n', []), + sql_insert(users, [id-1, name-'Alice', email-'alice@example.com']), + sql_insert(users, [id-2, name-'Bob', email-'bob@example.com']), + sql_insert(users, [id-3, name-'Charlie', email-'charlie@example.com']), + + % Show the event log + format('~nEvent log after inserts:~n', []), + print_events, + + % Show derived state + format('~nDerived state (by replaying events):~n', []), + print_state, + + % UPDATE - Emits row_updated event with old and new values + format('~n3. Updating Bob\\'s email...~n', []), + sql_update(users, [email-'robert@example.com'], [id-2]), + + % DELETE - Emits row_deleted event with old values (for undo) + format('4. Deleting Charlie...~n', []), + sql_delete(users, [id-3]), + + % Final event log + format('~nFinal event log:~n', []), + print_events, + + % Final state + format('~nFinal state:~n', []), + print_state. + +%% --------------------------------------------------------------------------- +%% Demo 2: CQL-Style Query Syntax +%% --------------------------------------------------------------------------- + +demo_query_syntax :- + format('~n=== Demo 2: CQL-Style Query Syntax ===~n', []), + + clear_events, + + % Setup data + create_table(products, [col(id, integer), col(name, varchar), col(price, decimal)]), + sql_insert(products, [id-1, name-'Widget', price-9.99]), + sql_insert(products, [id-2, name-'Gadget', price-19.99]), + sql_insert(products, [id-3, name-'Gizmo', price-29.99]), + + % SELECT using :: operator (CQL style) + format('~nQuerying with CQL-style syntax:~n', []), + format(' products :: [id-Id, name-Name, price-Price]~n', []), + format('~nResults:~n', []), + forall( + products :: [id-Id, name-Name, price-Price], + format(' id=~w, name=~w, price=~w~n', [Id, Name, Price]) + ), + + % SELECT with WHERE clause + format('~nQuerying with WHERE clause:~n', []), + format(' sql_select(products, [name-Name], [id-2])~n', []), + ( sql_select(products, [name-Name], [id-2]) + -> format(' Result: name=~w~n', [Name]) + ; format(' No results~n', []) + ). + +%% --------------------------------------------------------------------------- +%% Demo 3: Event Sourcing Benefits - Time Travel +%% --------------------------------------------------------------------------- + +demo_time_travel :- + format('~n=== Demo 3: Event Sourcing Benefits - Time Travel ===~n', []), + + clear_events, + + create_table(accounts, [col(id, integer), col(owner, varchar), col(balance, decimal)]), + sql_insert(accounts, [id-1, owner-'Alice', balance-1000]), + + format('~nInitial balance: $1000~n', []), + + % Simulate transactions + sql_update(accounts, [balance-800], [id-1]), + format('After withdrawal: $800~n', []), + + sql_update(accounts, [balance-1050], [id-1]), + format('After deposit: $1050~n', []), + + sql_update(accounts, [balance-950], [id-1]), + format('After purchase: $950~n', []), + + % Show complete history + format('~n--- Complete Event History ---~n', []), + get_events(Events), + forall(member(event(Seq, _, Term), Events), ( + format('[~w] ', [Seq]), + describe_event(Term) + )), + + % Show we can reconstruct state at any point + format('~n--- State Reconstruction ---~n', []), + format('The current state ($950) can always be derived by replaying~n', []), + format('all events from the beginning. We never lose history!~n', []), + print_state. + +describe_event(table_created(Name, _)) :- + format('Table ~w created~n', [Name]). +describe_event(row_inserted(Table, Id, Values)) :- + member(balance-Bal, Values), + format('~w[~w]: Initial balance = $~w~n', [Table, Id, Bal]). +describe_event(row_updated(Table, Id, Old, New)) :- + member(balance-OldBal, Old), + member(balance-NewBal, New), + Diff is NewBal - OldBal, + ( Diff >= 0 + -> format('~w[~w]: Balance +$~w ($~w -> $~w)~n', [Table, Id, Diff, OldBal, NewBal]) + ; AbsDiff is abs(Diff), + format('~w[~w]: Balance -$~w ($~w -> $~w)~n', [Table, Id, AbsDiff, OldBal, NewBal]) + ). +describe_event(Term) :- + format('Event: ~w~n', [Term]). + +%% --------------------------------------------------------------------------- +%% Demo 4: Transaction Support with Rollback +%% --------------------------------------------------------------------------- + +demo_transactions :- + format('~n=== Demo 4: Transaction Support with Rollback ===~n', []), + + clear_events, + + create_table(orders, [col(id, integer), col(item, varchar), col(qty, integer)]), + sql_insert(orders, [id-1, item-'Book', qty-2]), + + format('~nInitial state:~n', []), + print_state, + + % Start transaction + format('~nStarting transaction...~n', []), + begin_transaction, + + % Make changes within transaction + sql_insert(orders, [id-2, item-'Pen', qty-10]), + sql_update(orders, [qty-5], [id-1]), + + format('~nState during transaction (not yet committed):~n', []), + format('(Changes are pending - not visible in main event store)~n', []), + + % Rollback! + format('~nRolling back transaction...~n', []), + rollback_transaction, + + format('~nState after rollback (unchanged):~n', []), + print_state, + + % Now do a successful transaction + format('~nStarting new transaction...~n', []), + begin_transaction, + sql_insert(orders, [id-3, item-'Notebook', qty-3]), + format('Committing transaction...~n', []), + commit_transaction, + + format('~nState after commit:~n', []), + print_state. + +%% --------------------------------------------------------------------------- +%% Demo 5: Multiple Tables with Relationships +%% --------------------------------------------------------------------------- + +demo_relationships :- + format('~n=== Demo 5: Multiple Tables (Simulated Join) ===~n', []), + + clear_events, + + % Create tables + create_table(customers, [col(id, integer), col(name, varchar)]), + create_table(orders, [col(id, integer), col(customer_id, integer), col(product, varchar)]), + + % Insert data + sql_insert(customers, [id-1, name-'Alice']), + sql_insert(customers, [id-2, name-'Bob']), + + sql_insert(orders, [id-101, customer_id-1, product-'Widget']), + sql_insert(orders, [id-102, customer_id-1, product-'Gadget']), + sql_insert(orders, [id-103, customer_id-2, product-'Gizmo']), + + format('~nSimulated JOIN: Find customer names with their orders~n', []), + format('~nQuery: customers JOIN orders ON customers.id = orders.customer_id~n', []), + format('~nResults:~n', []), + + forall(( + customers :: [id-CustId, name-CustName], + orders :: [customer_id-CustId, product-Product] + ), format(' ~w ordered: ~w~n', [CustName, Product])). + +%% --------------------------------------------------------------------------- +%% Demo 6: Event Replay for Audit +%% --------------------------------------------------------------------------- + +demo_audit :- + format('~n=== Demo 6: Event Replay for Audit Trail ===~n', []), + + clear_events, + + create_table(sensitive_data, [col(id, integer), col(ssn, varchar), col(accessed_by, varchar)]), + + % Simulate access log through events + sql_insert(sensitive_data, [id-1, ssn-'XXX-XX-1234', accessed_by-'system']), + sql_update(sensitive_data, [accessed_by-'alice'], [id-1]), + sql_update(sensitive_data, [accessed_by-'bob'], [id-1]), + sql_update(sensitive_data, [accessed_by-'alice'], [id-1]), + + format('~nFull audit trail from events:~n', []), + get_events(Events), + forall(member(event(_, Timestamp, Term), Events), ( + ( Term = row_updated(_, _, Old, New) + -> member(accessed_by-OldBy, Old), + member(accessed_by-NewBy, New), + format_time(string(TimeStr), '%Y-%m-%d %H:%M:%S', Timestamp), + format(' [~w] Access changed: ~w -> ~w~n', [TimeStr, OldBy, NewBy]) + ; true + ) + )), + + format('~nThis complete audit trail is impossible to achieve with~n', []), + format('mutable state - but trivial with event sourcing!~n', []). + +%% --------------------------------------------------------------------------- +%% Run All Demos +%% --------------------------------------------------------------------------- + +run_demo :- + format('~n****************************************~n', []), + format('* SQL as Event Sourcing Demonstration *~n', []), + format('****************************************~n', []), + format('~nThis demonstration shows how every SQL database is~n', []), + format('fundamentally an event sourcing system. The current~n', []), + format('state is merely a projection of all past events.~n', []), + + demo_basic_crud, + demo_query_syntax, + demo_time_travel, + demo_transactions, + demo_relationships, + demo_audit, + + format('~n****************************************~n', []), + format('* End of Demonstration *~n', []), + format('****************************************~n~n', []). + +%% --------------------------------------------------------------------------- +%% Unit Tests +%% --------------------------------------------------------------------------- + +:- use_module(library(plunit)). + +:- begin_tests(sql_es). + +test(create_table) :- + clear_events, + create_table(test_table, [col(id, integer)]), + table_schema(test_table, Schema), + Schema = [col(id, integer)]. + +test(insert_and_select) :- + clear_events, + create_table(t, [col(x, integer)]), + sql_insert(t, [x-42]), + sql_select(t, [x-X]), + X = 42. + +test(update) :- + clear_events, + create_table(t, [col(id, integer), col(val, varchar)]), + sql_insert(t, [id-1, val-'old']), + sql_update(t, [val-'new'], [id-1]), + sql_select(t, [val-V], [id-1]), + V = 'new'. + +test(delete) :- + clear_events, + create_table(t, [col(id, integer)]), + sql_insert(t, [id-1]), + sql_insert(t, [id-2]), + sql_delete(t, [id-1]), + findall(X, sql_select(t, [id-X]), Xs), + Xs = [2]. + +test(transaction_commit) :- + clear_events, + create_table(t, [col(x, integer)]), + begin_transaction, + sql_insert(t, [x-100]), + commit_transaction, + sql_select(t, [x-X]), + X = 100. + +test(transaction_rollback) :- + clear_events, + create_table(t, [col(x, integer)]), + sql_insert(t, [x-1]), + begin_transaction, + sql_insert(t, [x-2]), + rollback_transaction, + findall(X, sql_select(t, [x-X]), Xs), + Xs = [1]. + +test(cql_syntax) :- + clear_events, + create_table(items, [col(name, varchar)]), + sql_insert(items, [name-'test']), + items :: [name-N], + N = 'test'. + +test(event_count) :- + clear_events, + create_table(t, [col(x, integer)]), + sql_insert(t, [x-1]), + sql_insert(t, [x-2]), + sql_update(t, [x-10], [x-1]), + get_events(Events), + length(Events, 4). % 1 create + 2 inserts + 1 update + +:- end_tests(sql_es). + +%% Run tests +run_tests_sql :- + run_tests(sql_es).