Designing a Robust Slow-Changing Ownership Model in PostgreSQL and PHPRunner

This is the second iteration of a former post looking to tighten up a pattern I originally demonstrated with planning application history but here I am trying to implement with asset ownership.

One of the most reliable architectural patterns for handling changing ownership, responsibility, or allocation in business systems is to model changes as dated events rather than continually updating a single field.

This works exceptionally well for:

asset ownership
cost centre allocation
property responsibility
infrastructure stewardship
equipment assignment
operational accountability

The pattern becomes especially powerful when combined with:

UTC timestamps
timezone-aware user input
append-only history
future-dated ownership changes
user audit tracking
soft deletion

The result is a highly scalable and audit-friendly design that works extremely well in PostgreSQL and PHPRunner applications.

The Core Principle

The primary asset table stores the original ownership position:

accountcode_initial

Any subsequent ownership changes are stored separately in a history table.

This means:

the original ownership remains preserved
the complete ownership history is retained
future ownership changes can be scheduled safely
full audit history is maintained
deleted rows can be hidden without destroying history
Recommended Table Structure
Primary Asset Table

CREATE TABLE IF NOT EXISTS public.t001assets
(
    gid integer NOT NULL GENERATED ALWAYS AS IDENTITY,

    asset_name varchar(200),

    accountcode_initial varchar(100) NOT NULL,

    softdelete smallint DEFAULT NULL
        CHECK (softdelete IS NULL OR softdelete IN (0,1)),

    created_at timestamptz DEFAULT now(),

    CONSTRAINT pk_t001assets
        PRIMARY KEY (gid)
);

Ownership Change History Table

CREATE TABLE IF NOT EXISTS public.t002account_code_changes
(
    gid integer NOT NULL GENERATED ALWAYS AS IDENTITY,

    pkidt001 integer NOT NULL,

    input_time timestamp without time zone NOT NULL,

    timezone text NOT NULL,

    utc_datetime timestamptz NOT NULL,

    new_owner_accountcode varchar(100) NOT NULL,

    change_notes text,

    user_creator varchar(100) NOT NULL,

    user_editor varchar(100),

    softdelete smallint DEFAULT NULL
        CHECK (softdelete IS NULL OR softdelete IN (0,1)),

    created_at timestamptz DEFAULT now(),

    updated_at timestamptz DEFAULT now(),

    CONSTRAINT pk_t002account_code_changes
        PRIMARY KEY (gid),

    CONSTRAINT fk_t002account_code_changes_t001
        FOREIGN KEY (pkidt001)
        REFERENCES public.t001assets (gid)
);

Why This Pattern Works Well

The asset table contains the original ownership:

accountcode_initial

The history table stores only subsequent changes.

This creates:

immutable ownership history
minimal duplication
future scheduling support
audit-friendly architecture
simplified reporting
operational rollback capability

The system becomes event-driven rather than overwrite-driven.

The Soft Delete Pattern

A particularly useful refinement is adding:

softdelete

to both tables.

Recommended values:

Value Meaning
NULL Active
0 Active
1 Deleted / ignored

This provides several advantages over hard deletion:

historical integrity preserved
accidental deletion recovery
simpler auditing
safer integrations
reduced referential integrity problems

The recommended filtering pattern is:

COALESCE(softdelete,0) <> 1

This treats:

NULL as active
0 as active
1 as deleted
The Timezone Pattern

The ownership change table uses three separate fields:

Field Purpose
input_time What the user entered
timezone Intended timezone
utc_datetime Canonical UTC value

Example:

Field Value
input_time 2026-05-23 10:00
timezone Europe/London
utc_datetime 2026-05-23 09:00:00+00

This preserves both:

user intent
globally consistent ordering
Recommended Timezone Values

Always use official IANA timezone names.

Recommended examples:

Display Name Stored Value
UTC UTC
United Kingdom Europe/London
Central Europe Europe/Paris
Eastern US America/New_York
Central US America/Chicago
Pacific US America/Los_Angeles
Dubai Asia/Dubai
India Asia/Kolkata
Japan Asia/Tokyo
Sydney Australia/Sydney

