diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index a065a02..376077b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -25,8 +25,13 @@ jobs: pg_version: [14, 15, 16, 17, 18] os: [ - ubuntu-latest, - ubuntu-24.04-arm, + # Built on the OLDEST runtime we support, not the newest. glibc is + # backward but not forward compatible: a binary linked on 24.04 + # (glibc 2.39) will not load on Debian bookworm (2.36), which is + # what cloudnative-pg and most Postgres images are based on. + # 22.04 gives glibc 2.35, which loads everywhere newer. + ubuntu-22.04, + ubuntu-22.04-arm, macos-latest, ] @@ -85,6 +90,35 @@ jobs: cargo pgrx package --features pg${{ matrix.pg_version }} \ --pg-config "$PG_CONFIG_PATH" + # --- Guard against shipping a binary the targets cannot load ------ + # + # Compiling and linking succeed on the build host regardless; the failure + # only appears at dlopen time on an older runtime, which no test here + # would reach. So assert the requirement directly. + # + # 2.35 is what Ubuntu 22.04 provides. Debian bookworm, the base for + # cloudnative-pg and the official postgres images, has 2.36. + - name: Check glibc requirements (Linux) + if: runner.os == 'Linux' + run: | + set -euo pipefail + MAX_ALLOWED="2.35" + SO="target/release/${{ env.EXTENSION_NAME }}-pg${{ matrix.pg_version }}$(/usr/lib/postgresql/${{ matrix.pg_version }}/bin/pg_config --pkglibdir)/${{ env.EXTENSION_NAME }}.so" + echo "Inspecting ${SO}" + + REQUIRED="$(objdump -T "${SO}" | grep -oE 'GLIBC_[0-9]+\.[0-9]+' | sort -u -V || true)" + echo "Requires: $(echo "${REQUIRED}" | tr '\n' ' ')" + + HIGHEST="$(echo "${REQUIRED}" | sed 's/GLIBC_//' | sort -V | tail -1)" + if [ "$(printf '%s\n%s\n' "${MAX_ALLOWED}" "${HIGHEST}" | sort -V | tail -1)" != "${MAX_ALLOWED}" ]; then + echo "::error::Binary needs glibc ${HIGHEST}, above the ${MAX_ALLOWED} ceiling." + echo "It will fail to load on Debian bookworm (2.36) with 'version GLIBC_... not found'." + echo "Offending symbols:" + objdump -T "${SO}" | grep -E "GLIBC_(2\.3[6-9]|2\.[4-9][0-9])" || true + exit 1 + fi + echo "OK: highest requirement is ${HIGHEST}" + # --- Normalise OS + arch names ----------------------------------- - name: Format OS name for release run: echo "LOWERCASE_OS=$(uname -s | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV diff --git a/Cargo.toml b/Cargo.toml index 1217209..945a95b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "typeid" -version = "0.4.0" +version = "0.4.1" edition = "2024" resolver = "3" diff --git a/sql/typeid--0.4.0--0.4.1.sql b/sql/typeid--0.4.0--0.4.1.sql new file mode 100644 index 0000000..f3d43c8 --- /dev/null +++ b/sql/typeid--0.4.0--0.4.1.sql @@ -0,0 +1,13 @@ +/* + * No schema changes. + * + * 0.4.1 exists only to republish the binaries. The 0.4.0 artifacts were built + * on Ubuntu 24.04 and required GLIBC_2.38/2.39, so they failed to load on + * Debian bookworm (glibc 2.36) — the base for cloudnative-pg and the official + * postgres images — with: + * + * could not load library ".../typeid.so": version `GLIBC_2.38' not found + * + * The release now builds on Ubuntu 22.04 (glibc 2.35) and CI refuses to + * publish a binary requiring anything newer. + */ diff --git a/sql/typeid--0.4.1.sql b/sql/typeid--0.4.1.sql new file mode 100644 index 0000000..0f0ed61 --- /dev/null +++ b/sql/typeid--0.4.1.sql @@ -0,0 +1,505 @@ +/* */ +/* +This file is auto generated by pgrx. + +The ordering of items is not stable, it is driven by a dependency graph. +*/ +/* */ + +/* */ +-- src/lib.rs:60 +-- typeid::typeid_is_valid +CREATE FUNCTION "typeid_is_valid"( + "input" TEXT /* & str */ +) RETURNS bool /* bool */ +IMMUTABLE STRICT PARALLEL SAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'typeid_is_valid_wrapper'; +/* */ + +/* */ +-- src/typeid.rs:113 +-- TypeID +CREATE TYPE TypeID; + +-- src/typeid.rs:113 +-- typeid::typeid::typeid_in +CREATE FUNCTION "typeid_in"( + "input" cstring /* Option < & :: core :: ffi :: CStr > */ +) RETURNS TypeID /* Option < TypeID > */ +IMMUTABLE PARALLEL SAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'typeid_in_wrapper'; + +-- src/typeid.rs:113 +-- typeid::typeid::typeid_out +CREATE FUNCTION "typeid_out"( + "input" TypeID /* TypeID */ +) RETURNS cstring /* :: pgrx :: ffi :: CString */ +IMMUTABLE STRICT PARALLEL SAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'typeid_out_wrapper'; + +-- src/typeid.rs:113 +-- typeid::typeid::typeid_recv +CREATE FUNCTION "typeid_recv"( + "internal" internal /* :: pgrx :: datum :: Internal */ +) RETURNS TypeID /* TypeID */ +IMMUTABLE STRICT PARALLEL SAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'typeid_recv_wrapper'; + +-- src/typeid.rs:113 +-- typeid::typeid::typeid_send +CREATE FUNCTION "typeid_send"( + "input" TypeID /* TypeID */ +) RETURNS bytea /* Vec < u8 > */ +IMMUTABLE STRICT PARALLEL SAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'typeid_send_wrapper'; + + +-- src/typeid.rs:113 +-- TypeID +CREATE TYPE TypeID ( + INTERNALLENGTH = variable, + INPUT = typeid_in, /* typeid::typeid::typeid_in */ + OUTPUT = typeid_out, /* typeid::typeid::typeid_out */ + RECEIVE = typeid_recv, /* typeid::typeid::typeid_recv */ + SEND = typeid_send, /* typeid::typeid::typeid_send */ + STORAGE = extended +); +/* */ + +/* */ +-- src/aggregate.rs:12 +-- typeid::aggregate::type_id_min_type_id_min_combine +CREATE FUNCTION "type_id_min_type_id_min_combine"( + "this" TypeID, /* Option < TypeID > */ + "v" TypeID /* Option < TypeID > */ +) RETURNS TypeID /* Option < TypeID > */ +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'type_id_min_type_id_min_combine_wrapper'; +/* */ + +/* */ +-- src/lib.rs:113 +-- typeid::typeid_ge +CREATE FUNCTION "typeid_ge"( + "a" TypeID, /* TypeID */ + "b" TypeID /* TypeID */ +) RETURNS bool /* bool */ +IMMUTABLE STRICT PARALLEL SAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'typeid_ge_wrapper'; +/* */ + +/* */ +-- src/lib.rs:66 +-- typeid::typeid_prefix +CREATE FUNCTION "typeid_prefix"( + "typeid" TypeID /* TypeID */ +) RETURNS TEXT /* String */ +IMMUTABLE STRICT PARALLEL SAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'typeid_prefix_wrapper'; +/* */ + +/* */ +-- src/lib.rs:108 +-- typeid::typeid_eq +CREATE FUNCTION "typeid_eq"( + "a" TypeID, /* TypeID */ + "b" TypeID /* TypeID */ +) RETURNS bool /* bool */ +IMMUTABLE STRICT PARALLEL SAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'typeid_eq_wrapper'; +/* */ + +/* */ +-- src/aggregate.rs:12 +-- typeid::aggregate::type_id_min_type_id_min_state +CREATE FUNCTION "type_id_min_type_id_min_state"( + "this" TypeID, /* Option < TypeID > */ + "arg_one" TypeID /* TypeID */ +) RETURNS TypeID /* Option < TypeID > */ +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'type_id_min_type_id_min_state_wrapper'; +/* */ + +/* */ +-- src/aggregate.rs:42 +-- typeid::aggregate::type_id_max_type_id_max_state +CREATE FUNCTION "type_id_max_type_id_max_state"( + "this" TypeID, /* Option < TypeID > */ + "arg_one" TypeID /* TypeID */ +) RETURNS TypeID /* Option < TypeID > */ +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'type_id_max_type_id_max_state_wrapper'; +/* */ + +/* */ +-- src/lib.rs:118 +-- typeid::typeid_gt +CREATE FUNCTION "typeid_gt"( + "a" TypeID, /* TypeID */ + "b" TypeID /* TypeID */ +) RETURNS bool /* bool */ +IMMUTABLE STRICT PARALLEL SAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'typeid_gt_wrapper'; +/* */ + +/* */ +-- src/lib.rs:656 +-- typeid::typeid_is_nil_prefix +CREATE FUNCTION "typeid_is_nil_prefix"( + "typeid" TypeID /* TypeID */ +) RETURNS bool /* bool */ +IMMUTABLE STRICT PARALLEL SAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'typeid_is_nil_prefix_wrapper'; +/* */ + +/* */ +-- src/lib.rs:48 +-- typeid::typeid_generate_nil +CREATE FUNCTION "typeid_generate_nil"() RETURNS TypeID /* TypeID */ +STRICT VOLATILE PARALLEL SAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'typeid_generate_nil_wrapper'; +/* */ + +/* */ +-- src/lib.rs:644 +-- typeid::typeid_has_prefix +CREATE FUNCTION "typeid_has_prefix"( + "typeid" TypeID, /* TypeID */ + "prefix" TEXT /* & str */ +) RETURNS bool /* bool */ +IMMUTABLE STRICT PARALLEL SAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'typeid_has_prefix_wrapper'; +/* */ + +/* */ +-- src/lib.rs:129 +-- typeid::typeid_hash +CREATE FUNCTION "typeid_hash"( + "typeid" TypeID /* TypeID */ +) RETURNS INT /* i32 */ +IMMUTABLE STRICT PARALLEL SAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'typeid_hash_wrapper'; +/* */ + +/* */ +-- src/lib.rs:668 +-- typeid::typeid_generate_batch +CREATE FUNCTION "typeid_generate_batch"( + "prefix" TEXT, /* & str */ + "count" INT /* i32 */ +) RETURNS TypeID[] /* Vec < TypeID > */ +STRICT VOLATILE PARALLEL SAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'typeid_generate_batch_wrapper'; +/* */ + +/* */ +-- src/lib.rs:93 +-- typeid::typeid_cmp +CREATE FUNCTION "typeid_cmp"( + "a" TypeID, /* TypeID */ + "b" TypeID /* TypeID */ +) RETURNS INT /* i32 */ +IMMUTABLE STRICT PARALLEL SAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'typeid_cmp_wrapper'; +/* */ + +/* */ +-- src/lib.rs:31 +-- typeid::typeid_generate +CREATE FUNCTION "typeid_generate"( + "prefix" TEXT /* & str */ +) RETURNS TypeID /* TypeID */ +STRICT VOLATILE PARALLEL SAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'typeid_generate_wrapper'; +/* */ + +/* */ +-- src/lib.rs:98 +-- typeid::typeid_lt +CREATE FUNCTION "typeid_lt"( + "a" TypeID, /* TypeID */ + "b" TypeID /* TypeID */ +) RETURNS bool /* bool */ +IMMUTABLE STRICT PARALLEL SAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'typeid_lt_wrapper'; +/* */ + +/* */ +-- src/aggregate.rs:42 +-- typeid::aggregate::type_id_max_type_id_max_combine +CREATE FUNCTION "type_id_max_type_id_max_combine"( + "this" TypeID, /* Option < TypeID > */ + "v" TypeID /* Option < TypeID > */ +) RETURNS TypeID /* Option < TypeID > */ +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'type_id_max_type_id_max_combine_wrapper'; +/* */ + +/* */ +-- src/lib.rs:136 +-- typeid::typeid_hash_extended +CREATE FUNCTION "typeid_hash_extended"( + "typeid" TypeID, /* TypeID */ + "seed" bigint /* i64 */ +) RETURNS bigint /* i64 */ +IMMUTABLE STRICT PARALLEL SAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'typeid_hash_extended_wrapper'; +/* */ + +/* */ +-- src/lib.rs:103 +-- typeid::typeid_le +CREATE FUNCTION "typeid_le"( + "a" TypeID, /* TypeID */ + "b" TypeID /* TypeID */ +) RETURNS bool /* bool */ +IMMUTABLE STRICT PARALLEL SAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'typeid_le_wrapper'; +/* */ + +/* */ +-- src/lib.rs:123 +-- typeid::typeid_ne +CREATE FUNCTION "typeid_ne"( + "a" TypeID, /* TypeID */ + "b" TypeID /* TypeID */ +) RETURNS bool /* bool */ +IMMUTABLE STRICT PARALLEL SAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'typeid_ne_wrapper'; +/* */ + +/* */ +-- src/lib.rs:72 +-- typeid::typeid_to_uuid +CREATE FUNCTION "typeid_to_uuid"( + "typeid" TypeID /* TypeID */ +) RETURNS uuid /* pgrx :: Uuid */ +IMMUTABLE STRICT PARALLEL SAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'typeid_to_uuid_wrapper'; +/* */ + +/* */ +-- src/lib.rs:144 +-- typeid::typeid_uuid_generate_v7 +CREATE FUNCTION "typeid_uuid_generate_v7"() RETURNS uuid /* pgrx :: Uuid */ +STRICT +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'typeid_uuid_generate_v7_wrapper'; +/* */ + +/* */ +-- src/lib.rs:77 +-- typeid::uuid_to_typeid +CREATE FUNCTION "uuid_to_typeid"( + "prefix" TEXT, /* & str */ + "uuid" uuid /* pgrx :: Uuid */ +) RETURNS TypeID /* TypeID */ +IMMUTABLE STRICT PARALLEL SAFE +LANGUAGE c /* Rust */ +AS 'MODULE_PATHNAME', 'uuid_to_typeid_wrapper'; +/* */ + +/* */ +-- src/aggregate.rs:42 +-- typeid::aggregate::TypeIDMax +CREATE AGGREGATE max ( + TypeID /* TypeID */ +) +( + SFUNC = "type_id_max_type_id_max_state", /* typeid::aggregate::TypeIDMax::state */ + STYPE = TypeID, /* Option < TypeID > */ + COMBINEFUNC = "type_id_max_type_id_max_combine", /* typeid::aggregate::TypeIDMax::combine */ + PARALLEL = SAFE /* typeid::aggregate::TypeIDMax::PARALLEL */ +); +/* */ + +/* */ +-- src/aggregate.rs:12 +-- typeid::aggregate::TypeIDMin +CREATE AGGREGATE min ( + TypeID /* TypeID */ +) +( + SFUNC = "type_id_min_type_id_min_state", /* typeid::aggregate::TypeIDMin::state */ + STYPE = TypeID, /* Option < TypeID > */ + COMBINEFUNC = "type_id_min_type_id_min_combine", /* typeid::aggregate::TypeIDMin::combine */ + PARALLEL = SAFE /* typeid::aggregate::TypeIDMin::PARALLEL */ +); +/* */ + +/* */ +-- src/lib.rs:149 +-- finalize + +/* ────────────────────────────────────────────────────────────── + * Implicit cast: text → typeid + * Allows: SELECT 'user_01h…' = id; + * Context: IMPLICIT (works everywhere a typeid is expected) + * Safety: relies on typeid_in for validation; bad literals + * still fail with ERROR. + * ──────────────────────────────────────────────────────────────*/ +CREATE CAST (text AS typeid) + WITH INOUT + AS IMPLICIT; + +/* ────────────────────────────────────────────────────────────── + * Additional utility functions for better SQL integration + * ──────────────────────────────────────────────────────────────*/ + +-- Create an operator for prefix matching to enable efficient queries +-- +-- @< must actually exist. Naming it as the commutator of @> without defining +-- it leaves a shell operator behind, and any query using it fails with +-- "operator is only a shell: text @< typeid". +-- +-- contsel/contjoinsel are the estimators Postgres uses for containment-style +-- operators; without them the planner assumes a prefix test matches half the +-- table. +CREATE FUNCTION typeid_prefix_matches(text, typeid) RETURNS boolean + LANGUAGE sql + IMMUTABLE + PARALLEL SAFE + STRICT + AS $$ SELECT typeid_has_prefix($2, $1) $$; + +COMMENT ON FUNCTION typeid_prefix_matches(text, typeid) IS 'Commutator form of typeid_has_prefix - backs the @< operator'; + +CREATE OPERATOR @> ( + LEFTARG = typeid, + RIGHTARG = text, + PROCEDURE = typeid_has_prefix, + COMMUTATOR = '@<', + RESTRICT = contsel, + JOIN = contjoinsel +); + +CREATE OPERATOR @< ( + LEFTARG = text, + RIGHTARG = typeid, + PROCEDURE = typeid_prefix_matches, + COMMUTATOR = '@>', + RESTRICT = contsel, + JOIN = contjoinsel +); + +-- Create a functional index helper for prefix-based queries +-- Usage: CREATE INDEX idx_user_ids ON users (typeid_prefix(id)) WHERE typeid_has_prefix(id, 'user'); +COMMENT ON FUNCTION typeid_prefix(typeid) IS 'Extract the prefix from a TypeID for indexing and filtering'; +COMMENT ON FUNCTION typeid_has_prefix(typeid, text) IS 'Check if TypeID has a specific prefix - useful for filtering'; +COMMENT ON FUNCTION typeid_is_valid(text) IS 'Validate TypeID format without parsing - useful for constraints'; +COMMENT ON FUNCTION typeid_generate_nil() IS 'Generate TypeID with empty prefix (UUID-only format)'; + +/* Every operator below declares RESTRICT and JOIN. + * + * Without them the planner has no way to estimate how many rows a predicate + * matches, so it falls back to a fixed guess of ~50% of the table — even for + * equality on a unique primary key. At that estimate a sequential scan always + * looks cheaper than an index scan, so indexes on typeid columns are built, + * maintained, and then never used. + * + * These are the same estimators Postgres uses for its own scalar types, and + * they work here because typeid_cmp gives the type a total order (it already + * backs the btree opclass below). + * + * COMMUTATOR/NEGATOR are declared for the ordering operators too, so the + * planner can flip predicates into index-friendly form. + */ + CREATE OPERATOR < ( + LEFTARG = typeid, + RIGHTARG = typeid, + PROCEDURE = typeid_lt, + COMMUTATOR = '>', + NEGATOR = '>=', + RESTRICT = scalarltsel, + JOIN = scalarltjoinsel + ); + + CREATE OPERATOR <= ( + LEFTARG = typeid, + RIGHTARG = typeid, + PROCEDURE = typeid_le, + COMMUTATOR = '>=', + NEGATOR = '>', + RESTRICT = scalarlesel, + JOIN = scalarlejoinsel + ); + + CREATE OPERATOR = ( + LEFTARG = typeid, + RIGHTARG = typeid, + PROCEDURE = typeid_eq, + COMMUTATOR = '=', + NEGATOR = '<>', + RESTRICT = eqsel, + JOIN = eqjoinsel, + HASHES, + MERGES + ); + + CREATE OPERATOR >= ( + LEFTARG = typeid, + RIGHTARG = typeid, + PROCEDURE = typeid_ge, + COMMUTATOR = '<=', + NEGATOR = '<', + RESTRICT = scalargesel, + JOIN = scalargejoinsel + ); + + CREATE OPERATOR > ( + LEFTARG = typeid, + RIGHTARG = typeid, + PROCEDURE = typeid_gt, + COMMUTATOR = '<', + NEGATOR = '<=', + RESTRICT = scalargtsel, + JOIN = scalargtjoinsel + ); + + CREATE OPERATOR <> ( + LEFTARG = typeid, + RIGHTARG = typeid, + PROCEDURE = typeid_ne, + COMMUTATOR = '<>', + NEGATOR = '=', + RESTRICT = neqsel, + JOIN = neqjoinsel + ); + + CREATE OPERATOR CLASS typeid_ops DEFAULT FOR TYPE typeid USING btree AS + OPERATOR 1 < (typeid, typeid), + OPERATOR 2 <= (typeid, typeid), + OPERATOR 3 = (typeid, typeid), + OPERATOR 4 >= (typeid, typeid), + OPERATOR 5 > (typeid, typeid), + FUNCTION 1 typeid_cmp(typeid, typeid); + + CREATE OPERATOR FAMILY typeid_hash_ops USING hash; + + CREATE OPERATOR CLASS typeid_hash_ops DEFAULT FOR TYPE typeid USING hash AS + OPERATOR 1 = (typeid, typeid), + FUNCTION 1 typeid_hash(typeid), + FUNCTION 2 typeid_hash_extended(typeid, bigint); +/* */ +