-- A pure SQL replacement for the `semver` PGXN extension. -- -- The only reason `semver` was ever a dependency is that `fsm.statechart` -- needs a version column that sorts correctly. `semver` is a C extension, so -- depending on it means every user needs a compiler and the PostgreSQL server -- headers just to install pg_statecharts. -- -- An integer array gets that for free. Arrays compare element by element, so -- `order by version desc` is correct with no helper function and no chance of -- a caller getting it wrong: 1.10.0 outranks 1.9.0 because 10 > 9. The cost is -- that the natural text form is '{1,10,0}' rather than '1.10.0'; use -- fsm.semver_text() when a version is being shown to a person. -- -- Everything lives in the fsm schema. That matters for more than tidiness: -- the `semver` extension defines its own to_semver(text) in the schema it is -- installed into, so an unqualified to_semver(text) here would collide with -- it and make `create extension pg_statecharts` fail outright in any database -- that happens to use `semver` for unrelated reasons. The only unqualified -- object is the compatibility shim at the bottom of this file, and it is -- skipped when the name is already taken. -- The check is deliberately thorough. A CHECK that evaluates to NULL passes, -- so `value[2] >= 0` alone would let array[1,null,3] through; and an array -- with a lower bound other than 1 compares unequal to the same elements -- starting at 1, which would let two rows that mean the same version both -- into the unique index on (name, version). -- -- The length test is cardinality(), not array_length(): for the empty array -- '{}' array_ndims, array_lower and array_length are all NULL, so a check -- built only from those is NULL for it and lets it through. cardinality('{}') -- is 0, which makes that term FALSE and the whole conjunction with it. create domain fsm.semver as integer[] check (array_ndims(value) = 1 and array_lower(value, 1) = 1 and cardinality(value) = 3 and array_position(value, null) is null and value[1] >= 0 and value[2] >= 0 and value[3] >= 0); comment on domain fsm.semver is $comment$ A semantic version as a three element integer array: {major, minor, patch}. '1.10.0' is array[1, 10, 0]. Stored as integers rather than text so that ordering is correct by default. Arrays compare element by element, so `order by version desc` ranks 1.10.0 above 1.9.0 with no helper function involved. Build one with fsm.to_semver('1.10.0') and render one with fsm.semver_text(version). Casting to text gives the array form, '{1,10,0}'. Prerelease and build metadata suffixes (1.0.0-rc1, 1.0.0+build5) are not supported. $comment$; create or replace function fsm.to_semver(version text) returns fsm.semver as $$ declare dot_count integer; padded text; begin if version is null then return null; end if; dot_count := length(version) - length(replace(version, '.', '')); if dot_count > 2 then raise exception 'invalid version %', quote_literal(version) using hint = 'expected at most three components, e.g. 1.2.3'; end if; -- pads with '.0' if the provided value has fewer than two dots, so that -- both '1' and '1.2' are accepted and normalised to 1.0.0 and 1.2.0 padded := version || repeat('.0', 2 - dot_count); if padded !~ '^\d+\.\d+\.\d+$' then raise exception 'invalid version %', quote_literal(version) using hint = 'expected digits separated by dots, e.g. 1.2.3'; end if; return string_to_array(padded, '.')::integer[]::fsm.semver; end; $$ language plpgsql immutable parallel safe; comment on function fsm.to_semver(text) is $comment$ Parses text into an fsm.semver, padding out omitted components so that '1' and '1.2' become 1.0.0 and 1.2.0. This is the canonical name. An unqualified to_semver(text) is also created when that name is free, for the benefit of statechart migrations generated against older versions of pg_statecharts. $comment$; create or replace function fsm.semver_text(version fsm.semver) returns text as $$ select array_to_string(version, '.') $$ language sql immutable strict parallel safe; comment on function fsm.semver_text(fsm.semver) is $comment$ Renders a version the way people write it: fsm.semver_text(array[1,10,0]) is '1.10.0'. Casting to text instead gives the array form, '{1,10,0}'. $comment$; -- Compatibility shim. -- -- Statechart migrations generated by pg_statecharts 0.0.0 call an unqualified -- to_semver(...), which back then resolved to the `semver` extension's -- function. Creating one here keeps those migrations deploying, reverting and -- verifying unchanged. -- -- It is conditional because that is exactly the name `semver` occupies. If -- something else already provides to_semver(text) we leave it alone and carry -- on: a database that uses `semver` for its own purposes can still install -- pg_statecharts, it just does not get the shim. Note that in that case those -- 0.0.0 migrations cannot work whatever we do here -- `semver`'s to_semver -- returns its own type, which has no assignment cast to anything we could -- store -- so they have to be regenerated. Migrations generated from 0.1.0 -- onwards call fsm.to_semver(...) and never needed the shim. -- -- Objects created inside a DO block during CREATE EXTENSION are still -- recorded as members of the extension, so this is dropped by DROP EXTENSION -- like any other object here. -- -- The skip is reported at WARNING rather than NOTICE deliberately: PostgreSQL -- runs extension scripts with client_min_messages set to warning, so a NOTICE -- here would never reach whoever ran CREATE EXTENSION. do $shim$ begin if exists ( select 1 from pg_proc p where p.proname = 'to_semver' and p.pronamespace = current_schema()::regnamespace and pg_get_function_identity_arguments(p.oid) = 'text' ) then raise warning 'to_semver(text) already exists in schema %, skipping the pg_statecharts compatibility shim', current_schema() using hint = 'use fsm.to_semver(text) instead; migrations generated by ' 'pg_statecharts 0.0.0 will need regenerating'; return; end if; create function to_semver(version text) returns fsm.semver as $body$ select fsm.to_semver(version) $body$ language sql immutable parallel safe; comment on function to_semver(text) is $c$ Deprecated alias for fsm.to_semver(text), kept so that statechart migrations generated by pg_statecharts 0.0.0 keep working. Not created if the name was already taken, so do not rely on it in new code. $c$; end; $shim$;