Avoid ambiguous abbreviations such as:

BST
GMT
EST
PST

because daylight savings behaviour becomes unreliable.

Automatic UTC Calculation

Users should never manually edit:

utc_datetime

It should always be automatically derived.

Trigger function

CREATE OR REPLACE FUNCTION public.fn_t002account_code_changes_set_utc()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN

    IF NEW.input_time IS NULL
       OR NEW.timezone IS NULL
       OR btrim(NEW.timezone) = '' THEN

        NEW.utc_datetime := NULL;

        RETURN NEW;

    END IF;

    IF NOT EXISTS
    (
        SELECT 1
        FROM pg_timezone_names
        WHERE name = NEW.timezone
    )
    THEN
        RAISE EXCEPTION
            'Invalid timezone: %',
            NEW.timezone;
    END IF;

    NEW.utc_datetime :=
        NEW.input_time AT TIME ZONE NEW.timezone;

    RETURN NEW;

END;
$$;

Trigger

CREATE TRIGGER trg_t002account_code_changes_set_utc
BEFORE INSERT OR UPDATE OF input_time, timezone
ON public.t002account_code_changes
FOR EACH ROW
EXECUTE FUNCTION public.fn_t002account_code_changes_set_utc();

Audit Tracking

A major refinement is adding:

user_creator
user_editor
created_at
updated_at

This creates a fully auditable ownership timeline.

The system can then record:

who created the ownership change
who last edited it
when it was created
when it was modified
when it becomes effective
Recommended Audit Trigger

CREATE OR REPLACE FUNCTION public.fn_t002account_code_changes_audit()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN

    IF TG_OP = 'INSERT'
    THEN

        NEW.created_at := now();

        NEW.updated_at := now();

        IF NEW.user_creator IS NULL
        THEN
            NEW.user_creator := current_user;
        END IF;

        NEW.user_editor := NEW.user_creator;

    ELSIF TG_OP = 'UPDATE'
    THEN

        NEW.updated_at := now();

        IF NEW.user_editor IS NULL
        THEN
            NEW.user_editor := current_user;
        END IF;

    END IF;

    RETURN NEW;

END;
$$;

Audit Trigger

CREATE TRIGGER trg_t002account_code_changes_audit
BEFORE INSERT OR UPDATE
ON public.t002account_code_changes
FOR EACH ROW
EXECUTE FUNCTION public.fn_t002account_code_changes_audit();

Supporting Future Ownership Changes

One of the strongest advantages of this design is the ability to safely schedule future ownership changes.

Example:

Asset UTC Datetime Owner Status
Vehicle A 2026-01-01 10:00 ACC100 Historical
Vehicle A 2026-06-01 09:00 ACC200 Current
Vehicle A 2026-09-01 09:00 ACC300 Future

The future row already exists in the database but should not become active until its effective date arrives.

The Critical Requirement

If an asset has:

no ownership change rows
or no effective ownership change rows yet

then the system should return:

accountcode_initial

from:

t001assets

This makes the ownership model operationally complete.

Current Ownership View

CREATE OR REPLACE VIEW public.v001currentassetownership AS

WITH latest_changes AS
(
    SELECT DISTINCT ON (c.pkidt001)

        c.pkidt001,

        c.new_owner_accountcode,

        c.utc_datetime,

        c.user_creator,

        c.user_editor,

        c.updated_at

    FROM public.t002account_code_changes c

    WHERE c.utc_datetime <= now()

      AND COALESCE(c.softdelete,0) <> 1

    ORDER BY
        c.pkidt001,
        c.utc_datetime DESC,
        c.gid DESC
)

SELECT

    a.gid AS pkidt001,

    a.asset_name,

    COALESCE
    (
        lc.new_owner_accountcode,
        a.accountcode_initial
    ) AS current_accountcode,

    lc.utc_datetime AS ownership_change_utc,

    lc.user_creator,

    lc.user_editor,

    lc.updated_at

