[BUG] PIT with Backdated Transactions: Is Balance Filtering Supported in list_accounts API? #1949
-
| Hi team, I'm encountering unexpected behaviour when listing accounts using the My main question:
Context
Reproduction Script (Python)from datetime import datetime, timedelta, timezone from formance_sdk_python import SDK from formance_sdk_python.models import operations, shared current_datetime = datetime.now().strftime("%Y%m%d_%H%M%S") LEDGER_NAME = f"example_{current_datetime}" SERVER_URL = "http://localhost:3068" SEND_EXAMPLE = """ vars { account $source monetary $amount } send $amount ( source = $source allowing unbounded overdraft destination = @world ) """ TRANSACTION_DATE = datetime(2025, 10, 1, 0, 0, second=0, tzinfo=timezone(timedelta(hours=0))) ONE_SECOND_AFTER = datetime(2025, 10, 1, 0, 0, second=1, tzinfo=timezone(timedelta(hours=0))) def run(): client = SDK(server_url=SERVER_URL) # Create a ledger client.ledger.v2.create_ledger( request=operations.V2CreateLedgerRequest( ledger=LEDGER_NAME, v2_create_ledger_request=shared.V2CreateLedgerRequest( features={}, metadata={}, bucket="_default" ), ) ) # Post a backdated transaction client.ledger.v2.create_transaction( request=operations.V2CreateTransactionRequest( ledger=LEDGER_NAME, v2_post_transaction=shared.V2PostTransaction( metadata={}, timestamp=TRANSACTION_DATE, script=shared.V2PostTransactionScript( plain=SEND_EXAMPLE, vars={"source": "user:1234", "amount": "USD/2 50"}, ), ), ) ) # 1️⃣ List all accounts print("✅ List all accounts") accounts = client.ledger.v2.list_accounts( request=operations.V2ListAccountsRequest( ledger=LEDGER_NAME, request_body={}, expand="effectiveVolumes" ) ) print(accounts.v2_accounts_cursor_response.cursor.data) # 2️⃣ List all accounts with negative balance print("\n✅ List all accounts with negative balance") accounts = client.ledger.v2.list_accounts( request=operations.V2ListAccountsRequest( ledger=LEDGER_NAME, request_body={"$lt": {"balance[USD/2]": 0}}, expand="effectiveVolumes", ) ) print(accounts.v2_accounts_cursor_response.cursor.data) # 3️⃣ List all accounts at PIT print("\n✅ List all accounts at PIT") accounts = client.ledger.v2.list_accounts( request=operations.V2ListAccountsRequest( ledger=LEDGER_NAME, request_body={}, expand="effectiveVolumes", pit=ONE_SECOND_AFTER ) ) print(accounts.v2_accounts_cursor_response.cursor.data) # 4️⃣ List all accounts at PIT with balance filter print("\n❌ List all accounts at PIT and with balance filter") accounts = client.ledger.v2.list_accounts( request=operations.V2ListAccountsRequest( ledger=LEDGER_NAME, request_body={"$lt": {"balance[USD/2]": 0}}, expand="effectiveVolumes", pit=ONE_SECOND_AFTER, ) ) print(accounts.v2_accounts_cursor_response.cursor.data) if __name__ == "__main__": run()Expected vs Actual Results
Observed SQL QueryWITH "dataset" AS ( SELECT *, row_number() OVER (ORDER BY address ASC) FROM ( SELECT "address", "address_array", "first_usage", "insertion_date", "updated_at", COALESCE(accounts_metadata.metadata, '{}'::jsonb) AS metadata FROM "_default".accounts LEFT JOIN ( SELECT DISTINCT ON (accounts_address) "accounts_address", FIRST_VALUE(metadata) OVER (PARTITION BY accounts_address ORDER BY revision DESC) AS metadata FROM "_default".accounts_metadata WHERE (ledger = 'example_20251011_212737') AND (date <= '2025-10-01T00:00:01Z') ) accounts_metadata ON accounts_metadata.accounts_address = accounts.address WHERE (ledger = 'example_20251011_212737') AND (accounts.first_usage <= '2025-10-01T00:00:01Z') ) dataset WHERE ( SELECT balance < '0' FROM ( SELECT DISTINCT ON (asset) FIRST_VALUE((post_commit_volumes).inputs - (post_commit_volumes).outputs) OVER (PARTITION BY (accounts_address, asset) ORDER BY seq DESC) AS balance FROM "_default".moves WHERE (accounts_address = dataset.address) AND (ledger = 'example_20251011_212737') AND (insertion_date <= '2025-10-01T00:00:01Z') AND (asset = 'USD/2') ) balance ) LIMIT 101 ), "expand0" AS ( WITH "rows" AS ( SELECT DISTINCT ON (accounts_address, asset) "accounts_address", "asset", FIRST_VALUE(post_commit_effective_volumes) OVER (PARTITION BY (accounts_address, asset) ORDER BY effective_date DESC, seq DESC) AS volumes FROM "_default".moves WHERE (accounts_address IN (SELECT address FROM dataset)) AND (ledger = 'example_20251011_212737') AND (effective_date <= '2025-10-01T00:00:01Z') ) SELECT "accounts_address", public.aggregate_objects( json_build_object(asset, json_build_object('input', (volumes).inputs, 'output', (volumes).outputs))::jsonb ) AS effective_volumes FROM rows GROUP BY "accounts_address" ) SELECT * FROM dataset LEFT JOIN expand0 ON expand0.accounts_address = dataset.address ORDER BY "row_number";ObservationIn the SQL, the balance subquery uses insertion_date instead of effective_date: AND (insertion_date <= '2025-10-01T00:00:01Z')This may explain why the results for PIT with balance filtering are not as expected. QuestionShould the balance computation for PIT queries use effective_date instead of insertion_date when applying balance filters (e.g., $lt on balance[...])? Potential Code reference for issue from ledger source. |
Beta Was this translation helpful? Give feedback.
Replies: 4 comments 2 replies
-
| Thanks for the analysis @imtiazmangerah , we'll have a look and get back to you shortly |
Beta Was this translation helpful? Give feedback.
-
| We are checking. In the meantime, you can also retrieve balances using |
Beta Was this translation helpful? Give feedback.
-
| This is going to be fixed in order to have the same behavior as |
Beta Was this translation helpful? Give feedback.
-
| Thank you. I will take a look at the volumes endpoint as a short-term workaround, although my goal was to fetch account metadata directly via the API call. Regarding account metadata, I’ve observed another behavior: when posting backdated transactions, the metadata is added with the insertion timestamp. This means that queries at a point-in-time (PIT) before the insertion time do not return the metadata as expected. Here’s a minimal script to reproduce the issue: from datetime import datetime, timedelta, timezone from formance_sdk_python import SDK from formance_sdk_python.models import operations, shared current_datetime = datetime.now().strftime("%Y%m%d_%H%M%S") LEDGER_NAME = f"example_{current_datetime}" SERVER_URL = "http://localhost:3068" SEND_EXAMPLE = """ vars { account $source monetary $amount } send $amount ( source = $source allowing unbounded overdraft destination = @world ) set_account_meta($source, "foo", "bar") """ TRANSACTION_DATE = datetime(2025, 10, 1, 0, 0, second=0, tzinfo=timezone(timedelta(hours=0))) ONE_SECOND_AFTER = datetime(2025, 10, 1, 0, 0, second=1, tzinfo=timezone(timedelta(hours=0))) def run(): client = SDK(server_url=SERVER_URL) # Create a ledger client.ledger.v2.create_ledger( request=operations.V2CreateLedgerRequest( ledger=LEDGER_NAME, v2_create_ledger_request=shared.V2CreateLedgerRequest( features={}, metadata={}, bucket="_default" ), ) ) # Post a backdated transaction client.ledger.v2.create_transaction( request=operations.V2CreateTransactionRequest( ledger=LEDGER_NAME, v2_post_transaction=shared.V2PostTransaction( metadata={}, timestamp=TRANSACTION_DATE, script=shared.V2PostTransactionScript( plain=SEND_EXAMPLE, vars={ "source": "user:1234", "amount": "USD/2 50", }, ), ), ) ) # List accounts without PIT print("List all accounts") accounts = client.ledger.v2.list_accounts( request=operations.V2ListAccountsRequest( ledger=LEDGER_NAME, request_body={}, ) ) print(accounts.v2_accounts_cursor_response.cursor.data) print("\n\n✅ List all accounts at PIT") accounts = client.ledger.v2.list_accounts( request=operations.V2ListAccountsRequest( ledger=LEDGER_NAME, request_body={}, pit=ONE_SECOND_AFTER, ) ) print(accounts.v2_accounts_cursor_response.cursor.data) if __name__ == "__main__": run()Could you confirm whether this is the expected behavior for metadata historization? |
Beta Was this translation helpful? Give feedback.
Fixed in v2.3.1 to take into account effective_date
cc @imtiazmangerah