FROM public.t001assets a

LEFT JOIN latest_changes lc
    ON a.gid = lc.pkidt001

WHERE COALESCE(a.softdelete,0) <> 1;

Why This View Works Well

The logic becomes:

Situation Result
No ownership changes Initial account code
Only future changes Initial account code
Current effective change exists Latest effective change
Multiple historical changes Most recent effective change

The system always derives the current state dynamically from historical events.

Future Ownership View

Future scheduled changes can also be exposed separately.

CREATE OR REPLACE VIEW public.v002futureassetownership AS

SELECT

    c.pkidt001,

    a.asset_name,

    c.new_owner_accountcode,

    c.utc_datetime,

    c.timezone,

    c.user_creator,

    c.user_editor

FROM public.t002account_code_changes c

INNER JOIN public.t001assets a
    ON a.gid = c.pkidt001

WHERE c.utc_datetime > now()

  AND COALESCE(a.softdelete,0) <> 1

  AND COALESCE(c.softdelete,0) <> 1

ORDER BY
    c.pkidt001,
    c.utc_datetime ASC,
    c.gid ASC;

Recommended Index

For performance:

CREATE INDEX idx_t002account_code_changes_asset_utc
ON public.t002account_code_changes
(
    pkidt001,
    utc_datetime DESC
);

This significantly improves ownership lookups.

Recommended Timezone Lookup Table

A very useful refinement is maintaining a dedicated timezone lookup table rather than allowing free-text timezone entry.

This provides:

validation
consistent naming
cleaner PHPRunner dropdowns
simpler administration
safer reporting
Timezone Table

CREATE TABLE IF NOT EXISTS public.t003timezones
(
    gid integer NOT NULL GENERATED ALWAYS AS IDENTITY,

    timezone_name varchar(100) NOT NULL,

    timezone_display varchar(200) NOT NULL,

    timezone_active boolean DEFAULT true,

    created_at timestamptz DEFAULT now(),

    CONSTRAINT pk_t003timezones
        PRIMARY KEY (gid),

    CONSTRAINT uq_t003timezones_name
        UNIQUE (timezone_name)
);

Recommended Seed Data

INSERT INTO public.t003timezones
(
    timezone_name,
    timezone_display
)
VALUES
('UTC', 'UTC'),
('Europe/London', 'United Kingdom'),
('Europe/Paris', 'Central Europe'),
('America/New_York', 'Eastern United States'),
('America/Chicago', 'Central United States'),
('America/Los_Angeles', 'Pacific United States'),
('Asia/Dubai', 'Dubai'),
('Asia/Kolkata', 'India'),
('Asia/Tokyo', 'Japan'),
('Australia/Sydney', 'Sydney');

PHPRunner Lookup Configuration

In PHPRunner the timezone field would normally use:

Setting Value
Link field timezone_name
Display field timezone_display

This allows users to see friendly values such as:

United Kingdom
Central Europe
Eastern United States

while PostgreSQL stores the official IANA timezone identifiers.

Final Thoughts

This pattern combines several very strong architectural principles:

immutable history
UTC canonical timestamps
timezone-aware user entry
append-only ownership changes
future scheduling support
soft deletion
audit tracking
derived current-state views

The result is a highly scalable, operationally safe, and audit-friendly ownership model that performs exceptionally well in PostgreSQL and PHPRunner applications.

Control Lockout – An increasing danger of AI Coding

I regularly read a blog from a programmer called Simon Willison and recently he posted about something he refers to as Cognitive Debt. I prefer the term Control Lockout as it seems to me to reflect the central danger of control loss.

Control Lockout happens when organisations or individuals gradually lose understanding of systems they themselves are responsible for. The distance between those who operate the system and those who build it becomes so wide that the system becomes opaque. It still functions. It may even function brilliantly. But the understanding no longer resides inside the organisation. I personally have seen this occur in several organisations where I have worked.

Complex software stacks, outsourced platforms, and vendor-managed systems have always created this risk. What is new is the velocity. AI coding tools can generate entire architectures in minutes. That acceleration dramatically increases the risk that systems are deployed faster than they are understood.

The danger might not be that AI code is bad. Often it’s good. The danger is that it can bypass the slow, painful process through which humans build mental models. When that learning process is skipped, control migrates away from people and toward tools.

And that is the essence of Control Lockout.

If that risk is real — and I believe it is — what can be done?

1. Reconstruct Mental Models Internally

Even if AI generates the code, teams must be able to explain the system from first principles. Architecture diagrams, walkthrough sessions, internal documentation, and “explain it back” exercises should not be optional.

2. Rotate Ownership

Knowledge must be distributed. If only one developer understands the system — or worse, only the AI does — resilience collapses. Rotating ownership and cross-reviewing code forces shared understanding.

3. Enforce Explainability

If a team cannot clearly describe why something works, how data flows, or where failure points exist, that is a governance issue — not just a technical one. Explainability should be treated as a control requirement.

4. Design for Comprehension

This may be the most important principle. Systems should be structured so they can be understood. Simplicity is not aesthetic; it is strategic. Clean schemas, predictable naming, transparent logic — these are not nostalgic habits. They are safeguards against lockout.

5. Periodically Test Independence

Can the process operate without the system?
Do people know the minimum information required to perform the task?
Could the team reconstruct the workflow from first principles if required?

Simon Willison recently admitted to creating AI projects that he himself did not understand. He also wrote a blog post on it at his great blog Simon Willison Blog He also brought my attention to an academic’s experience of the very same thing Margaret-Anne Storey

PHPRunner – Pass Value to CSS Reference using Javascript in a pop up

Javascript is very powerful and will easily calculate all sorts of interesting things for you dynamically. In PHPRunner I use the popup windows for nearly every table or view form so I wanted it to work with these.

But with PHPRunner we want to store these in the database. I had a devilish time finding a way of referencing the field with which to copy any Javascript value into. After some lengthy discussion with ChatGPT 4 (via Bing) it suggested that I might try and use the CSS Selector.

I then discovered that I couldn’t seem to identify the name of the CSS Selector.

What I discovered is that I could not see a static CSS Selector reference for any of the fields except those that I had altered the formatting on for example changing the font to Roboto Mono.

So first step choose your target field and then alter it using the PHP page designer and then publish.

What I discovered was after that I could use the inspect item to identify the CSS Selector

On the published application navigate to the form and the field you wish to target for entry right click and select inspect.

Look to the DevTools window (in chrome and you should see in bold the css names of your field

You can then ask ChatGPT the following

Can you parse the input css selector I need from the following string that can be used by javascipt to be passed a value

[data-page="t0017_add"][data-itemid="integrated_edit_field4"][data-page="t0017_add"][data-itemid="integrated_edit_field4"][data-page="t0017_add"][data-itemid="integrated_edit_field4"][data-page="t0017_add"][data-itemid="integrated_edit_field4"] > * > * > input

An element is a part of a webpage. In XML and HTML, an element may contain a data item or a chunk of text or an image, or perhaps nothing. A typical element includes an opening tag with some attributes, enclosed text content, and a closing tag. Elements and tags are not the same things.

More on elements is available here

This can now be used behind a button and away we go… see below

document.querySelector('[data-page="t0017_add"][data-itemid="integrated_edit_field4"] > * > * > input').value = "Password123";

And here are some notes on finding CSS Selectors by ChatGPT4 I am still investigating CSS Selectors there seems to be a black art to understanding their structure and how they can be useful

I also asked chatGPT about child selectors in CSS. In my discussions with chatGPT I have discovered that spaces are important and symbols are important in naming and of course Javascript is case sensitive. The dynamic nature of CSS Selectors and their very specific naming conventions combined with spaces potentially being characters really means you need to be on your toes when you use them.

MS SQL Azure – Computed Column – Create UTC Datetime value from Datetime & Timezone fields – Daylight Saving

This is not tested at time of writing obtained from ChatGPT 4

Answer
Sure, here’s an example of how you might implement the ConvertToUTC function to handle time zones like ‘London/Europe’. This example uses the AT TIME ZONE clause, which is available in SQL Server 2016 and later:

CREATE FUNCTION dbo.ConvertToUTC (@localDateTime DATETIME, @timeZone NVARCHAR(50))
RETURNS DATETIME
AS
BEGIN
    -- Convert the local datetime to a datetimeoffset value using the specified time zone
    DECLARE @datetimeoffset DATETIMEOFFSET = @localDateTime AT TIME ZONE @timeZone
    -- Convert the datetimeoffset value to UTC
    DECLARE @utcDateTime DATETIME = CONVERT(DATETIME, SWITCHOFFSET(@datetimeoffset, 0))
    RETURN @utcDateTime
END

You can then use this function in a computed column definition:

ALTER TABLE YourTable
ADD UtcDateTime AS dbo.ConvertToUTC(field01, field02)

In this example, field02 should contain time zone names that are valid for the AT TIME ZONE clause. For example, ‘UTC’, ‘Pacific Standard Time’, ‘Central European Standard Time’, etc. For London, you would use ‘GMT Standard Time’.

Please note that this is just a basic example. The actual implementation would depend on how you’re storing time zone information and the specific requirements of your application. It’s also worth noting that handling date and time conversions can be complex due to factors like daylight saving time, so it’s important to thoroughly test your solution to ensure it handles all edge cases correctly.

If you’re frequently performing these types of conversions, you might want to consider storing datetime values in UTC directly in your database. This can simplify your data model and avoid potential issues with time zone conversions.

Question
Does AT TIME ZONE have a default value that will alter depending on whether the UK is under British Summer time?

Answer
Yes, the AT TIME ZONE clause in SQL Server does take daylight saving time into account. When you use a time zone name with this clause, it will automatically adjust the time based on the current daylight saving time rules for that time zone.

For example, if you use ‘GMT Standard Time’ (which is the time zone name for UK time in SQL Server), it will automatically show the correct offset for daylight saving when it is in effect.

Here’s an example:

SELECT GETDATE() AT TIME ZONE 'GMT Standard Time'

This will return the current date and time on the database BUT in my testing does NOT adjust the time to the timezone but will show the correct time zone OFFSET for the date for corrected for daylight saving i.e Greenwich Mean Time (GMT) or British Summer Time (BST), depending on the date.

Please note that the actual time zone names and their daylight saving time rules are based on the time zone data on the server where SQL Server is running. This data is updated through the Windows operating system, so it’s important to keep your servers up-to-date to ensure that the time zone rules are accurate.

And here are what I think are the supported time zones
MS Azure supported time zone values

Post-script
A lot of online editors won’t let you run the At Time Zone function but this one does..

dofactory

Comparing the time to my location and the time where I am I can tell that this is Pacific Standard Time – i.e Western America – California perhaps.

We also note that the time isn’t adjusted to the stated timezone but we do see the offset (see +01:00 in record return) Again this is because per se it doesn’t tell you what the server is set to but by using the sysdatetimeoffset we can correct the server time back to UTC and the adjust for timezone see second code example. The Timezone although included in the time is again be stated.

SELECT SYSDATETIMEOFFSET() AT TIME ZONE 'W. Europe Standard Time' as WEuropeTime, 
SYSDATETIMEOFFSET() AT TIME ZONE 'GMT Standard Time' as GMTStandard

And so there is a direction to timezone switch – In the above we have mainly been switching from UTC to a timezone but the below switches from a timezone to UTC which is what we will need if we are storing the input as a datetime and a separate timezone for each record.

SELECT SWITCHOFFSET(GetDate() AT TIME ZONE 'Pacific Standard Time', '+00:00')

Remember though timezones are held outside SQL Server databases on the server and as such are non deterministic. This is a good demonstration of determinism in practice

Deterministic algorithms are entirely predictable and always produce the same output for the same input.

Non-deterministic algorithms may produce different outputs for the same input due to random events or other factors.